I am using React Native for a game (perhaps not the best framework but not the point). I need user to be able to slide on a grid. On the pic, user is selecting MKEC without removing its finger from screen.
I am from the web, so I thought I would use onHover event. But it dossn't exist on mobile. So I created a parent component and a grid component. Parent component catch event with PanResponder, and I calculate where user finger is on the grid, and change state of the grid component.
However, if user is going too fast from on letter to another, it sometimes 'skip' a letter. If you go from M to G (first column) very fast, it seems that not event is triggered on H. Am I clear ? User feels latency.
componentWillMount() {
const onRelease = (evt, gestureState) => {
this.props.validateWord();
};
const onMove = (evt, gestureState: GestureState) => {
const obj = Object.assign({}, {
pageX: evt.nativeEvent.pageX,
pageY: evt.nativeEvent.pageY
});
const pos = {
x: evt.nativeEvent.pageX - this.position.pageX,
y: evt.nativeEvent.pageY - this.position.pageY
}
const whatSquare = whatSquareIsTouched(
pos,
defaultLetter.width, // size of letter
defaultLetter.width * 0.22 // padding for diagonale
);
const letter = this.props.gridReducer[whatSquare.y]
&& this.props.gridReducer[whatSquare.y][whatSquare.x] ?
this.props.gridReducer[whatSquare.y][whatSquare.x] : null;
// if letter => if touch is on a letter and not outside of grid comp
if (letter) {
this.props.letterTouched(letter, whatSquare.insidePadding);
}
}
this._panResponder = createResponder(onMove, onRelease);
}
How could I improve my code ? Or perhaps go another way to tackle this problem?
Update after comment :
I have not solved the problem yet. But i tried the following. I assumed that when user change direction, a new event is trigger. So I could calculate the finger path. If user starts at M and finish in G without changing direction, then H has been hovered.
Related
Is there a way to require that a layout doesn't change the position of one "root" node while considering it during the algorithm ? Or equivalently, is there a way to always center the camera to this node/keep the camera at the same relative position to this node ?
A bit of context. I am working on an iteratively built graph. Each time parts are added to the graph, the layout is completed. The graph may grow too big to be printed on a screen, and alternatives to the fit options are welcome. What is important though, is that the user is able to follow the node he selected. The best would be that this node doesn't move.
Here is an outline of the general approach. It can be applied with whatever strategy you like re. zoom and pan. The main thing is that you need to know the start and end positions of each node. So you run the layout in a batch, creating a visual nop, and then run a preset layout with the desired zoom and pan.
const clone = obj => Object.assign({}, obj);
const savePos1 = n => n.scratch('_layoutPos1', clone(n.position()));
const savePos2 = n => n.scratch('_layoutPos2', clone(n.position()));
const restorePos1 = n => n.position(n.scratch('_layoutPos1'));
const getPos2 = n => n.scratch('_layoutPos2');
cy.startBatch();
const nodes = cy.nodes();
const layout = cy.layout(myLayoutOptions); // n.b. animate:false
const layoutstop = layout.promiseOn('layoutstop');
nodes.forEach(savePos1);
layout.run();
await layoutstop;
nodes.forEach(savePos2);
nodes.forEach(restorePos1);
cy.endBatch();
cy.layout({
name: 'preset',
animate: true,
positions: getPos2,
// specify zoom and pan as desired
zoom,
pan
}).run();
I'm trying to make an SVG element draggable inside but PanResponder doesn't account for the SVG viewBox causing the dragged element to not follow the touch around the screen.
I've tried utilizing the X and Y values of the touch event to affect the X and Y value of the SVG element, but get a worse effect than when I use the PanResponder. When I switched to using just the X and Y values of the touch event I tried to convert the touch event X and Y to the SVG coordinates by using an equation I found on svgwg.org.
viewBox: "0 0 960.1 1856.51"
Code used when I was converting X Y values to SVG cordinates
svgXYConvert = (eX, eY) =>{
let eW = this.props.width;
let eH = this.props.height;
let [vbX, vbY, vbW, vbH] = this.props.viewBox.split(" ");
// Calculate the scale of elements and vb
let scaleX = eW/vbW;
let scaleY = eH/vbH;
// translation points
let translateX = eX - (vbX * scaleX);
let translateY = eY - (vbY * scaleY);
return [translateX, translateY];
}
PanResponder component
import React from 'react';
import {PanResponder, Animated} from 'react-native';
import {Image, G, Rect} from 'react-native-svg';
const AnimatedG = Animated.createAnimatedComponent(G);
class DragImg extends React.Component {
constructor(props) {
super(props)
this.state = {
pan: new Animated.ValueXY({x: this.props.x, y: this.props.y}),
x: this.props.x,
y: this.props.y,
}
}
componentWillMount() {
this._panResponder = PanResponder.create({
onPanResponderTerminationRequest: () => false,
onStartShouldSetPanResponder: (e, gesture) => true,
onPanResponderGrant: (e, gesture) => {
this.state.pan.setOffset(this.state.pan.__getValue());
},
onPanResponderMove: Animated.event([
null, { dx: this.state.pan.x, dy: this.state.pan.y}
])
},
onPanResponderRelease: (e) => {
this.state.pan.flattenOffset();
}
})
}
render(){
let imgStyle = {transform: this.state.pan.getTranslateTransform()};
return <AnimatedG style={imgStyle} {...this._panResponder.panHandlers} x={this.state.x} y={this.state.y}>
<Image href={this.props.href} height={this.props.height} width={this.props.width} />
<Rect fill="transparent" height={this.props.height} width={this.props.width}/>
</AnimatedG>
}
}
export default DragImg;
When this is rendered inside of an SVG that is 100% height of the device, I am forcing landscape on an iPad, I allow them to zoom the SVG that is being used to see the details clearer. If they zoom in where the SVG takes up the entire screen the element moves at the right speed with the PanResponder and follows the touch perfectly almost, but if they are zoomed out or at the default screen we show them where the entire SVG is visible the dragged element moves slowly and doesn't track well.
I'm not sure what I should be doing differently to get the SVG element to track well regardless of if they are zoomed in or out on the SVG.
I made some similar code. I added an Animated.View with position: absolute so I move it and in the same time the SVG Circleis moved inside of the SVG area. To do it, I scaled the SVG size to have a perfect size with the maxWidth and minWidth. Also, the SVG must have this property: preserveAspectRatio="xMinYMin"its because it need to start in (0,0) as start point.
You can see it working here: https://snack.expo.io/#albertcito/svg-pan-responder
I have a drawer that hovers over a view. The user can vertically drag up to open and drag down to close it. I have the opening and closing part working smoothly. What I have a problem with is making sure that the drag animation stops once it reaches about 200 pixels from the top of the phone screen and also not drag beyond 100 pixels from the bottom of the screen. It's an absolute element and I have used Animated.ValueXY(). I know that I need to stop animation in the onPanResponderMove: (e, gestureState) =>{} function. I tried stopAnimation but it doesn't seem to affect anything.
This is what causes the drag to happen -
onPanResponderMove: (e, gestureState) => {
return Animated.event([null, {
dy: this.state.drag.y,
}])(e, gestureState)
},
Using {...this.panResponder.panHandlers} on the view that users can drag to drag the whole drawer. Like a handle.
Using this.state.drag.getLayout() in styles, on the view that I want to drag in response to dragging the 'handle'.
Any response is appreciated!
1st) you need to know the screen size..
Importing { Dimensions } from 'react-native' would give that for you
import { Dimensions } from 'react-native'
Dimensions.get('window').height;
Dimensions.get('window').width;
Dimensions.get('screen').height;
Dimensions.get('screen').width;
2nd) If you do a console.log( gestures ) inside 'onPanResponderMove', you would see something like this
{
stateID: 0.04776943042437365,
moveX: 140.58839416503906, //the pixel where the "finger" is at X
moveY: 351.9721374511719, //the pixel where the "finger" is at Y
x0: 89.08513641357422, //the pixel where finger touched first in X
y0: 390.5161437988281, //the pixel where finger touched first in Y
dx: 51.503257751464844, //distance finger dragged X
dy: -38.54400634765625, //distance finger dragged Y
veJS: dy: -38.54400634765625,
vx: 0.0880593692555147,
vy: 0.06345143037683823,
numberActiveTouches: 1,
_accountsForMovesUpTo: 7017915
}
3rd) With this information you can manipulate, limiting as you like, like this:
//...
const [pan, setPan] = React.useState(new Animated.ValueXY());
//...
//to start to drag only after a 10% drag threshold
const DRAG_THRESHOLD = Dimensions.get('screen').height* 0.1;
//to limit the drag on 80% of the screen height
const DRAG_LIMIT = Dimensions.get('screen').height* 0.8
onPanResponderMove: (e, gesture) => {
console.log(gesture);
//for THRESHOLDs
if ( (Math.abs( gesture.dy ) > DRAG_THRESHOLD) &&
(Math.abs( gesture.dy ) < DRAG_LIMIT ) )
{
return Animated.event([
null, {dx: 0, dy: pan.y}
]) (e, gesture)
}
},
I hope it helps.
Keep Calm and Happy Coding!
My complete source code for this issue is posted as an Expo app: https://exp.host/#kevinoldlifeway/swipe-scrollview-of-webviews as well as on Github https://github.com/kevinold/swipe-scrollview-of-webviews
I am building a book view in React Native. Using a ScrollView, I would like to swipe left and right to navigate through the pages of a title that could have several hundred to several thousand.
Since that is the case, my goal is to only the minimal amount of data so that the user is able to swipe between pages, seeing the immediately previous and next pages.
I am loading a 3 element array like so:
[Previous, Current, Next]
That would be updated in the state by Redux (not used here to keep simple) and would re-render and refocus the list.
My goal is that my ScrollView is always "centered" on the "Current" page.
Page scrolls to the previous and next page are handled by a handleScroll method which loads the appropriate precomputed array so that the current page stays in focus, but the previous and next pages (offscreen) are updated appropriately.
handleScroll (event) {
//const x = event.nativeEvent.contentOffset.x;
const { activeIndex, scrollTimes } = this.state;
const windowWidth = Dimensions.get('window').width;
const eventWidth = event.nativeEvent.contentSize.width;
const offset = event.nativeEvent.contentOffset.x;
console.log('event width: ', eventWidth);
console.log('event offset: ', offset);
console.log('scrollTimes: ', scrollTimes);
//if (scrollTimes <= 1 ) return;
if (windowWidth + offset >= eventWidth) {
//ScrollEnd, do sth...
console.log('scrollEnd right (nextPage)', offset);
const nextIndex = activeIndex + 1;
console.log('nextIndex: ', nextIndex);
// Load next page
this.loadMore()
} else if (windowWidth - offset <= eventWidth) {
//ScrollEnd, do sth...
console.log('scrollEnd left (prevPage)', offset);
// Load prev page
this.loadPrev()
}
this.setState({ scrollTimes: scrollTimes + 1 });
}
I have tried to balance the "current" page using a combination of:
contentOffset={{ x: width, y: 0 }} on ScrollView
And
componentDidMount() {
// Attempt to keep "center" element in array as focused "screen" in the horizontal list view
this.scrollView.scrollTo({ x: width, y: 0, animated: false });
}
I've also tried to scrollTo in the callback after this.setState, but have not had any luck.
I'm wondering if this "centering" could be accomplished by using Animated.
I gave this a shot but I'm not entirely sure I understood the problem, and I'm not sure how well this would hold up.
Basically I just simplified the handleScroll function significantly. First checking if we were on a scroll completion and if so determining if when we landed on that screen it was the "previous" screen or "next" - do nothing if it's already the middle screen.
I think in your code the issue was that it would fire and load data if it was the middle screen, not just the first or last. Therefore it would fire twice for each transition.
Here's the handleScroll that I think will work for you.
handleScroll (event) {
const offset = event.nativeEvent.contentOffset.x;
const mod = offset % width;
if (mod === 0) { // only transition on scroll complete
if (offset === width * 2) { // last screen
console.log('load more')
this.loadMore();
this.scrollView.scrollTo({ x: width, y: 0, animated: false });
} else if (offset !== width) { // first screen
console.log('load prev')
this.loadPrev();
this.scrollView.scrollTo({ x: width, y: 0, animated: false });
}
}
}
And a Snack demoing it.
I have a list of projects in a Redux store. When new location data arrives from the GPS I'm updating the distance to each project in the list. Currently I'm throttling the update to 5 second intervals.
If the update coincides exactly with a navigation transition (using Ex-navigation) I get a non-smooth transition.
I would like to throttle based on a time interval and on data from InteractionManager.
Here is the current code:
let throttledProjectUpdate = _.throttle((position) => {
dispatch(ProjectState.projectNewLongLat(position.coords.longitude, position.coords.latitude))
}, 5000)
let watchId = navigator.geolocation.watchPosition((position) => {
dispatch(sensorGeoNewPositionAction(position))
throttledProjectUpdate(position)
});
I would like to do something like this:
let throttledProjectUpdate = _.throttle((position) => {
InteractionManager.runAfterInteractions(() =>
dispatch(ProjectState.projectNewLongLat(position.coords.longitude, position.coords.latitude))
);
}, 5000)
let watchId = navigator.geolocation.watchPosition((position) => {
dispatch(sensorGeoNewPositionAction(position))
throttledProjectUpdate(position)
});
But a few things are stopping me:
Is this really possible? I.e is InteractionManager a singleton which I can require from everywhere?
It feels wrong to mix GUI stuff with my background state updating
How should I solved this?