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 如果有更好的方法请一定联系我,欢迎探讨

  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值