Limit PanResponder movements React Native - react-native

This question is related to
React Native: Constraining Animated.Value
ReactNative PanResponder limit X position
I am trying to build a horizontal slider with a PanResponder. I can move the element on the x-axis with the following code, but I want to limit the range in which I can move it.
This is an annotated example:
export class MySlider extends React.Component {
constructor(props) {
super(props);
this.state = {
pan: new Animated.ValueXY()
};
this._panResponder = PanResponder.create({
onStartShouldSetPanResponder : () => true,
onPanResponderGrant: (e, gestureState) => {
this.state.pan.setOffset(this.state.pan.__getValue());
this.setState({isAddNewSession:true});
},
///////////////////////////
// CODE OF INTEREST BELOW HERE
///////////////////////////
onPanResponderMove: (evt, gestureState) => {
// I need this space to do some other functions
// This is where I imagine I should implement constraint logic
return Animated.event([null, {
dx: this.state.pan.x
}])(evt, gestureState)
},
onPanResponderRelease: (e, gesture) => {
this.setState({isAddNewSessionModal:true});
this.setState({isAddNewSession:false});
}
});
render() {
let { pan } = this.state;
let translateX = pan.x;
const styles = StyleSheet.create({
pan: {
transform: [{translateX:translateX}]
},
slider: {
height: 44,
width: 60,
backgroundColor: '#b4b4b4'
},
holder: {
height: 60,
width: Dimensions.get('window').width,
flexDirection: 'row',
backgroundColor: 'transparent',
justifyContent: 'space-between',
borderStyle: 'solid',
borderWidth: 8,
borderColor: '#d2d2d2'
}
});
const width = Dimensions.get('window').width - 70
return (
<View style={styles.holder}>
<Animated.View
hitSlop={{ top: 16, left: 16, right: 16, bottom: 16 }}
style={[styles.pan, styles.slider]}
{...this._panResponder.panHandlers}/>
</View>
)
}
}
To Limit the Value so that it can not go below 0,I have tried implementing if else logic like so:
onPanResponderMove: (evt, gestureState) => {
return (gestureState.dx > 0) ? Animated.event([null, {
dx: this.state.pan1.x
}])(evt, gestureState) : null
},
but this is buggy - it seems to work initially, but the minimum x limit appears to effectively increase. The more I scroll back and forward, the minimum x-limit seems to increase.
I also tried this:
onPanResponderMove: (evt, gestureState) => {
return (this.state.pan1.x.__getValue() > 0) ? Animated.event([null, {
dx: this.state.pan1.x
}])(evt, gestureState) : null
},
but it doesn't seem to work at all.
How can interpolate the full breadth of the detected finger movement into a limited range I define?

gestureState.dx is the difference the user moved with the finger from it's original position per swipe. So it resets whenever the user lifts the finger, which causes your problem.
There are several ways to limit the value:
Use interpolation:
let translateX = pan.x.interpolate({inputRange:[0,100],outputRange:[0,100],extrapolateLeft:"clamp"})
While this works, the more the user swipes left the more he has to swipe right to get to "real 0"
reset the value on release
onPanResponderRelease: (e, gestureState)=>{
this.state.pan.setValue({x:realPosition<0?0:realPosition.x,y:realPosition.y})
}
make sure you get the current value using this.state.pan.addListener and put it in realPosition
You can allow some swiping left and animate it back in some kind of spring or just prevent it from going off entirely using the previous interpolation method.
But you should consider using something else since PanResponder doesn't support useNativeDriver. Either use scrollView (two of them if you want 4 direction scrolling) which limits scrolling by virtue of it's content or something like wix's react-native-interactable .

I found this post while looking to linked posts at React Native: Constraining Animated.Value. Your problem seems to be similar to what I experienced and my solution was posted there. Basically, dx can get out of bound b/c it is just the accumulated distance and my solution is cap at the pan.setOffset so dx won't get crazy.

This is a workaround solution for your problem or you can use this as an alternative solution. I am not using pan in this solution. The idea is , restrict slider movement inside the parent view. so it won't move over to parent. Consider below code
export default class MySlider extends Component<Props> {
constructor(props) {
super(props);
this.containerBounds={
width:0
}
this.touchStart=8;
this.sliderWidth= 60;
this.containerBorderWidth=8
this.state = {
frameStart:0
};
this._panResponder = PanResponder.create({
onStartShouldSetPanResponder: () => true,
onPanResponderGrant: (e, gestureState) => {
this.touchStart=this.state.frameStart;
this.setState({ isAddNewSession: true });
},
onPanResponderMove: (evt, gestureState) => {
frameStart = this.touchStart + gestureState.dx;
if(frameStart<0){
frameStart=0
}else if(frameStart+this.sliderWidth>this.containerBounds.width-2*this.containerBorderWidth){
frameStart=this.containerBounds.width-this.sliderWidth-2*this.containerBorderWidth
}
this.setState({
frameStart:frameStart
})
},
onPanResponderRelease: (e, gesture) => {
this.setState({ isAddNewSessionModal: true });
this.setState({ isAddNewSession: false });
}
});
}
render() {
const styles = StyleSheet.create({
slider: {
height: 44,
width: this.sliderWidth,
backgroundColor: '#b4b4b4'
},
holder: {
height: 60,
width: Dimensions.get('window').width,
flexDirection: 'row',
backgroundColor: 'transparent',
justifyContent: 'space-between',
borderStyle: 'solid',
borderWidth: this.containerBorderWidth,
borderColor: '#d2d2d2'
}
});
return (
<View style={styles.holder}
onLayout={event => {
const layout = event.nativeEvent.layout;
this.containerBounds.width=layout.width;
}}>
<Animated.View
hitSlop={{ top: 16, left: 16, right: 16, bottom: 16 }}
style={[{left:this.state.frameStart}, styles.slider]}
{...this._panResponder.panHandlers} />
</View>
)
}
}

Like #AlonBarDavid said above, gestureState.dx is the difference the user moved with the finger from it's original position per swipe. So it resets whenever the user lifts the finger, which causes your problem.
One solution is to create a second variable to hold this offset position from a previous touch, then add it to the gestureState.x value.
const maxVal = 50; // use whatever value you want the max horizontal movement to be
const Toggle = () => {
const [animation, setAnimation] = useState(new Animated.ValueXY());
const [offset, setOffset] = useState(0);
const handlePanResponderMove = (e, gestureState) => {
// use the offset from previous touch to determine current x-pos
const newVal = offset + gestureState.dx > maxVal ? maxVal : offset + gestureState.dx < 0 ? 0 : offset + gestureState.dx;
animation.setValue({x: newVal, y: 0 });
// setOffset(newVal); // <- works in hooks, but better to put in handlePanResponderRelease function below. See comment underneath answer for more.
};
const handlePanResponderRelease = (e, gestureState) => {
console.log("release");
// set the offset value for the next touch event (using previous offset value and current gestureState.dx)
const newVal = offset + gestureState.dx > maxVal ? maxVal : offset + gestureState.dx < 0 ? 0 : offset + gestureState.dx;
setOffset(newVal);
}
const animatedStyle = {
transform: [...animation.getTranslateTransform()]
}
const _panResponder = PanResponder.create({
onStartShouldSetPanResponder: () => true,
onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
onMoveShouldSetPanResponder: () => true,
onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
onPanResponderGrant: () => console.log("granted"),
onPanResponderMove: (evt, gestureState) => handlePanResponderMove(evt, gestureState),
onPanResponderRelease: (e, g) => handlePanResponderRelease(e, g)
});
const panHandlers = _panResponder.panHandlers;
return (
<View style={{ width: 80, height: 40 }}>
<Animated.View { ...panHandlers } style={[{ position: "absolute", backgroundColor: "red", height: "100%", width: "55%", borderRadius: 50, opacity: 0.5 }, animatedStyle ]} />
</View>
)
}

Related

Is it not possible to change styles through JS in React Native?

Can't RN do style changes using JS?
...
const PR = PanResponder.create({
onStartShouldSetPanResponder: (e, gestureState) => true,
onPanResponderStart: (e, gestureState) => {
console.log("start");
},
onPanResponderMove: (e, gestureState) => {
const dx = Math.abs(gestureState.dx);
**target.current.style.backgroundColor** = `rgba(${dx},${dx / 2},106,1)`;
},
onPanResponderEnd: (e, gestureState) => {
const dx = Math.abs(gestureState.dx);
if (!dx) {
target.current.style.backgroundColor = "green";
}
console.log("End");
},
});
...
As above, it is difficult to change the style on mobile.
Thank you for your reply.
The useState hook was not what I was expecting.
help me..
In the code above it looks like you were storing the style with useRef, which doesnt trigger component updates when its value changes. Here's a useState example
import * as React from 'react';
import { Text, View, StyleSheet, PanResponder } from 'react-native';
import Animated, {
useSharedValue,
interpolateColor,
} from 'react-native-reanimated';
import Constants from 'expo-constants';
export default function App() {
const [style, setStyle] = React.useState({
borderWidth: 1,
width: '100%',
height: 280,
backgroundColor: 'lightgreen',
});
const PR = PanResponder.create({
onStartShouldSetPanResponder: (e, gestureState) => true,
onPanResponderStart: (e, gestureState) => {
console.log('start');
},
onPanResponderMove: (e, gestureState) => {
const dx = Math.abs(gestureState.dx);
console.log(dx);
setStyle((prev) => ({
...prev,
backgroundColor: `rgba(${dx},${dx / 2},106,1)`,
}));
},
onPanResponderEnd: (e, gestureState) => {
const dx = Math.abs(gestureState.dx);
console.log(dx);
if (!dx) {
setStyle((prev) => ({ ...prev, backgroundColor: 'green' }));
}
console.log('End');
},
});
return (
<View style={styles.container}>
<View style={style} {...PR.panHandlers} />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
paddingTop: Constants.statusBarHeight,
backgroundColor: '#ecf0f1',
padding: 8,
},
paragraph: {
margin: 24,
fontSize: 18,
fontWeight: 'bold',
textAlign: 'center',
},
});
While it works, the best results would come from using Animated.View and some Animated values
Look into this panResponder . It has some example of how to use
PanResponderGestureState
in
react-native

