Trying to make swiper with React Native PanResponder. When I click again to button after release button starts at begining position. I tried using setOffset in onPanResponderGrant but that makes button exceeds parent container when you scroll it. If sser scrolled 45% of the containers half width I'm animating button to that halfs end.
Here is a link for my code
https://snack.expo.io/#sargnec/react-native-swiper
I think I made it work, with a few changes:
I kept
onPanResponderGrant: () => {
this.pan.setOffset({x: this.pan.x._value});
},handlePanResponderMove
This is important so that, when a user click a second time on your button and make gestureState.dx = 10, you do read 10px, not dx since initial position (after first click)
on handlePanResponderMove, I commented "// this.setState({ xPosition: gestureState.dx })" Your "xPosition" is useful to know where was your point in the beginning, so xPosition + dx move past the limit or not. If you update it on panResponderMove, if you make many very small dx steps, you will reach DRAG_TOP_LIMIT before the end
on onPanResponderRelease: (a) Here, I changed the xPosition, and (b) I do not make a test "gesture.dx > DRAG_TOP_LIMIT", but "xPosition + gesture.dx > DRAG_TOP_LIMIT"
(optional) Actually, your xPosition is not used (and not useful) for the render, so you should remove it from the state and just make this._xPosition
So, here is the code
pan = new Animated.ValueXY();
handlePanResponderMove = (e, gestureState) => {
const currentValue = this.state.xPosition + gestureState.dx
if (currentValue > DRAG_TOP_LIMIT) {
this.pan.setValue({ x: DRAG_TOP_LIMIT })
} else if (currentValue < DRAG_ALT_LIMIT) {
this.pan.setValue({ x: DRAG_ALT_LIMIT })
} else {
this.pan.setValue({ x: gestureState.dx })
}
};
panResponder = PanResponder.create({
onMoveShouldSetPanResponder: () => true,
onPanResponderGrant: () => {
this.pan.setOffset({x: this.pan.x._value});
},
onPanResponderMove: (e, gestureState) => this.handlePanResponderMove(e, gestureState),
onPanResponderRelease: (e, gestureState) => {
this.pan.flattenOffset();
const currPosition = gestureState.dx + this.state.xPosition
if (currPosition >= DRAG_TOP_LIMIT * 0.45) {
Animated.timing(this.pan.x, {
toValue: DRAG_TOP_LIMIT,
duration: 100
}).start()
this.setState({xPosition: DRAG_TOP_LIMIT})
} else if (currPosition <= DRAG_ALT_LIMIT * 0.45) {
Animated.timing(this.pan.x, {
toValue: DRAG_ALT_LIMIT,
duration: 100
}).start()
this.setState({xPosition: DRAG_ALT_LIMIT})
} else {
this.setState({xPosition: this.state.xPosition + gestureState.dx })
}
}
})
Related
I have a list of items that should change state when they are swiped passed a certain threshold. I'm using runOnJs to call a function to change the state. Now when I swipe an item the first time, it updates it's state but every swipe after that does nothing. Can someone please explain to me what I'm missing here?
let [cleaned, setCleaned] = useState(false);
let handleCleanPress = () => {
console.log(clean);
setCleaned(!cleaned);
translateX.value = withTiming(0);
};
let panGesture = useAnimatedGestureHandler<PanGestureHandlerGestureEvent>({
onStart: (_, context) => {
context.startX = translateX.value;
},
onActive: (event, context) => {
let start = context.startX + event.translationX;
if (start < 0) {
translateX.value = start;
}
},
onEnd: () => {
let shouldTriggerClean = translateX.value < translateXThreshold;
translateX.value =
translateX.value >= snapThreshold && translateX.value < -BUTTON_WIDTH
? withTiming(snapPoint, { duration: 200 })
: withTiming(0, { duration: 200 });
if (shouldTriggerClean) {
runOnJS(handleCleanPress)();
}
},
});
Feels a bit wrong doing it like this but it works. Maybe someone can suggest a better way or confirm this is correct?
let setCleanState = () => {
setCleaned(!cleaned);
};
let handleCleanPress = () => {
translateX.value = withTiming(0, { duration: 200 }, (finished) => {
if (finished) {
runOnJS(setCleanState)();
}
});
};
I think part of the problem here may be that you're mixing the "JS in UI Thread"("worklets", translateX.value) with the "Main React Native JS Thread"(setState).
Read more about that [here][1].
You fixed that in your follow-up comment by only using runOnJS on setCleanState. Which I think is why it was working, albeit not reliably.
Did you also remove the withTiming functions in your onEnd() after your comment?
[1]: https://docs.swmansion.com/react-native-reanimated/docs/#:~:text=interactions%20and%20animations,the%20UI%20thread).
I am developing an app where I can add multiple images, which can be dragged accross the whole screen. This works till a certain extend.
But what I am trying to do is saving the image, position (x, y) to firestore. I haven't been able to find some sample code that does that.
Anyone an idea?
Managed to find a code snippet for dragging and dropping a html object. According to this discussion, you can use element.getBoundingClientRect() method and have the shape's positions logged. Should look something like this:
var rect = element.getBoundingClientRect();
console.log(rect.top, rect.right, rect.bottom, rect.left);
From there, for the positions returned. You can follow this documentation on adding data to a collection in Firestore.
I managed to find the solution, as I use panresponder. I get the x and y values and add them to firestore.
This is part of the code that I use:
const pan = useState(new Animated.ValueXY({x: xval, y: yval}))[0];
const panResponder = useState(
PanResponder.create({
onMoveShouldSetPanResponder: () => true,
onPanResponderGrant: () => {
pan.setOffset({
x: pan.x._value,
y: pan.y._value
});
pan.setValue({x: xval, y: yval});
},
onPanResponderMove: Animated.event(
[
null,
{ dx: pan.x, dy: pan.y }
],
{useNativeDriver: false}
),
onPanResponderRelease: () => {
pan.flattenOffset();
stagesRef.doc(parent).collection('stageimages').doc(id)
.update({
x: pan.x._value,
y: pan.y._value,
}).then(function() {
});
}
})
)[0];
The stagesRef is the base for the firestore.
The xval and yval are values coming from anther file.
I'm trying to implement a counter that stops when it has reached a certain amount, but what I have at the moment doesn't stop. It seems to increment continuously; why is this? Does it have something to do with when or how many times useEffect is executed?
const [counter, setCounter] = useState(0);
useEffect(() => {
if (counter < 10) {
const interval = setInterval(() => {
setCounter(counter => counter + 1);
}, incrementRate);
return () => clearInterval(interval);
}
}, []);
return (
<View>
<Text>Circular progress bar</Text>
<Text>{`${counter}`}</Text>
</View>
);
You're using the useState functional updater inside your effect, which means it will use its previous state to increment the count, which is correct but it gives you a false perception that the effect is working correctly. Your mental model for how useEffect works needs to a shift a little, it's really common, so much so Dan Abramov has written an extensive article on useEffect with this exact scenario.
The quick fix is to add counter to your dependency array, so that you can tell React its changed, so it will no longer skip updating the effect.
useEffect(() => {
if (counter < 10) {
const interval = setInterval(() => {
setCounter((counter) => counter + 1);
}, incrementRate);
return () => {
clearInterval(interval);
};
}
}, [counter]);
It's not the cleanest approach as you'll clear the interval and create a new one each time the effect runs.
You could however use setTimeout instead and avoid the clearing of intervals altogether.
useEffect(() => {
if (counter < 10) {
setTimeout(() => {
setCounter((counter) => counter + 1);
}, incrementRate);
}
}, [counter]);
I have a scroll view, which contains list of children(Webview - vertically scrollable). when I try to scroll the Webview vertically, there is a deflection or a small movement in parent - Horizontal direction. I tried passing messages from the children to the parent for disabling or enabling scroll, which is to slow and not 100% perfect. Is there any way to achieve this through props in react-native-scrollview for android.
Thnak you.
I found an answer to my query, by using PanResponder of react-native.
componentWillMount() {
this._panResponder = PanResponder.create({
onMoveShouldSetPanResponder: (evt, gestureState) => {
return Math.abs(gestureState.dx) > minDragWidth;
},
onPanResponderMove: (evt, gestureState) => {
if(!this._swipeDirection) this.checkSwipeDirection(gestureState);
},
onPanResponderRelease: (evt, gestureState) => {
this._swipeDirection = null;
},
});
}
so when a touch event is started, and when the drag continues, these functions will be called respectively.
by checking whether the scroll is horizontal or vertical, we can do the actions.
checkSwipeDirection(gestureState) {
const isHorizontalSwipe = (Math.abs(gestureState.dx) > Math.abs(gestureState.dy * 3)) &&
(Math.abs(gestureState.vx) > Math.abs(gestureState.vy * 3));
if(isHorizontalSwipe) {
this._swipeDirection = "horizontal";
let index = (gestureState.dx > 0) ? this.state.paginationIndex - 1 :
this.state.paginationIndex + 1;
index = (index > 0) ? (index > (this.props.data.length - 1) ? (index - 1) : index) : 0;
this._scrollToIndex(index, true);
this.props.doRequiredAction({ index });
} else {
this._swipeDirection = "vertical";
}
}
by referring to this post.
It is possible to get the current animated value using ._value while nativeDriver is false.
But is it possible to get current value when nativeDriver is turned to true ?
Do we have any work arounds ?
use addListener like so:
this._animatedValue = new Animated.Value(0);
this._animatedValue.addListener(({value}) => this._value = value);
Animated.timing(this._animatedValue, {
toValue: 100,
duration: 500
}).start();
I figured out a workaround for scenarios like this, where the native driver spoils our offset and gives a jump in our animation towards the end.
Create a pseudo animated value (which will not be used for any animation) and keep updating it alongside your original animated value. Finally when your animation is over (like decay, spring, etc.) set the offsets and current angle with the pseudo value as below:
const angle = useRef(new Animated.Value(0)).current; //original value used in animation; will run on native side
const psuedoAngle = useRef(new Animated.Value(0)).current; //only to track the value on JS side
//using PanResponder
...
onPanResponderMove: (evt, gestureState) => {
angle.setValue(some_value);
psuedoAngle.setValue(same_value);
},
onPanResponderRelease: (evt, {vx, vy}) => {
// calculate your velocity
...
const decay1 = Animated.decay(angle,{
velocity: velocity,
deceleration: 0.997,
useNativeDriver: true
}
);
const decay2 = Animated.decay(psuedoAngle,{
velocity: velocity,
deceleration: 0.997,
useNativeDriver: false //this will keep your pseudo value intact
}
);
//set offset and current angle in callback as below
Animated.parallel([decay1,decay2]).start(() => {
psuedoAngle.flattenOffset();
angle.setOffset(psuedoAngle._value); //correct value passed to original value
psuedoAngle.setOffset(psuedoAngle._value);
angle.setValue(0);
psuedoAngle.setValue(0);
});
}