ReactNative 移动与点击手势冲突解决办法与拖动view的及时更新
这段时间根据业务需求,需要在一个界面上code一个可以随意滑动和点击的按钮,类似于iPhone的小圆点,功能就是点击时跳转界面,滑动是可以在界面上拖动。
功能设计还是比较简单的,但是在实际code的过程中就发现了许多RN的坑,所以记下来方便大家避坑。
我的RN版本是0.43.4版本的
-
如何使用手势
在这个需求里,需要实现拖拽和点击两种手势,我之前是使用一个 TouchableOpacity 包裹着一个 View,但是发现实现了View的拖动,TouchableOpacity的点击方法就没有效果,所以只有找资料,发现官网中有一个PanResponder 就是手势,用于实现view的手势实现。
但是官网的资料比较少,所以就只好查别的大神的资料,所幸实现还比较简单,代码的话就先copy出来,重点在后面
import React, {Component} from 'react';
import {View, PanResponder} from 'react-native';
export default class TouchableView extends Component{
constructor(props){
super(props);
this.pressStatus = false;
}
componentWillMount() {
this._panResponder = PanResponder.create({
//开启点击手势响应
onStartShouldSetPanResponder: (evt, gestureState) => true,
//开启点击手势响应是否劫持 true:不传递给子view false:传递给子view
onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
//开启移动手势响应
onMoveShouldSetPanResponder: (evt, gestureState) => true,
//开启移动手势响应是否劫持 true:不传递给子view false:传递给子view
onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
//手指触碰屏幕那一刻触发 成为**状态。
onPanResponderGrant: (evt, gestureState) => {
},
// 表示手指按下时,成功申请为事件响应者的回调。
onPanResponderStart: (evt, gestureState) => {
// 表示申请成功,你成为了事件的响应者,这个时候开始,组件就进入了**状态。
},
//手指在屏幕上移动触发
onPanResponderMove: (evt, gestureState) => {
},
//当有其他不同手势出现,响应是否中止当前的手势
onPanResponderTerminationRequest: (evt, gestureState) => true,
//手指离开屏幕触发
onPanResponderRelease: (evt, gestureState) => {
},
// 另一个组件已经成为了新的响应者,所以当前手势将被取消。
onPanResponderTerminate: (evt, gestureState) => {
},
onShouldBlockNativeResponder: (evt, gestureState) => {
// 返回一个布尔值,决定当前组件是否应该阻止原生组件成为JS响应者
// 默认返回true。目前暂时只支持android。
//基于业务交互场景,如果这里使用js事件处理,会导致容器不能左右滑动。所以设置成false.
return false;
},
});
}
render(){
const {children} = this.props;
return (
<View {...this._panResponder.panHandlers}>//把创建好的pandGesture赋值给View的属性
</View>
);
}
}
以上就是基本的使用方法
- 如何快速响应拖动
这个问题其实我最先开始想到的解决方法就是使用setState修改view的位置,比如在拖动的时候获取移动距离,然后重新setState,改变位置。可当我实现后发现手机就像延迟了一样,每次拖动,view要反应一下才会跟着慢慢过来。这样的体验肯定是不行的。所以只能找另外的方法。最后找到了一个叫setNativeProps的属性,来直接更改原生组件的样式属性 来达到相同的效果 ,使用方法需要个view一个ref
<View {...this._panResponder.panHandlers} //把创建好的pandGesture赋值给View的属性
ref="touchBackView">
</View>
使用的时候
this.refs.touchBackView.setNativeProps({
style:{
right:XXX,
bottom:XXX,
}
})
- 解决拖动与点击的冲突
这个冲突我相信只要做过与手势相关的人都遇到过,但是我要讲的是RN这个手势冲突有点问题。
这个问题的来源是,要实现拖动和点击是需要一个View来实现这两个手势,还是两个view分别识别一个手势。
例如:
<View {...this._panResponder.panHandlers}>
<View {...this._childPanResponder.panHandlers}
ref="touchBackView">
<Image/>
</View>
</View>
而实现函数是
父组件
//手指触碰屏幕那一刻触发 成为**状态。
onPanResponderGrant: (evt, gestureState) => {
// 表示申请成功,你成为了事件的响应者,这个时候开始,组件就进入了**状态。
console.log('父view点击-11')
},
// 表示手指按下时,成功申请为事件响应者的回调。
onPanResponderStart: (evt, gestureState) => {
// 表示申请成功,你成为了事件的响应者,这个时候开始,组件就进入了**状态。
console.log('父view点击-22')
},
//手指在屏幕上移动触发
//locationX 和 locationY :触摸点相对组件的位置;
//pageX 和 pageY :触摸点相对于屏幕的位置;
onPanResponderMove: (evt, gestureState) => {
//就是我手指按住然后一直拖啊拖。
console.log('父view移动-33')
},
子组件
//手指触碰屏幕那一刻触发 成为**状态。
onPanResponderGrant: (evt, gestureState) => {
// 表示申请成功,你成为了事件的响应者,这个时候开始,组件就进入了**状态。
console.log('子view点击-1')
},
// 表示手指按下时,成功申请为事件响应者的回调。
onPanResponderStart: (evt, gestureState) => {
// 表示申请成功,你成为了事件的响应者,这个时候开始,组件就进入了**状态。
console.log('子view点击-2')
},
onPanResponderMove: (evt, gestureState) => {
//就是我手指按住然后一直拖啊拖,这里可以拿很多数据来做一些有意思的事情。
console.log('子view移动-3')
this.refs.touchBackView.setNativeProps({
style:{
right:ScreenWidth-evt.nativeEvent.pageX-imageSize/2,
bottom:ScreenHeight-evt.nativeEvent.pageY-imageSize/2,
}
})
},
上面这段代码是使用一个父View包裹子View,父View实现点击,子View实现拖动。两个view都有对应的手势。在RN里面的手势响应系统中,如果有父子等多个组件嵌套时,就需要知道谁是响应手势的组件,具体流程如图
触摸事件开始,首先调用 A 组件的 onStartShouldSetResponderCapture,若此回调返回 false,则按照图传递到 B 组件,然后调用 B 组件 onStartShouldSetResponderCapture,若返回 true,则事件不再传递给 C 组件,直接调用本组件的 onResponderStart,则 B 组件就成为事件响应者,后续事件直接传递给它。其他的分析类似。
onStartShouldSetResponder与onMoveShouldSetResponder是以冒泡的形式调用的,即嵌套最深的节点最先调用。这意味着当多个View同时在*ShouldSetResponder中返回true时,最底层的View将优先“夺权”。在多数情况下这并没有什么问题,因为这样可以确保所有控件和按钮是可用的。
但是有些时候,某个父View会希望能先成为响应者。我们可以利用“捕获期”来解决这一需求。响应系统在从最底层的组件开始冒泡之前,会首先执行一个“捕获期”,在此期间会触发on*ShouldSetResponderCapture系列事件。因此,如果某个父View想要在触摸操作开始时阻止子组件成为响应者,那就应该处理onStartShouldSetResponderCapture事件并返回true值。
这里面提到有个函数onShouldSetPanResponderCapture* 表示是否拦截此次的响应事件,默认是false,当为true时,会拦截,子组件就不会受到响应的。关键就是在这一点上
如果父组件的点击劫持onStartShouldSetResponderCapture=true,父组件的onPanResponderGrant方法就会有输出而子组件的该方法则不会。
同样如果父组件的移动劫持onMoveShouldSetPanResponderCapture=true 父组件的onPanResponderMove方法就会有输出而子组件的该方法则不会。
所以按照这个逻辑 我是这么写的
父组件:
onStartShouldSetPanResponder: (evt, gestureState) => true,
onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
onMoveShouldSetPanResponder: (evt, gestureState) => false,
onMoveShouldSetPanResponderCapture: (evt, gestureState) => false,
子组件:
onStartShouldSetPanResponder: (evt, gestureState) => true,
onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
onMoveShouldSetPanResponder: (evt, gestureState) => true,
onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
父组件拦截点击方法,不拦截移动的方法, 然而结果却十分奇怪
这里父组件的onStartShouldSetPanResponderCapture 把子组件的移动也同样拦截了,
这样我把父组件的移动拦截也加上
父组件:
onStartShouldSetPanResponder: (evt, gestureState) => true,
onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
onMoveShouldSetPanResponder: (evt, gestureState) => true,
onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
结果也是一样
所以可以得出 只要父组件的onStartShouldSetPanResponderCapture设置为true 子组件就都不会得到响应,而onMoveShouldSetPanResponderCapture设置true或false已经没有关系了,这样不能实现需求,因为只有父组件有方法的实现,子组件没有反应。
这样 我们将父组件onStartShouldSetPanResponderCapture设置为false onMoveShouldSetPanResponderCapture设置为true 试试
父组件不拦截点击,拦截移动
父组件:
onStartShouldSetPanResponder: (evt, gestureState) => false,
onStartShouldSetPanResponderCapture: (evt, gestureState) => false,
onMoveShouldSetPanResponder: (evt, gestureState) => false,
onMoveShouldSetPanResponderCapture: (evt, gestureState) => false,
子组件:
onStartShouldSetPanResponder: (evt, gestureState) => true,
onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
onMoveShouldSetPanResponder: (evt, gestureState) => true,
onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
结果是:
子组件会先进行点击方法,在进行移动前,一定会调用父组件的点击方法,而父组件的移动方法就不会调用。所以说,虽然表面上父组件不干预子组件的活动 但是只要有移动 父组件一定会调用onPanResponderGrant 方法。
那这样要实现需求貌似也不行 因为父组件无论怎样都会实现点击方法。不能分开实现点击和拖动。
接下来试试只有子组件的移动onMoveShouldSetPanResponderCapture劫持
父组件:
onStartShouldSetPanResponder: (evt, gestureState) => false,
onStartShouldSetPanResponderCapture: (evt, gestureState) => false,
onMoveShouldSetPanResponder: (evt, gestureState) => false,
onMoveShouldSetPanResponderCapture: (evt, gestureState) => false,
子组件:
onStartShouldSetPanResponder: (evt, gestureState) => false,
onStartShouldSetPanResponderCapture: (evt, gestureState) => false,
onMoveShouldSetPanResponder: (evt, gestureState) => true,
onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
结果是
这样 子组件做任何事 父组件都不会有响应了。 所以 需求也实现不了。
所以经过以上的实验,貌似没有很好的方法去实现父子组件分别实现不同的方法,所以我们只能考虑在一个view上实现两个手势。
- 如何在一个view上解决拖动与点击的手势冲突
经过上面的测试,我重新在一个view上写了两个手势
<View {...this._panResponder.panHandlers} //把创建好的pandGesture赋值给View的属性
ref="touchBackView">
</View>
在实现时通过移动距离来判断是否点击还是移动。
先定义个状态,判断是否是点击
this.presentState = false;
然后在触碰到屏幕时设置为true
//手指触碰屏幕那一刻触发 成为**状态。
onPanResponderGrant: (evt, gestureState) => {
// 表示申请成功,你成为了事件的响应者,这个时候开始,组件就进入了**状态。
console.log('父view点击-11')
this.presentState = true;
}
然后在移动时进行判断
//手指在屏幕上移动触发
//locationX 和 locationY :触摸点相对组件的位置;
//pageX 和 pageY :触摸点相对于屏幕的位置;
//gestureState.dx:从触摸操作开始时的累计横向路程
//gestureState.dy:从触摸操作开始时的累计纵向路程
onPanResponderMove: (evt, gestureState) => {
console.log('dy:'+gestureState.dy );
// 如果累计的移动距离小于5 则表示没移动
if (gestureState.dx < 5 && gestureState.dx > -5 && gestureState.dy < 5 && gestureState.dy > -5){
this.presentState = false;
}else {
this.presentState = true;
this.refs.touchBackView.setNativeProps({
style:{
bottom:this.normal_Y-gestureState.dy,
}
})
}
},
最后在手指离开时进行操作处理
//手指离开屏幕触发
onPanResponderRelease: (evt, gestureState) => {
// 如果移动了 则进行移动操作
if (this.presentState) {
this.normal_Y -=gestureState.dy
}else { // 如果判断没移动 则进行返回操作
this.clickToNewCar()
}
},
这样就完成了拖动和点击手势冲突的解决方法。
中间曲折还是比较多的,查找了很多资料,都感觉千篇一律,没有实际操作,所以我只有一个一个实验。幸好弄出来了,虽然过程很繁琐,但是结果很美好。
所以,别人告诉你的是知道,你自己操作后的才是知识。
PS 如果有更好的方法请一定联系我,欢迎探讨