interpolate is not a function

Interpolate function from Animated return is not a function
I am following this, but I am currently doing it as function component
(chapter : "Designing the Tinder Cards Movement"):
https://www.instamobile.io/react-native-controls/react-native-swipe-cards-tinder/
I don't understand why interpolate return "is not a function". I spent a lot of time on it and I didn't find something.
There is the card component I have written :
import React, { useState, useEffect } from 'react'
import { Text, View, Animated, Image, StyleSheet, PanResponder } from 'react-native'
function Card(props){
const { screen_height, screen_width, image, Data, currentIndex, Index } = props;
const [pan, setPan] = useState(new Animated.ValueXY());
const [panResponder, setPanResponder] = useState(PanResponder.create({
onStartShouldSetPanResponder: (evt, gestureState) => true,
onPanResponderMove: (evt, gestureState) => {
setPan({ x: gestureState.dx, y: gestureState.dy });
},
onPanResponderRelease: (evt, gestureState) => {
}
}));
let value = Math.round(pan.x);
if(value !== NaN || value !== undefined){
console.log(value.interpolate({ inputRange: [0, 1], outputRange: [150, 0]}));
}
if (Index < currentIndex) { return null }
else if(Index == currentIndex){
return(
<Animated.View style={[{ transform:[{ translateX: pan.x }, { translateY: pan.y }] },{ height:screen_height - 120, width:screen_width, padding:10, position:'absolute' }]} {...panResponder.panHandlers}>
<Image style={[{
flex:1,
height:null,
width:null,
resizeMode:"cover",
borderRadius:20,
}]} source={image.uri}/>
</Animated.View>)
} else {
return(
<Animated.View style={[{ height:screen_height - 120, width:screen_width, padding:10, position:'absolute' }]}>
<Image style={[{
flex:1,
height:null,
width:null,
resizeMode:"cover",
borderRadius:20,
}]} source={image.uri}/>
</Animated.View>)
}
}
export default Card;
setPan is wrongly used.pan is now a object not an Animated.Value anymore.You should call setValue or using Animated.event on the animated value Instead.
onPanResponderMove: Animated.event([
null,
{
dx: pan.x, // x,y are Animated.Value
dy: pan.y,
},
]),

