简体   繁体   English

限制 PanResponder 移动 React Native

[英]Limit PanResponder movements React Native

This question is related to这个问题与

I am trying to build a horizontal slider with a PanResponder.我正在尝试使用 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.我可以使用以下代码在 x 轴上移动元素,但我想限制可以移动它的范围。

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:为了限制值使其不能低于 0,我尝试实现 if else 逻辑,如下所示:

        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.但这有问题 - 它最初似乎有效,但最小 x 限制似乎有效增加。 The more I scroll back and forward, the minimum x-limit seems to increase.我向后和向前滚动得越多,最小 x 限制似乎就会增加。

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. gestureState.dx是用户每次滑动手指从其原始位置移动的差异。 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" let translateX = pan.x.interpolate({inputRange:[0,100],outputRange:[0,100],extrapolateLeft:"clamp"})虽然这有效,但用户向左滑动的次数越多,他就越需要向右滑动才能到达“真正的 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.确保使用this.state.pan.addListener获取当前值并将其放入realPosition您可以允许向左滑动并在某种弹簧中将其动画化,或者只是使用先前的插值方法防止它完全消失。

But you should consider using something else since PanResponder doesn't support useNativeDriver.但是您应该考虑使用其他东西,因为 PanResponder 不支持 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 .要么使用 scrollView (如果你想要 4 方向滚动,可以使用两个),它通过它的内容或类似 wix 的react-native-interactable来限制滚动。

I found this post while looking to linked posts at React Native: Constraining Animated.Value .我在查看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.基本上,dx 可以超出 b/c 范围,这只是累积距离,而我的解决方案是在 pan.setOffset 处设置上限,因此 dx 不会发疯。

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.我没有在这个解决方案中使用pan 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.就像上面@AlonBarDavid 所说的那样, gestureState.dx是用户每次滑动手指从其原始位置移动的差异。 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.一种解决方案是创建第二个变量来保存与前一次触摸的offset位置,然后将其添加到gestureState.x值中。


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

}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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