React Konva - undo free draw lines - konvajs

I was following this tutorial on how to build a whiteboard with react and konva and it provides an undo function for shapes but does not work for lines because lines are not added to the layer in the same way. How can I implement undo for free draw line?
EDIT:
To expand on my question, here is the relevant code:
I have a public repo that you can check out (and make a PR if that's easier).
https://github.com/ChristopherHButler/Sandbox-react-whiteboard
I have also have a demo you can try out here:
https://whiteboard-rho.now.sh/
Here is the relevant code
line component:
import Konva from "konva";
export const addLine = (stage, layer, mode = "brush") => {
let isPaint = false;
let lastLine;
stage.on("mousedown touchstart", function(e) {
isPaint = true;
let pos = stage.getPointerPosition();
lastLine = new Konva.Line({
stroke: mode == "brush" ? "red" : "white",
strokeWidth: mode == "brush" ? 5 : 20,
globalCompositeOperation:
mode === "brush" ? "source-over" : "destination-out",
points: [pos.x, pos.y],
draggable: mode == "brush",
});
layer.add(lastLine);
});
stage.on("mouseup touchend", function() {
isPaint = false;
});
stage.on("mousemove touchmove", function() {
if (!isPaint) {
return;
}
const pos = stage.getPointerPosition();
let newPoints = lastLine.points().concat([pos.x, pos.y]);
lastLine.points(newPoints);
layer.batchDraw();
});
};
HomePage component:
import React, { useState, createRef } from "react";
import { v1 as uuidv1 } from 'uuid';
import ButtonGroup from "react-bootstrap/ButtonGroup";
import Button from "react-bootstrap/Button";
import { Stage, Layer } from "react-konva";
import Rectangle from "../Shapes/Rectangle";
import Circle from "../Shapes/Circle";
import { addLine } from "../Shapes/Line";
import { addTextNode } from "../Shapes/Text";
import Image from "../Shapes/Image";
const HomePage = () => {
const [rectangles, setRectangles] = useState([]);
const [circles, setCircles] = useState([]);
const [images, setImages] = useState([]);
const [selectedId, selectShape] = useState(null);
const [shapes, setShapes] = useState([]);
const [, updateState] = useState();
const stageEl = createRef();
const layerEl = createRef();
const fileUploadEl = createRef();
const getRandomInt = max => {
return Math.floor(Math.random() * Math.floor(max));
};
const addRectangle = () => {
const rect = {
x: getRandomInt(100),
y: getRandomInt(100),
width: 100,
height: 100,
fill: "red",
id: `rect${rectangles.length + 1}`,
};
const rects = rectangles.concat([rect]);
setRectangles(rects);
const shs = shapes.concat([`rect${rectangles.length + 1}`]);
setShapes(shs);
};
const addCircle = () => {
const circ = {
x: getRandomInt(100),
y: getRandomInt(100),
width: 100,
height: 100,
fill: "red",
id: `circ${circles.length + 1}`,
};
const circs = circles.concat([circ]);
setCircles(circs);
const shs = shapes.concat([`circ${circles.length + 1}`]);
setShapes(shs);
};
const drawLine = () => {
addLine(stageEl.current.getStage(), layerEl.current);
};
const eraseLine = () => {
addLine(stageEl.current.getStage(), layerEl.current, "erase");
};
const drawText = () => {
const id = addTextNode(stageEl.current.getStage(), layerEl.current);
const shs = shapes.concat([id]);
setShapes(shs);
};
const drawImage = () => {
fileUploadEl.current.click();
};
const forceUpdate = React.useCallback(() => updateState({}), []);
const fileChange = ev => {
let file = ev.target.files[0];
let reader = new FileReader();
reader.addEventListener(
"load",
() => {
const id = uuidv1();
images.push({
content: reader.result,
id,
});
setImages(images);
fileUploadEl.current.value = null;
shapes.push(id);
setShapes(shapes);
forceUpdate();
},
false
);
if (file) {
reader.readAsDataURL(file);
}
};
const undo = () => {
const lastId = shapes[shapes.length - 1];
let index = circles.findIndex(c => c.id == lastId);
if (index != -1) {
circles.splice(index, 1);
setCircles(circles);
}
index = rectangles.findIndex(r => r.id == lastId);
if (index != -1) {
rectangles.splice(index, 1);
setRectangles(rectangles);
}
index = images.findIndex(r => r.id == lastId);
if (index != -1) {
images.splice(index, 1);
setImages(images);
}
shapes.pop();
setShapes(shapes);
forceUpdate();
};
document.addEventListener("keydown", ev => {
if (ev.code == "Delete") {
let index = circles.findIndex(c => c.id == selectedId);
if (index != -1) {
circles.splice(index, 1);
setCircles(circles);
}
index = rectangles.findIndex(r => r.id == selectedId);
if (index != -1) {
rectangles.splice(index, 1);
setRectangles(rectangles);
}
index = images.findIndex(r => r.id == selectedId);
if (index != -1) {
images.splice(index, 1);
setImages(images);
}
forceUpdate();
}
});
return (
<div className="home-page">
<ButtonGroup style={{ marginTop: '1em', marginLeft: '1em' }}>
<Button variant="secondary" onClick={addRectangle}>
Rectangle
</Button>
<Button variant="secondary" onClick={addCircle}>
Circle
</Button>
<Button variant="secondary" onClick={drawLine}>
Line
</Button>
<Button variant="secondary" onClick={eraseLine}>
Erase
</Button>
<Button variant="secondary" onClick={drawText}>
Text
</Button>
<Button variant="secondary" onClick={drawImage}>
Image
</Button>
<Button variant="secondary" onClick={undo}>
Undo
</Button>
</ButtonGroup>
<input
style={{ display: "none" }}
type="file"
ref={fileUploadEl}
onChange={fileChange}
/>
<Stage
style={{ margin: '1em', border: '2px solid grey' }}
width={window.innerWidth * 0.9}
height={window.innerHeight - 150}
ref={stageEl}
onMouseDown={e => {
// deselect when clicked on empty area
const clickedOnEmpty = e.target === e.target.getStage();
if (clickedOnEmpty) {
selectShape(null);
}
}}
>
<Layer ref={layerEl}>
{rectangles.map((rect, i) => {
return (
<Rectangle
key={i}
shapeProps={rect}
isSelected={rect.id === selectedId}
onSelect={() => {
selectShape(rect.id);
}}
onChange={newAttrs => {
const rects = rectangles.slice();
rects[i] = newAttrs;
setRectangles(rects);
}}
/>
);
})}
{circles.map((circle, i) => {
return (
<Circle
key={i}
shapeProps={circle}
isSelected={circle.id === selectedId}
onSelect={() => {
selectShape(circle.id);
}}
onChange={newAttrs => {
const circs = circles.slice();
circs[i] = newAttrs;
setCircles(circs);
}}
/>
);
})}
{images.map((image, i) => {
return (
<Image
key={i}
imageUrl={image.content}
isSelected={image.id === selectedId}
onSelect={() => {
selectShape(image.id);
}}
onChange={newAttrs => {
const imgs = images.slice();
imgs[i] = newAttrs;
}}
/>
);
})}
</Layer>
</Stage>
</div>
);
}
export default HomePage;

As a solution, you should just use the same react modal for lines. It is not recommended to create shape instances manually (like new Konva.Line) when you work with react-konva.
Just define your state and make a correct render() from it, as you do in HomePage component.
You may store all shapes in one array. Or use a separate for lines. So to draw lines in react-konva way you can do this:
const App = () => {
const [lines, setLines] = React.useState([]);
const isDrawing = React.useRef(false);
const handleMouseDown = (e) => {
isDrawing.current = true;
const pos = e.target.getStage().getPointerPosition();
setLines([...lines, [pos.x, pos.y]]);
};
const handleMouseMove = (e) => {
// no drawing - skipping
if (!isDrawing.current) {
return;
}
const stage = e.target.getStage();
const point = stage.getPointerPosition();
let lastLine = lines[lines.length - 1];
// add point
lastLine = lastLine.concat([point.x, point.y]);
// replace last
lines.splice(lines.length - 1, 1, lastLine);
setLines(lines.concat());
};
const handleMouseUp = () => {
isDrawing.current = false;
};
return (
<Stage
width={window.innerWidth}
height={window.innerHeight}
onMouseDown={handleMouseDown}
onMousemove={handleMouseMove}
onMouseup={handleMouseUp}
>
<Layer>
<Text text="Just start drawing" />
{lines.map((line, i) => (
<Line key={i} points={line} stroke="red" />
))}
</Layer>
</Stage>
);
};
Demo: https://codesandbox.io/s/hungry-architecture-v380jlvwrl?file=/index.js
Then the next step is how to implement undo/redo. You just need to keep a history of state changes. Take a look here for demo: https://konvajs.org/docs/react/Undo-Redo.html

If I understand this right you saying that for shapes which are added individually there is an easy 'undo' process, but for lines which use an array of points for their segments, there is no simple undo - and no code in the tutorial you are following?
I can't give you a react code sample but I can explain some of the concepts you need to code up.
The 'freehand line' in your whiteboard is created as a sequence of points. You mousedown and the first point is noted, then you move the mouse and on each movemove event that fires the current mouse position is added to the end of the array. By the time you complete the line and mouseup fires, you have thrown multiple points into the line array.
In the Konvajs line tutorial it states:
To define the path of the line you should use points property. If you
have three points with x and y coordinates you should define points
property as: [x1, y1, x2, y2, x3, y3].
[Because...] Flat array of numbers should work faster and use less memory than
array of objects.
So - your line points are added as separate values into the line.points array.
Now lets think about undo - you are probably there already but I'll write it out anyway - to undo a single segment of the line you need to erase the last 2 entries in the array. To erase the entire line - well you can use the standard shape.remove() or shape.destroy() methods.
In the following snippet the two buttons link to code to 'undo' lines. The 'Undo by segment' button shows how to pop the last two entries in the line.points array to remove a segment of the line, and the 'Undo by line' button removes entire lines. This is not a react example specifically, but you will in the end create something very close to this in your react case.
// Code to erase line one segment at a time.
$('#undosegment').on('click', function(){
// get the last line we added to the canvas - tracked via lines array in this demo
if (lines.length === 0){
return;
}
lastLine = lines[lines.length - 1];
let pointsArray = lastLine.points(); // get current points in line
if (pointsArray.length === 0){ // no more points so destroy this line object.
lastLine.destroy();
layer.batchDraw();
lines.pop(); // remove from our lines-tracking array.
return;
}
// remove last x & y entrie, pop appears to be fastest way to achieve AND adjust array length
pointsArray.pop(); // remove the last Y pos
pointsArray.pop(); // remove the last X pos
lastLine.points(pointsArray); // give the points back into the line
layer.batchDraw();
})
// Code to erase entire lines.
$('#undoline').on('click', function(){
// get the last line we added to the canvas - tracked via lines array in this demo
if (lines.length === 0){
return;
}
lastLine = lines[lines.length - 1];
lastLine.destroy(); // remove from our lines-tracking array.
lines.pop();
layer.batchDraw();
})
// code from here on is all about drawing the lines.
let
stage = new Konva.Stage({
container: 'container',
width: $('#container').width(),
height: $('#container').height()
}),
// add a layer to draw on
layer = new Konva.Layer();
stage.add(layer);
stage.draw();
let isPaint = false;
let lastLine;
let lines = [];
stage.on('mousedown', function(){
isPaint = true;
let pos = stage.getPointerPosition();
lastLine = new Konva.Line({ stroke: 'magenta', strokeWidth: 4, points: [pos.x, pos.y]});
layer.add(lastLine);
lines.push(lastLine);
})
stage.on("mouseup touchend", function() {
isPaint = false;
});
stage.on("mousemove touchmove", function() {
if (!isPaint) {
return;
}
const pos = stage.getPointerPosition();
let newPoints = lastLine.points().concat([pos.x, pos.y]);
lastLine.points(newPoints);
layer.batchDraw();
});
body {
margin: 10;
padding: 10;
overflow: hidden;
background-color: #f0f0f0;
}
#container {
border: 1px solid silver;
width: 500px;
height: 300px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://unpkg.com/konva#^3/konva.min.js"></script>
<p>Click and drag to draw a line </p>
<p>
<button id='undosegment'>Undo by segment</button> <button id='undoline'>Undo by line</button>
</p>
<div id="container"></div>

Related

Are there any issues when using Konva on iOS mobile devices?

I am using a Konva stage to hover over floor areas on browser (see running Next app here https://www.planpoint.io/themes/modern-1) and it works great on any device, except on iPhones. After some touches on the floors areas, they suddenly dissapear as if the canvas element would no longer be there. Has tested it with Safari and Chrome on iOS16.2 . The only difference for mobile devices is the use of touchstart, touchend events, instead of mouseover, mouseout events.
Here is the code for rendering the canvas component and background image
import { useEffect, useState, useRef, useLayoutEffect } from 'react'
import Image from 'next/image'
import Konva from 'konva';
import i18nService from '../../../helpers/i18nService'
import useWindowSize from '../../../hooks/useWindowSize'
export default function InteractiveCanvas(props) {
const [showCanvas, setShowCanvas] = useState(false);
const [placeholderWidth, setPlaceholderWidth] = useState(1);
const [placeholderHeight, setPlaceholderHeight] = useState(1);
const [width, height] = useWindowSize();
const canvasAreaRef = useRef(null)
const canvasPlaceholderRef = useRef()
let stage, shapesLayer, tooltipLayer;
useEffect(() => {
if (showCanvas) {
props.loaded() // Canvas has loaded
}
},[showCanvas])
useLayoutEffect(() => { initializeCanvas() });
useEffect(() => { initializeCanvas() }, [width, height]);
function initializeCanvas() {
if (!canvasPlaceholderRef.current || !canvasPlaceholderRef.current.clientWidth || !canvasPlaceholderRef.current.clientHeight || !canvasPlaceholderRef.current.clientWidth) {
// wait until the placeholder is available
setTimeout(function() { initializeCanvas() }, 1000)
} else if (canvasPlaceholderRef.current) {
// initialize placeholder
setPlaceholderWidth(canvasAreaRef.current.clientWidth)
setPlaceholderHeight(canvasPlaceholderRef.current.clientHeight)
// initialize konva
let width = canvasPlaceholderRef.current.clientWidth;
let height = canvasPlaceholderRef.current.clientHeight;
stage = new Konva.Stage({
container: props.konvaContainer,
width: width,
height: height,
name: 'stage'
});
shapesLayer = new Konva.Layer({ name: 'shapes' });
tooltipLayer = new Konva.Layer({ name: 'tooltips' });
let tooltip = new Konva.Label({
opacity: 1,
visible: false,
listening: false,
name: 'label'
});
tooltip.add(
new Konva.Tag({
fill: '#313131',
pointerDirection: 'down',
pointerWidth: 20,
pointerHeight: 10,
cornerRadius: 4,
lineJoin: 'round',
shadowColor: 'black',
shadowBlur: 10,
shadowOffsetX: 10,
shadowOffsetY: 10,
shadowOpacity: 0.25,
name: 'tag'
})
);
tooltip.add(
new Konva.Text({
text: '',
align: 'center',
lineHeight: 2,
fontFamily: 'Inter',
fontSize: 13,
padding: 5,
fill: 'white',
name: 'tag',
width: props.wideTooltip ? 160 : 80
})
);
tooltipLayer.add(tooltip);
let areas = getData();
// draw areas
for (let key in areas) {
let area = areas[key];
let shape = new Konva.Line({
stroke: 'white',
strokeWidth: 2,
points: area.points,
fill: area.color,
opacity: area.disabled || area.visible ? 1 : key === props.selected?.name ? 1 : 0, // Keep the selected floor/unit highlight shape with opacity 1 after render
disabled: area.disabled,
closed: true,
target: area.target,
key: key,
subline: area.subline,
perfectDrawEnabled: false,
name: 'line',
cursor: 'pointer'
});
let group = new Konva.Group({name: 'group'})
shapesLayer.add(group);
group.add(shape);
}
stage.add(shapesLayer);
stage.add(tooltipLayer);
stage.on('mouseover touchstart', function (evt) {
if (evt && evt.target) {
let shape = evt.target;
if (shape && !shape.attrs.editable) {
shape.opacity(1);
shapesLayer.draw();
if (evt.type === 'touchstart' && shape.attrs.name === 'line') props.onClick(shape.attrs.target)
}
}
});
stage.on('mouseout touchend', function (evt) {
if (evt && evt.target) {
let shape = evt.target;
if(shape.attrs.name === 'stage' || shape.attrs.target === props.selected?._id) return // Do nothing unless it is a different line shape
if (shape && !shape.attrs.editable) {
shape.opacity(shape.attrs.disabled || shape.attrs.visible ? 1 : 0);
shapesLayer.draw();
tooltip.hide();
tooltipLayer.draw();
}
}
});
stage.on('mousemove', function(evt) {
let shape = evt.target;
if (shape) {
let mousePos = stage.getPointerPosition();
let x = mousePos.x;
let y = mousePos.y - 5;
updateTooltip(tooltip, x, y, shape.attrs.key, shape.attrs.subline);
tooltipLayer.draw();
}
});
stage.on('click tap', function(evt) {
let shape = evt.target;
if(shape.attrs.name === 'stage' || shape.attrs.target === props.selected?._id) return // Do nothing unless it is a different line shape
if (shape && shape.attrs.target) props.onClick(shape.attrs.target)
})
setTimeout(() => {
setShowCanvas(true)
shapesLayer.draw();
tooltipLayer.draw();
}, 100)
}
}
function getData() {
let areas = {}
function transformPoints(points) {
const c = JSON.parse(points || '[]')
return c.map((e, i) => (i % 2) ? (placeholderWidth * e) : placeholderHeight * e)
}
// add inactive paths
for (let p of props.inactivePaths) {
let newData = {
target: p.target,
color: p.disabled ? (props.disabledColor || '#E1171799') : props.accentColor || 'rgba(15, 33, 49, 0.65)',
points: transformPoints(p.path),
visible: p.visible,
disabled: p.disabled
}
if (props.project && props.project.showFloorOverview) {
let availableUnits = p.units.filter(u => u.availability.toLowerCase() === 'available').length
newData.subline = props.project.showFloorOverview ? `${availableUnits} ${i18nService.i18n(props.locale, "canvas.unitsavailable")}` : ''
}
areas[p.title] = newData
}
return areas;
}
function updateTooltip(tooltip, x, y, text, subline) {
const conditionalSubline = subline ? `\n${subline}` : ''
tooltip.children[1].text(text+conditionalSubline);
// tooltip.getText().text(text);
tooltip.position({ x: x, y: y });
if (text) tooltip.show();
};
return (
<div className={props.styles.canvasContainer}>
<img
className={props.styles.canvasImgPlaceholder}
ref={canvasPlaceholderRef}
alt='Canvas Placeholder'
src={props.background}
/>
<div className={props.styles.canvasPlaceholderBox} data-hide={showCanvas}>
<Image src="/images/planpoint_icon.svg" width={200} height={200} alt="" />
</div>
<div
className={props.styles.canvasKonvaContainer}
id={props.konvaContainer}
ref={canvasAreaRef}
data-hide={!showCanvas}
style={{backgroundImage: `url('${props.background}')`}}
>
</div>
</div>
)
}

Highcharts react X-range: how to add a custom component?

I have an x-range chart, and I need to add custom components (SVG icons) at the start of each data point ("x" prop value). Something like this (red circles are where custom components should be):
My idea is to place a custom component with position absolute, but I can't figure out how to transform timestamp values of the X axis to pixels. Or maybe there is a better solution?
Codesandbox demo
There are multiple ways to achieve what you want:
A. You can get a chart reference and use Axis.toPixels method to calculate the required coordinates:
export default function Chart() {
const chartComponentRef = useRef(null);
const [x1, setX1] = useState(0);
useEffect(() => {
if (chartComponentRef) {
const chart = chartComponentRef.current.chart;
const xAxis = chart?.xAxis[0];
const x1 = xAxis?.toPixels(Date.UTC(2022, 7, 10));
setX1(x1);
}
}, [chartComponentRef]);
return (
<div style={{ position: "relative" }}>
<HighchartsReact
ref={chartComponentRef}
highcharts={Highcharts}
options={staticOptions}
/>
<div style={{ position: "absolute", bottom: 70, left: x1 }}>ICON</div>
</div>
);
}
Live demo: https://codesandbox.io/s/highcharts-react-custom-componentt-ysjfh8?file=/src/Chart.js
API Reference: https://api.highcharts.com/class-reference/Highcharts.Axis#toPixels
B. You can also get coordinates from a point:
useEffect(() => {
if (chartComponentRef) {
const chart = chartComponentRef.current.chart;
const point = chart.series[0].points[2];
setX1(chart.plotLeft + point.plotX);
}
}, [chartComponentRef]);
Live demo: https://codesandbox.io/s/highcharts-react-custom-componentt-f7nveg?file=/src/Chart.js
C. Use Highcharts API to render and update the custom icons:
const staticOptions = {
chart: {
type: "xrange",
height: 200,
events: {
render: function () {
const chart = this;
const r = 10;
const distance = 25;
chart.series[0].points.forEach((point, index) => {
if (!point.customIcon) {
point.customIcon = chart.renderer
.circle()
.attr({
fill: "transparent",
stroke: "red",
"stroke-width": 2
})
.add();
}
point.customIcon.attr({
x: point.plotX + chart.plotLeft + r,
y:
point.plotY +
chart.plotTop +
(index % 2 ? distance : -distance),
r
});
});
}
}
},
...
};
Live demo: https://codesandbox.io/s/highcharts-react-custom-componentt-f-87eof0?file=/src/Chart.js
API Reference: https://api.highcharts.com/class-reference/Highcharts.SVGRenderer#circle

Konva onDragMove and onDragEnd not updating position?

I'm trying to onDragMove to manually update an elements position. The shape itself is dragging around, and the is updating the objects, but it is not being rendered?
Same with onDragEnd. Both are updating the array of shapes correctly, but it is not appearing on the render, even though
import React, { useState, useEffect } from "react";
import { Stage, Layer, Rect } from "react-konva";
import "./styles.css";
export default function App() {
const [objects, setObject] = useState([{ id: "rect1", x: 50, y: 50 }]);
// Function
const updatePosition = (id) => {
let update = objects.map((entry) => {
if (entry.id !== id) return entry;
else return { ...entry, x: 100, y: 0 };
});
setObject(update);
};
// We can see the object is updated with the new coords
useEffect(() => {
console.log(objects);
});
return (
<main style={{ background: "lightgrey" }}>
<Stage width={800} height={800}>
<Layer>
{objects.map((object) => {
// This shows an updated X value correclty
console.log(object.x);
// It doesn't render the new x position at all
return (
<Rect
key={object.id}
fill={"green"}
width={200}
height={300}
x={object.x}
y={object.y}
draggable
onDragMove={() => updatePosition(object.id)}
onDragEnd={() => updatePosition(object.id)}
/>
);
})}
</Layer>
</Stage>
</main>
);
}
https://codesandbox.io/s/priceless-dust-cjr6z?file=/src/App.js:0-1323
From your demo, I see that you are setting the same {x, y} position to the shape:
const updatePosition = (id) => {
let update = objects.map((entry) => {
if (entry.id !== id) return entry;
else return { ...entry, x: 100, y: 0 };
});
setObject(update);
};
By default react-konva will set 100, 0 position just once. On the next render calls, properties for <Rect /> element are not changing. react-konva will update only CHANGED from previous render properties.
If you want to strictly set the last properties, you should use strict mode

go to view in animated.Scrollview

Using this code i try to add on each marker an onpress option
there is the source source and there is a sample of my work
after many try i give up ... is there a way to add on my onpress the x position to my Animated.ScrollView
when i scroll i can see the marker changing but i want to add an onpress function in each marker. when press one off the marker i want to the scrollview set to the position of my maker
componentWillMount() {
this.index = 0;
this.animation = new Animated.Value(0);
}
componentDidMount() {
// We should detect when scrolling has stopped then animate
// We should just debounce the event listener here
AsyncStorage.getItem('userToken', (err, result) => {
if (this.state.userToken == null) {
this.setState({ userToken: result })
this.GetAllMarker()
}
});
this.animation.addListener(({ value }) => {
console.log(value)
let index = Math.floor(value / CARD_WIDTH + 0.3); // animate 30% away from landing on the next item
if (index >= this.state.markers.length) {
index = this.state.markers.length - 1;
}
if (index <= 0) {
index = 0;
}
clearTimeout(this.regionTimeout);
this.regionTimeout = setTimeout(() => {
if (this.index !== index) {
this.index = index;
const { coordinates } = this.state.markers[index];
console.log(index)
this.map.animateToRegion(
{
...coordinates,
latitudeDelta: this.state.region.latitudeDelta,
longitudeDelta: this.state.region.longitudeDelta,
},
350
);
}
}, 10);
});
}
GenerateBearer() {
let tmp = `Bearer ` + this.state.userToken
tmp = tmp.replace('"', '');
tmp = tmp.replace('"', '');
return (tmp)
}
GetAllMarker() {
let Bearer = this.GenerateBearer();
console.log(Bearer)
fetch(Config.API_URL + "/api/public/user/aroundMe?latitude=" + this.state.region.latitude + "&longitude=" + this.state.region.longitude + "&rayon=50", {
method: 'GET',
headers: {
'Accept': '*/*',
'Content-Type': 'application/json',
'Authorization': Bearer,
}
})
.then(res => res.json())
.then(res => {
this.setState({ markers: res })
})
.catch(error => {
this.setState({ error: error });
});
}
handleMarkerPress(e){
this.state.markers[1] = e
console.log(e)
}
render() {
const interpolations = this.state.markers.map((marker, index) => {
const inputRange = [
(index - 1) * CARD_WIDTH,
index * CARD_WIDTH,
((index + 1) * CARD_WIDTH),
];
const scale = this.animation.interpolate({
inputRange,
outputRange: [1, 2.5, 1],
extrapolate: "clamp",
});
const opacity = this.animation.interpolate({
inputRange,
outputRange: [0.35, 1, 0.35],
extrapolate: "clamp",
});
return { scale, opacity };
});
return (
<View style={styles.container}>
<MapView
ref={map => this.map = map}
initialRegion={this.state.region}
style={styles.container}
>
<UrlTile
urlTemplate="http://ip/styles/klokantech-basic/{z}/{x}/{y}.png"
zIndex={-1}
/>
{this.state.markers.map((marker, index) => {
const scaleStyle = {
transform: [
{
scale: interpolations[index].scale,
},
],
};
const opacityStyle = {
opacity: interpolations[index].opacity,
};
return (
<MapView.Marker key={index} coordinate={marker.coordinates} onPress={(event) => this.handleMarkerPress(index)} >
<Animated.View style={[styles.markerWrap, opacityStyle]} >
<Animated.View style={[styles.ring, scaleStyle]} />
<View style={styles.marker} />
</Animated.View>
</MapView.Marker>
);
})}
</MapView>
<Animated.ScrollView
horizontal
scrollEventThrottle={1}
showsHorizontalScrollIndicator={true}
snapToInterval={CARD_WIDTH}
onScroll={Animated.event(
[{nativeEvent: {
contentOffset: {
x: this.animation,
},
},},],
{ useNativeDriver: true }
)}
style={styles.scrollView}
contentContainerStyle={styles.endPadding}
>
{this.state.markers.map((marker, index) => {
if (marker.isAlerte == false)
return (
<View style={styles.card} key={index}>
<Image
source={marker.image}
style={styles.cardImage}
resizeMode="cover"
/>
<View style={styles.textContent}>
<Text numberOfLines={1} style={styles.cardtitle}>{marker.espace.titre}</Text>
<Text numberOfLines={1} style={styles.cardDescription}>
{marker.description}
</Text>
</View>
</View>)
else
return (
<View style={styles.card} key={index}>
<Image
source={marker.image}
style={styles.cardImage}
resizeMode="cover"
/>
<View style={styles.textContent}>
<Text numberOfLines={1} style={styles.cardtitle}>{marker.alerte.type}</Text>
<Text numberOfLines={1} style={styles.cardDescription}>
{marker.description}
</Text>
</View>
</View>)
})
}
</Animated.ScrollView>
</View>
);
}
}
found the solution
Add
<Animated.ScrollView
horizontal
ref={(c) => {this.scroll = c}}
scrollEventThrottle={1}
showsHorizontalScrollIndicator={true}
snapToInterval={CARD_WIDTH}
onScroll={Animated.event( [{ nativeEvent: { contentOffset: { x: this.animation, }, }, },], { useNativeDriver: true } )}
style={styles.scrollView}
contentContainerStyle={styles.endPadding} >
and this for the map view marker
<MapView.Marker
key={index}
coordinate={marker.coordinates}
onPress={() => this.handleMarkerPress(index)} >
and the handlemarkerpress
this.scroll.getNode().scrollTo({x: e * 375, y: 0, animated: true});
(375 for my card width)
To scroll to a position x,y in your scroll view. Use the scrollTo function in the scroll view. Checkout here the react native documentation about it https://facebook.github.io/react-native/docs/scrollview#scrollto.
Use the ref property to execute the method in your scrollview.
Now you need to identify the x and y of your markers so you can scroll to them. Never done something like that but here is an article of someone calculating the x and y of react native elements https://github.com/facebook/react-native/issues/1374.

React Native multiple panresponders

With this code how would I add a second or multiple panresponders that can be moved independently of each other? If I use the same panresponder instance and code they move together as one. I want to know how to have several independently draggable panresponders.
'use strict';
var React = require('react-native');
var {
PanResponder,
StyleSheet,
View,
processColor,
} = React;
var CIRCLE_SIZE = 80;
var CIRCLE_COLOR = 'blue';
var CIRCLE_HIGHLIGHT_COLOR = 'green';
var PanResponderExample = React.createClass({
statics: {
title: 'PanResponder Sample',
description: 'Shows the use of PanResponder to provide basic gesture handling.',
},
_panResponder: {},
_previousLeft: 0,
_previousTop: 0,
_circleStyles: {},
circle: (null : ?{ setNativeProps(props: Object): void }),
componentWillMount: function() {
this._panResponder = PanResponder.create({
onStartShouldSetPanResponder: this._handleStartShouldSetPanResponder,
onMoveShouldSetPanResponder: this._handleMoveShouldSetPanResponder,
onPanResponderGrant: this._handlePanResponderGrant,
onPanResponderMove: this._handlePanResponderMove,
onPanResponderRelease: this._handlePanResponderEnd,
onPanResponderTerminate: this._handlePanResponderEnd,
});
this._previousLeft = 20;
this._previousTop = 84;
this._circleStyles = {
style: {
left: this._previousLeft,
top: this._previousTop
}
};
},
componentDidMount: function() {
this._updatePosition();
},
render: function() {
return (
<View
style={styles.container}>
<View
ref={(circle) => {
this.circle = circle;
}}
style={styles.circle}
{...this._panResponder.panHandlers}
/>
</View>
);
},
_highlight: function() {
const circle = this.circle;
circle && circle.setNativeProps({
style: {
backgroundColor: processColor(CIRCLE_HIGHLIGHT_COLOR)
}
});
},
_unHighlight: function() {
const circle = this.circle;
circle && circle.setNativeProps({
style: {
backgroundColor: processColor(CIRCLE_COLOR)
}
});
},
_updatePosition: function() {
this.circle && this.circle.setNativeProps(this._circleStyles);
},
_handleStartShouldSetPanResponder: function(e: Object, gestureState: Object): boolean {
// Should we become active when the user presses down on the circle?
return true;
},
_handleMoveShouldSetPanResponder: function(e: Object, gestureState: Object): boolean {
// Should we become active when the user moves a touch over the circle?
return true;
},
_handlePanResponderGrant: function(e: Object, gestureState: Object) {
this._highlight();
},
_handlePanResponderMove: function(e: Object, gestureState: Object) {
this._circleStyles.style.left = this._previousLeft + gestureState.dx;
this._circleStyles.style.top = this._previousTop + gestureState.dy;
this._updatePosition();
},
_handlePanResponderEnd: function(e: Object, gestureState: Object) {
this._unHighlight();
this._previousLeft += gestureState.dx;
this._previousTop += gestureState.dy;
},
});
var styles = StyleSheet.create({
circle: {
width: CIRCLE_SIZE,
height: CIRCLE_SIZE,
borderRadius: CIRCLE_SIZE / 2,
backgroundColor: CIRCLE_COLOR,
position: 'absolute',
left: 0,
top: 0,
},
container: {
flex: 1,
paddingTop: 64,
},
});
module.exports = PanResponderExample;
You can use an array of PanResponders, created like so:
this._panResponders = yourObjectsArray.map((_, index) => (
PanResponder.create({
onMoveShouldSetPanResponder: () => true,
...
})
));
yourObjectsArray is an array that you use for creating as many panResponders as you want, I imagine each object in that array will correspond to a data instance of whatever data structure you use to create the moveable Views.
Then to actually use it in your View:
render: function() {
return yourObjectsArray.map((_, index) => (
<View
style={styles.container}>
<View
... some stuff here ...
{...this._panResponders[index].panHandlers}
/>
</View>
)
};

Resources