React Native Collapsible springy Header with PanResponder

There are many examples available from great developers around the world for collapsible header which work based on the scroll value of scrollView. But I'm trying to implement the same by using PanResponder.
Here is what I'm trying to achieve imgur link
in terms of React Native if I explain the screen components we see in the image is a header at the top and a scrollView thereafter (lets skip the directory breadcrumbs)
initially when the screen renders the header stays collapsed, if we slide down the screen the header expands with a springy animation similarly when sliding up the header gets back to initial collapsed state with animation. During the header collapse / expansion the scrollView scroll stays lock until the header animation is complete i.e we have to track gesture via PanResponder.
If we slide up the header to any position that is less than 50% of the header then the header springs back & expands, similarly reverse happens when sliding down from a collapsed condition.
In last few days I went up to the expand & collapse part but can't figure out the springy effect for the header animation now my back is against the wall, any help will be much appreciated
Here is the code
class App extends Component {
constructor(props) {
super(props);
this.state = {
pan: new Animated.ValueXY(),
items: [1,2,3,4,5]
};
this.animY = new Animated.Value(0);
this.lastY = 0;
this._panResponder = PanResponder.create({
onStartShouldSetPanResponder: (evt, gestureState) => true,
onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
onMoveShouldSetPanResponder: (evt, gestureState) => true,
onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
onPanResponderGrant: (evt, gestureState) => {
},
onPanResponderMove: (evt, gestureState) => {
let { dy } = gestureState;
this.animY.setValue(this.lastY + dy);
},
onPanResponderTerminationRequest: (evt, gestureState) => true,
onPanResponderRelease: (evt, gestureState) => {
let { dy } = gestureState;
this.lastY += dy;
},
});
}
handleScroll = () => {
console.log('scroll init');
}
render() {
this.state.pan.addListener((value) => {
console.log('animY', this.animY);
});
const AnimateHeaderHeight = Animated.diffClamp(this.animY, -200, 0).interpolate(
{
inputRange: [ -200, 0 ],
outputRange: [ 50, 200 ],
extrapolate: 'clamp'
});
return (
<View style={{flex: 1}}>
<Animated.View style={[{backgroundColor: '#4287f5', height: AnimateHeaderHeight}]}>
<Text>Sample</Text>
</Animated.View>
<ScrollView style={{flex: 1, backgroundColor: '#e6efff'}}
onScroll={this.handleScroll}
{...this._panResponder.panHandlers}
scrollEnabled={false}
>
{this.state.items.map((item, index) => {
return (
<View style={styles.item} key={index}>
<Text>Sample</Text>
</View>
)
})}
</ScrollView>
</View>
)
}
};
here is the output of my above code

