简体   繁体   中英

Limit PanResponder movements React Native

This question is related to

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>
  )

}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM