限制PanResponder移动React Native



这个问题与

  • React Native: Conbinding Animated.Value
  • 反应本机泛响应器限制 X 位置

我正在尝试使用PanResponder构建一个水平滑块。我可以使用以下代码在 x 轴上移动元素,但我想限制可以移动它的范围。

这是一个带注释的示例:

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

为了限制该值,使其不能低于 0,我尝试实现其他逻辑,如下所示:

onPanResponderMove: (evt, gestureState) => {
return (gestureState.dx > 0) ? Animated.event([null, {
dx: this.state.pan1.x
}])(evt, gestureState) : null
},

但这是有缺陷的 - 它最初似乎有效,但最小 X 限制似乎有效地增加了。我前后滚动的次数越多,最小 x 限制似乎会增加。

我也试过这个:

onPanResponderMove: (evt, gestureState) => {
return (this.state.pan1.x.__getValue() > 0) ? Animated.event([null, {
dx: this.state.pan1.x
}])(evt, gestureState) : null
},

但它似乎根本不起作用。

如何将检测到的手指运动的整个宽度插值到我定义的有限范围内?

gestureState.dx是用户每次轻扫时用手指从其原始位置移动的差异。因此,每当用户抬起手指时,它就会重置,从而导致您的问题。

有几种方法可以限制该值:

使用插值

let translateX = pan.x.interpolate({inputRange:[0,100],outputRange:[0,100],extrapolateLeft:"clamp"})虽然这有效,但用户向左滑动的次数越多,他就越需要向右滑动才能到达"真正的 0">

重置发布时的值

onPanResponderRelease: (e, gestureState)=>{
this.state.pan.setValue({x:realPosition<0?0:realPosition.x,y:realPosition.y})
}

确保使用this.state.pan.addListener获取当前值并将其放入realPosition您可以允许向左滑动并在某种弹簧中将其动画化,或者只是使用以前的插值方法阻止它完全关闭。

但是你应该考虑使用其他东西,因为PanResponder不支持useNativeDriver。要么使用 scrollView(如果您想要 4 个方向滚动,则其中两个(,它通过其内容或类似的东西来限制滚动 wix 的react-native-interactable.

我在查看 React Native 上的链接帖子时发现了这篇文章:约束 Animated.Value。您的问题似乎与我遇到的相似,我的解决方案已发布在那里。基本上,dx 可以超出界限 b/c,它只是累积的距离,我的解决方案是 pan.setOffset 的上限,所以 dx 不会发疯。

这是针对您的问题的解决方法,或者您可以将其用作替代解决方案。我没有在此解决方案中使用pan。这个想法是,限制父视图中的滑块移动。所以它不会移动到父级。考虑以下代码

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

就像上面@AlonBarDavid说的,gestureState.dx是用户每次滑动时用手指从其原始位置移动的差异。因此,每当用户抬起手指时,它就会重置,从而导致您的问题。

一种解决方案是创建第二个变量来保存上一次触摸的此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>
)
}