Animated element display not updated after position change

I'm fairly new to React-Native, so it's very likely I'm missing some core concepts.
I want to create a draggable element and be able to move it back to its original position.
The first part is ok, but when I try to update the position, it looks like it works (because when I click again, the element goes back to its original position), but the view isn't updated.
I tried calling setState and forceUpdate but it doesn't update the view.
Do you guys have any idea why ?
Here is a demo of what I have so far :
import React from 'react';
import {Button, StyleSheet, PanResponder, View, Animated} from 'react-native';
export default class Scene extends React.Component {
constructor(props) {
super(props)
const rectanglePosition = new Animated.ValueXY({ x: 0, y: 0 })
const rectanglePanResponder = this.createPanResponder();
this.state = {
rectanglePosition,
rectanglePanResponder
}
}
createPanResponder = () => {
return PanResponder.create({
onStartShouldSetPanResponder: () => true,
onPanResponderMove: (event, gesture) => {
this.state.rectanglePosition.setValue({ x: gesture.dx, y: gesture.dy });
},
onPanResponderRelease: () => {
this.state.rectanglePosition.flattenOffset();
},
onPanResponderGrant: () => {
this.state.rectanglePosition.setOffset({
x: this.state.rectanglePosition.x._value,
y: this.state.rectanglePosition.y._value
});
}
});
}
resetPosition = () => {
const newPosition = new Animated.ValueXY({ x: 0, y: 0 })
this.setState({ rectanglePosition: newPosition }) // I thought updating the state triggered a re-render
this.forceUpdate() // doesn't work either
}
getRectanglePositionStyles = () => {
return {
top: this.state.rectanglePosition.y._value,
left: this.state.rectanglePosition.x._value,
transform: this.state.rectanglePosition.getTranslateTransform()
}
}
render() {
return (
<View style={styles.container}>
<Animated.View
style={[styles.rectangle, this.getRectanglePositionStyles()]}
{...this.state.rectanglePanResponder.panHandlers}>
</Animated.View>
<View style={styles.footer}>
<Button title="Reset" onPress={this.resetPosition}></Button>
</View>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
borderColor: 'red',
backgroundColor: '#F5FCFF',
},
footer: {
borderWidth: 1,
width: '100%',
position: 'absolute',
bottom: 0,
left: 0,
backgroundColor: 'blue',
},
rectangle: {
position: 'absolute',
height: 50,
width: 50,
backgroundColor: 'red'
}
});
If your only intention is to put it on upper left corner:
resetPosition = () => {
this.state.rectanglePosition.setValue({ x: 0, y: 0 });
};
Note! Refer to this snack to see how you do it without a state https://snack.expo.io/#ziyoshams/stack-overflow

