React Native 手势触摸事件机制详解(基础篇)

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/u013718120/article/details/82854868

     

欢迎大家关注【跨平台开发那些事】公众号,定期推送跨平台开发技术实践。

       源码已开源到Github,详细代码可以查看:《React Native 触摸事件代码实践》

       博客产出拖延了很久,老早定的主题现在才开始写。之前群里朋友对于React Native(以下简称RN)中手势触摸相关问题提出的频率很高,并且在实际开发过程中较难理解和处理。本篇内容将围绕触摸事件相关问题一探究竟,也作为记录供后期参考。

       作为与用户交互的第一层,触摸事件直接影响着用户行为体验。在Android 和 iOS 平台设备中,对于触摸机制做了非常完善的封装,能够很方便的帮助开发者处理基本的触摸行为操作,原生平台通过注册Listener的方式可以轻松的实现单击,双击等操作。在RN中同样提供了与Native触摸事件映射一致的处理方式,方便React Native开发者处理触摸行为,定义触摸操作。

一、RN系统触摸组件

RN中实现按钮单击组件的方式很简单,系统为我们提供了四种方式:

(1)TouchableOpacity

(2)TouchableHightLight

(3)TouchableNativeFeedback(仅限Android)

(4)TouchableWithoutFeedback

以上四种方式相信大家都不陌生,都是以 Touchable* 开始,实现单击事件只需要声明onPress属性即可。以 TouchableOpacity 为例:

<TouchableOpacity onPress={()=>this.onPress()}>
   <Text>单击按钮</Text>
</TouchableOpacity>

除了onPress属性,系统还为我们提供了:

(1)onLongPress

(2)onPressIn

(3)onPressOut

顾名思义,onLongPress即为长按点击、onPressIn点击开始(手指按下)、onPressOut点击结束(手指离开)。

<TouchableOpacity 
    onPressIn={()=>this.onPressIn()}
    onPressOut={()=>this.onPressOut()}
    onLongPress={()=>this.onLongPress()}
    onPress={()=>this.onPress()}>
    <Text>点击按钮</Text>
</TouchableOpacity>

四个方法也提供了触摸事件数据,所以也可以写成如下:

<TouchableOpacity 
    onPressIn={(evt)=>this.onPressIn(evt)}
    onPressOut={(evt)=>this.onPressOut(evt)}
    onLongPress={(evt)=>this.onLongPress(evt)}
    onPress={(evt)=>this.onPress(evt)}>
    <Text>点击按钮</Text>
</TouchableOpacity>

evt 中包含了关于触摸事件相关的数据,大部分情况下我们只需要关心 nativeEvent,其大致结构如下:

nativeEvent

可以看到 nativeEvent 中包含了手指触摸的一些坐标及时间戳参数,关于具体的参数含义我们后面再进行具体探讨。

二、自定义触摸事件处理

从Touchable* 组件的使用可以看出,其内部实现了触摸之后的事件处理。开发者以简单的属性声明方式,即可完成对于点击事件的操作。且RN组件默认不支持触摸事件的处理(Text组件除外),如果想要处理触摸事件,首先要【申请】成为事件响应者,当成为事件响应者之后,即可处理发生在该组件之上的触摸事件,例如:按下(Start)、移动(Move)、弹起(Release),最终释放响应行为。即一次完整的触摸流程大致如下:

申请成为触摸事件响应者 -> 成为触摸事件响应者 -> 处理触摸事件 -> 释放触摸事件 -> 触摸事件结束

整个事件流程中,组件的事件身份分为两种:非事件响应者、事件响应者。

非事件响应者

授权

在上述部分我们提到,RN中的组件除Text外,默认是不能进行触摸事件处理响应的,即非事件响应者。如果要进行触摸事件处理,首先需要申请成为触摸事件响应者。对应申请的处理方法如下:

View.props.onStartShouldSetResponder(evt): => bool  

View.props.onMoveShouldSetResponder(evt): => bool  

从名称可以看出,onStartShouldSetResponder在手势触开始时(按下)被触发,onMoveShouldSetResponder在手势滑动时被触发。两个方法都需要返回boolean类型值:onStartShouldSetResponder 在手指按下屏幕时,RN系统会询问当前组件是否需要申请成为事件响应者,true表示成为事件响应者,false则不处理。同样,onMoveShouldSetResponder表示手指在屏幕滑动时,RN系统询问当前组件是否需要申请成为事件响应者,true表示成为事件响应者,false则不处理。

授权结果

当接收到上述方法返回true时,组件申请成为响应者,并处理接收后续触摸事件。在同一时间只能有一个事件处理者,此时,RN系统会协调当前所有组件到事件处理,所以不是每个组件申请都能成功,RN 通过如下两个回调来通知告诉组件它的申请结果:

View.props.onResponderGrant: (evt) => { }

View.props.onResponderReject: (evt) => { }

onResponderGrant表示申请成功,组件已经成为事件响应者,并且接收后续的触摸事件。在该方法中,我们可以做一些手势事件初始化的操作。onResponderReject表示申请失败,失败的原因一般为其他组件正在进行触摸事件的处理,并且不放弃当前触摸事件的权限,在你申请时被拒。

事件响应者

通过上述步骤,当组件申请成为事件响应者后,后续当触摸事件行为都会被该组件拦截处理,并触发对应都行为函数。触摸事件行为函数如下:

