ReactNative PanResponder limit X position
Asked Answered
B

6

13

I'm building a Music Player and I'm focusing on the progress bar. I was able to react to swipe gestures, but I cant limit how far that gesture goes.

This is what I've done so far. I've reduced everything to the minumal:

constructor(props) {
    super(props);

    this.state = {
      pan: new Animated.ValueXY()
    };
}

componentWillMount() {
    this._panResponder = PanResponder.create({
        onMoveShouldSetResponderCapture: () => true,
        onMoveShouldSetPanResponderCapture: () => true,
        onPanResponderGrant: (e, gestureState) => {


            // Set the initial value to the current state
            let x = (this.state.pan.x._value < 0) ? 0 : this.state.pan.x._value;


            this.state.pan.setOffset({ x, y: 0 });
            this.state.pan.setValue({ x: 0, y: 0 });


        },
        onPanResponderMove: Animated.event([
            null, { dx: this.state.pan.x, dy: 0 },
        ]),
        onPanResponderRelease: (e, { vx, vy }) => {
            this.state.pan.flattenOffset();
        }
    });
}

render() {
    let { pan } = this.state;

    // Calculate the x and y transform from the pan value
    let [translateX, translateY] = [pan.x, pan.y];
    // Calculate the transform property and set it as a value for our style which we add below to the Animated.View component
    let imageStyle = { transform: [{ translateX }, { translateY }] };

    return (
        <View style={styles.container}>
            <Animated.View style={{imageStyle}} {...this._panResponder.panHandlers} />
        </View>
    );
}

Here there is an image showing what the problem is.

Initial position:

Initial position

Wrong Position, limit exceeded:

Wrong position

So the idea is to stop keeping moving once the limit (left as well as right) is reached. I tried checking if _value < 0, but it didn't work since It seems to be an offset, not a position.

Well any help will be appreciated.

Balneology answered 13/9, 2017 at 3:44 Comment(0)
J
14

Instead of letting your animation die at your borders, you could interpolate your Animated.Value with y=x, but with clamping it to your width.

return (
    <View style={styles.container}>
        <Animated.View 
            style={{
                transform: [{
                    translateX: this.state.pan.x.interpolate({
                        inputRange: [0, trackWidth ],
                        outputRange: [0, trackWidth ],
                        extrapolate: 'clamp'
                    })
                }],

            }} 
            {...this._panResponder.panHandlers}
        />
    </View>
);

Here's a more in-depth example: https://github.com/olapiv/expo-audio-player/blob/master/src/AudioSlider.js

Janina answered 10/10, 2019 at 10:43 Comment(0)
P
5
onPanResponderMove: (e, gestureState)=> {
    this.state.pan.x._value > 0 ? null : Animated.event([
            null, 
            {dx: this.state.pan.x, dy: this.state.pan.y},
        ])(e, gestureState)
    },
Popsicle answered 4/10, 2017 at 9:57 Comment(1)
The problem with this approach is that when reaching the limit, you can't move again in the other direction, because the this.state.pan was set as null.Aphorism
C
3

I was trying to do something similar; I wanted to have it so that you can pull the page part way and then release and it goes back to where it was.

My solution was this:

panResponder = PanResponder.create({
  onMoveShouldSetPanResponderCapture: (e, { dx }) => {
    // This will make it so the gesture is ignored if it's only short (like a tap).
    // You could also use moveX to restrict the gesture to the sides of the screen.
    // Something like: moveX <= 50 || moveX >= screenWidth - 50
    // (See https://facebook.github.io/react-native/docs/panresponder)
    return Math.abs(dx) > 20;
  },
  onPanResponderMove: (e, gestureState) => (
    // Here, 30 is the limit it stops at. This works in both directions
    Math.abs(gestureState.dx) > 30
      ? null
      : Animated.event([null, { dx: this.animatedVal }])(e, gestureState)
  ),
  onPanResponderRelease: (e, { vx, dx }) => {
    // Here, abs(vx) is the current speed (not velocity) of the gesture,
    // and abs(dx) is the distance traveled (not displacement)
    if (Math.abs(vx) >= 0.5 || Math.abs(dx) >= 30) {
      doSomeAction();
    }
    Animated.spring(this.animatedVal, {
      toValue: 0,
      bounciness: 10,
    }).start();
  },
});
Chemiluminescence answered 31/8, 2018 at 18:27 Comment(0)
A
2

🍰 This method doesn't cancel the gesture handler if the user is still holding it down after exceeding the X limit. Change MaxDistance & MinDistance to whatever values you like 😃

onPanResponderMove: (e, gestureState) => {
  // Configure Min and Max Values
  const MaxDistance = maxDistance;
  const MinDistance = 0;
  const dxCapped = Math.min(Math.max(parseInt(gestureState.dx), MinDistance), MaxDistance);

  // If within our bounds, use our gesture.dx....else use dxCapped
  const values = {}
  if(gestureState.dx < MaxDistance && gestureState.dx > MinDistance){
    values.dx = gestureState.dx
    values.dy = gestureState.dy
  }else{
    values.dx = dxCapped
    values.dy = gestureState.dy
  }

  //Animate Event
  Animated.event([null, {
    dx: pan.x,
    dy: pan.y,
  }])(e, values);
},

Hope this helps some folks. 🐱

Agnostic answered 11/6, 2020 at 17:24 Comment(0)
V
1

A tricky point is that while I can not move the icon beyond that clamp but the pan.x value is indeed beyond the clamp limit although you don't see it. Then, when you want to move it back, you don't know how much swipe you need to move it back. This could be a nuance.

My solution is:

      onPanResponderGrant: () => {
        console.log("pan responder was granted access!")
        pan.setOffset({
          x: (pan.x._value>xMax)? xMax : (pan.x._value<xMin)? xMin: pan.x._value,
          y: (pan.y._value>yMax)? yMax : (pan.y._value<yMin)? yMin: pan.y._value,
        });
      },

Then, can also console.log pan.x._value in following to double check.

      onPanResponderRelease: () => {
        pan.flattenOffset();}

I found this helpful for my own project. Note can only use pan.x_value not pan.x. In my case I also used useMemo instead of useRef so the limits can be reset, which I learned from React Native's panResponder has stale value from useState?

Velour answered 3/3, 2021 at 8:0 Comment(0)
B
0

There's a solution to a similar problem on this other question: https://mcmap.net/q/831647/-how-to-stop-react-native-animation-inside-39-onpanrespondermove-39-when-a-certain-condition-is-met

that can be repurposed for what you're looking for

const DRAG_THRESHOLD = /*configure min value*/
const DRAG_LIMIT = /*configure max value*/

onPanResponderMove: (e, gesture) => {
   if ( (Math.abs( gesture.dy ) > DRAG_THRESHOLD) && 
        (Math.abs( gesture.dy ) < DRAG_LIMIT ) )
   {
       return Animated.event([
           null, {dx: 0, dy: pan.y}
       ]) (e, gesture)
   }
 },

This is not my answer, so I recommend you follow the link to see further explanation and if you like it upvote the original poster! :) I was trying to do the same thing and it worked for me! hope it helps.

P.S. I found that other solutions relying on checking the animation value rather than the gesture value would sometimes get stuck.

Bald answered 25/6, 2020 at 15:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.