Why child component became shorter?

My project is a Animated view based on PanResponder.
The Animated.View has three child components.
When I dragged the animated view to top(and child component out of sight of the screen),the child content became shorter and shorter.
But no problem when dragged to bottom.
What is the matter,please?
Thanks!
=======
screen shot videos:
drag child: no problem
drag child: Problem occur when dragging
=====
source:
complete code:
drag panel
child component
main codes shown:
<View style={styles.draggableContainer}>
<Animated.View
onLayout={this.onLayout}
{...this.panResponder.panHandlers}
style={[
this.pan.getLayout(),
styles.aniView,
]}>
{this.renderChildren()}
</Animated.View>
</View>
renderChildren = () => {
const { source } = this.state;
const children = source.map((item, index) => {
return (
<View key={item.toString()}
style={{
width: WIDTH,
height: HEIGHT / 3,
flex: 1,
borderWidth: 1,
}}>
<ChildContent title={item} />
</View>
);
});
return children;
}
componentWillMount() {
this.pan.addListener((value) => {
this._value = value;
});
this.panResponder = PanResponder.create({
onStartShouldSetPanResponder: (evt, gestureState) => true,
onMoveShouldSetPanResponder: (evt, gestureState) => ((gestureState.dx != 0) && (gestureState.dy != 0)),
onPanResponderGrant: (e, gestureState) => {
this.pan.setOffset({ x: this._value.x, y: this._value.y });
this.pan.setValue({ x: 0, y: 0 });
},
onPanResponderMove: Animated.event([null, {
dx: this.pan.x,
dy: this.pan.y,
}]),
onPanResponderRelease: (e, gesture) => {
this.pan.flattenOffset();
this.animatePanel();
}
});
}
Environment:
OS: macOS High Sierra 10.13.6
I got the solution:
update to react-native 0.56.
(using react-native-git-upgrade ,and update babel-preset-react-native to 5.0.2).
then the problem disappeared.