View.props.onResponderStart: (evt) => { }

View.props.onResponderMove: (evt) => { }

View.props.onResponderEnd: (evt) => { }

View.props.onResponderRelease: (evt) => { }

可以很明显的看到,四个事件行为方法都是以【onResponder】作为前缀,后面紧接行为方式名称。

Start:表示手指按下,开始进行触摸行为。

Move:手指触摸屏幕并进行移动,此回调函数触发非常频繁,尽可能不要做过多任务处理。

End:触摸行为结束,手指弹起离开屏幕。

Release:当手指弹起离开屏幕时,事件行为结束,一次完整的触摸事件结束,并释放当前触摸行为权限,当前组件不再是事件响应者。

释放响应者权限

当前组件正在进行触摸事件行为处理且没有结束时,其他组件也可能会请求系统申请成为事件响应者,此时RN会询问当前组件

是否可以释放当前响应者身份,将权限让给其他组件。对应的函数如下:

View.props.onResponderTerminationRequest: (evt) => bool  

View.props.onResponderTerminate: (evt) => {}  

Termination 即中止,onResponderTerminationRequest 方法需要返回boolean类型值,true 表示可以立刻释放当前响应者角色,并触发 onResponderTerminate 方法,告知当前组件触摸事件被中止。

触摸事件被意外中断也会触发 onResponderTerminate 方法,例如:手机来电,自动关机,消息等。

触摸事件响应参数

从系统提供的 Touchable* 点击组件,到自定义触摸组件,可以发现触摸行为函数都有 event 参数,在文章开始介绍 Touchable* 组件时,我们说到 event 参数中包含一个触摸事件数据 nativeEvent,在该属性中包含了触摸事件中的相关参数,具体如下:

changedTouches:[{…}]  触摸事件集合,记录从上次到本次触摸事件到所有事件,在触摸过程中,由于非常频繁,可能有没有及时反馈,系统用这个属性来批量上报。

identifier:   手指触摸事件ID,多点触控场景下,用来区分手指的触摸事件。

locationX:  触摸点在组件横向上的位置。

locationY:  触摸点在组件纵向上的位置。

pageX:       当前组件触摸点相对屏幕横向上的位置。

pageY:       当前组件触摸点相对屏幕纵向上的位置。

target:        当前组件ID。

timestamp:当前触摸的事件的时间戳,可以用来进行滑动计算。

touches:[{…}]   触摸事件集合,多点触摸场景下,包含当前所有触摸点的事件参数。

在日常开发中常用的属性是 locationX|Y、pageX|Y。常用触摸事件的参数是从原生层传递到RN层,即参数数值是作为原生层到像素值,如果需要转换成RN中到逻辑单位,可以用如下方式:

const px = evt.nativeEvent.locationX / PixelRatio.get()

三、触摸事件拦截

在第二小节中,我们花了大量篇幅来介绍自定义触摸事件的相关行为函数,触摸参数等。可以看到所有的触摸行为都是基于一个View组件之上。在日常开发中,我们肯定离不开View组件嵌套实现一个视图效果。那么嵌套组件对于触摸事件响应情况又是怎样的?

了解 Native 开发的朋友肯定不陌生,在Native层系统处理触摸事件传递方式使用的是 冒泡机制。即事件的响应是从布局最底层的组件开始,逐层向父布局组件传递。同样,RN系统也提供了与 Native 层相同的触摸事件传递机制,用来保证在嵌套布局中的所有组件都可以得到响应处理。在某些情况下,父组件可能需要单独处理触摸事件,不需要交给子组件处理,即父组件拦截掉触摸事件的响应,消耗完成不再向下传递。同样,RN系统提供了两个函数,用来实现授权父组件事件拦截机制:

View.props.onStartShouldSetResponderCapture: (evt)=> bool

View.props.onMoveShouldSetResponderCapture: (evt)=> bool

在触摸事件 开始,RN父布局组件会回调 onStartShouldSetResponderCapture,询问是否要拦截事件,自己接收处理, true 表示拦截。在触摸 滑动 事件时,RN父布局组件会回调 onMoveShouldSetResponderCapture,询问是否要拦截事件,自己接收处理, true 表示拦截。

举个🌰,假设有A,B,C 三个组件,布局为 C是B的子组件,B是A的子组件:

嵌套组件

(1)A组件的 onStartShouldSetResponderCapture 返回 false,表示不拦截,事件传递到B。

(2)B组件的onStartShouldSetResponderCapture 返回 true,表示拦截,事件不再向下传递,即事件不会传递到C。

(3)此时B组件开始向系统申请事件响应者权限,即调用 on*ShouldSetResponder,如果返回 true,则表示授权成功,此时B组件成为事件响应者,执行StartMove 等事件行为。

(4)如果B组件不申请,则系统询问A组件是否申请成为事件响应者。即调用A组件等on*ShouldSetResponder,如果返回 true,则表示授权成功,此时A组件成为事件响应者,执行 StartMove 等事件行为。

 

emm... 终于到了喘口气到时候了,关于触摸事件到处理流程,相信大家看到这里都有了自己的认知与理解。下一篇,我们会从更高级的API来了解RN中的触摸事件,并通过代码运行的结果更深的去看事件触发的行为方式。

展开阅读全文

没有更多推荐了,返回首页