【React Native进阶】React Native Gesture Handler的使用
背景
说到React Navtive的性能优化,首先要了解React Native的运行机制。React Native程序主要运行在三个并行的线程上:
- JavaScript Thread:我们写的JavaScript代码逻辑都是在这个线程上执行;
- UI Thread:即原生线程,当我们需要调用原生的渲染或者能力时会运行到这个线程上;
- Shadow Thread:这个线程创建和管理着Shadow Tree,它类似于虚拟DOM。它通过Yoga引擎着Flexbox布局转化为原生的布局方式。
这三个线程独立运行的情况下,性能良好,但如果涉及到事件驱动与UI线程有交互的情况,React Native的这种设计效果不佳。
当与触摸屏交互时,用户希望屏幕上的效果是即时的。如果更新发生在单独的线程中,通常情况下,在JavaScript线程中所做的更改无法反映在同一帧中。在React Native中,默认情况下,由于UI和JavaScript线程之间的通信是异步的,并且UI线程从不等待JavaScript线程完成处理事件,因此所有更新都会延迟至少一个帧。
而且由于UI线程与其他线程通信存在序列化和反序列化这个比较消耗性能的步骤,当UI线程与其他线程交互比较频繁或者其他线程负荷较大时,通常事件无法立即处理,从而造成更严重的延迟。
我们的RN代码逻辑都是用JavaScript写的,JavaScript线程也是负荷最大的线程。因此在React Native的性能优化上主要要考虑两个方面:
- 减少与UI线程的通信;
- 减少JavaScript线程的负荷;
而React Native Gesture Handler
正是从这两个方面优化React Native在手势操作方面的性能。它旨在替换React Native自带的手势处理系统。如果你使用过系统自带的手势处理系统,会发现在JavaScript线程会有大量的计算,这些计算也会频繁与UI线程通信,对性能影响较大。具体代码可以自行比较,这里不再赘述。
功能
React Native Gesture Handler提供了以下功能:
- 提供了包括缩放、旋转、屏蔽滑动等手势的处理系统;
- 能够定义多个手势之间的关系。例如:当你在
ScrollView
里面加入一个滑动手势(pan handler)时,可以让滑动手势响应结束后再响应ScrollView
; - 提供了让手势运行在原生线程(UI线程)上并遵从原生平台默认行为机制;
- 由于使用了原生的动画驱动,即便在JavaScript线程已经超负荷的情况,也能够提供顺滑的手势交互。
安装
整个安装分为三个部分:JS部分、Android部分和iOS部分。其中JS和iOS部分都是统一的,Android在使用了第三方导航库和没使用的情况安装配置方式会有不同。
JS
使用yarn
安装:
1 | yarn add react-native-gesture-handler |
或者你也可以选择使用npm
:
1 | npm install --save react-native-gesture-handler |
Android
如果在项目中使用了导航库(例如:react-native-navigation
),直接跳过这部分看后面配合导航库使用的小节。
更新MainActivity.java
文件(或者你在其他地方创建的ReactActivityDelegate
实例的内部),重写创建ReactRootView
的方法,让这个库的根视图包裹安卓的主活动。注意在文件顶部需要导入ReactActivityDelegate
、ReactRootView
和RNGestureHandlerEnabledRootView
:
1 | package com.swmansion.gesturehandler.react.example; |
iOS
如果在项目中使用了Cocoapods(React Native 0.60及之后的版本创建时会自动使用),需要在启动前安装pods:
1 | cd ios && pod install |
如果React Native版本为0.61或更高,则需要在index.js文件顶部导入库文件:
1 | import 'react-native-gesture-handler'; |
配合导航库使用
如果你在项目中使用了像react-native-navigation 这样的导航库,由于本地导航库和Gesture Handler库都需要它们自己的ReactRootView
子类,在安卓不能使用上述配置,需要如下单独配置。
与上面的修改Java原生代码不同,你需要在JavaScript代码中将每个页面的组件用gestureHandlerRootHOC
包裹起来。可以像下面这样配置:
1 | import { gestureHandlerRootHOC } from 'react-native-gesture-handler'; |
这部分的配置也可以参考官方的示例项目。
记住你需要把每一个页面的组件(也就是导航库里管理的每个页面)都包裹在gestureHandlerRootHOC
下,只包裹主页面是不行的。
核心概念
Gesture Handlers
Gesture Handler是这个手势库的核心,它用来描述原生触控系统里的元素,这些元素能够被JavaScript代码使用React的组件进行实例化和控制。
每一个Handler类型都代表了一种手势(例如:滑动、缩放),也包含了每种手势特有的事件(例如:translation, scale)。
这些Handler可以在UI线程同步地解析触摸事件流,即便在JavaScript线程阻塞的情况下也能保证手势交互不被打断。
Gesture Handler的组件并不会在原生的视图层级里面创建一个视图,它仅仅是在自己库里面注册然后连接到原生的视图里。所以当我们在使用这些Handler组件的时候,一定要记得 在内部添加一个对应着原生视图的子组件。
这个库提供了以下几种手势:
- PanGestureHandler
- TapGestureHandler
- LongPressGestureHandler
- RotationGestureHandler
- FlingGestureHandler
- PinchGestureHandler
- ForceTouchGestureHandler
手势分类
这个手势库将手势分为两种:连续的和非连续的。
连续的手势被激活后会持续一段较长的时间,它会产生一个手势事件流。例如像滑动手势(PanGestureHandler),它被激活后就会开始持续为translation和其他属性提供更新。
而非连续性的手势一旦被激活就会立即结束。长按手势(LongPressGestureHandler
)就是一个非连续的手势,它只在手指按住持续一段时间后会被激活,并不会追踪手指的移动。
记住只有连续的手势才能使用onGestureEvent
,非连续性的手势Handler没有这个属性。
onGestureEvent
onGestureEvent
参数接收Animated.event
方法,这个方法是React Native系统自带的动画处理库的事件处理方法,例如:
1 | const circleRadius = 30; |
Animated.event
会持续将nativeEvent
里的x
属性的值同步到对应的_touchX
,而_touchX
的改变会同步到Animated.View
的translateX
的改变,从而导致Animated.View
的位移。上面就是一个简单的跟随手势移动的小球的例子。
这里其实也可以配合React Native Reanimated
库使用,直接传入useAnimatedGestureHandler
即可,在使用上也更简单,具体的使用方法以后的文章会讲到。
Handler的嵌套
Handler只是锚定了它的子组件,并没有在原生视图层级里创建新的视图,因此这些手势Handler并不支持直接嵌套,需要在两个手势Handler之间放入<Animated.View>
组件。
下面这种是不支持的:
1 | const PanAndRotate = () => ( |
需要在两个Handler之间放入<Animated.View>
:
1 | const PanAndRotate = () => ( |
另外一个特别需要注意的是当你在Animated.event
中使用了useNativeDriver
,它里面嵌套的子节点必须是Animated.API
类型的。比例像View
就必须被替换成Animated.View
。
Handler State
手势Handler可以被看作是一个状态机,每个Handler在有新的手势事件触发或者手势系统状态变更时都会更新当前的状态。
Handler的状态分为以下几种:
- UNDETERMINED
- FAILED
- BEGAN
- CANCELLED
- ACTIVE
- END
顾名思义,这里就不作过多解释了。
获取状态
我们可以通过onHandlerStateChange
来监听Handler的状态。状态可以通过nativeEvent
的state
属性获取到,然后与这个手势库中的State
对象里的常量进行对比:
1 | import { State, LongPressGestureHandler } from 'react-native-gesture-handler'; |
状态转换顺序
最典型的状态转换顺序就是手势Handler捕获到触摸事件,然后识别出具体的手势,手势结束后重置到最初状态。这种状态转换顺序如下所示(长箭头表示状态改变前这里可能有更多的触摸事件):
UNDETERMINED
-> BEGAN
——> ACTIVE
——> END
-> UNDETERMINED
下面这种是Handler捕获到了触摸事件但是识别手势的时候失败的情况:
UNDETERMINED
-> BEGAN
——> FAILED
-> UNDETERMINED
下面这种是手势中断的情况:
UNDETERMINED
-> BEGAN
——> ACTIVE
——> CANCELLED
-> UNDETERMINED
手势之间的交互
这个手势库支持不同的手势Handler之间通信来构建更加复杂的手势交互。
有下面两种方法可以实现这种交互控制。每一种方法手势Handler都需要提供一个引用给其他Handler。手势Handler的引用是通过React.createRef()
来创建的引用对象。
同时识别
默认情况下同一个时间只有一种手势Handler可以是激活状态。当手势Handler识别到了一个手势,它会取消其他所有处于began状态的手势Handler并且在其激活状态下停止接收其他任何触摸事件。
这种行为可以通过simultaneousHandlers
这个属性来改变,并且这个属性每种类型的Handler都有。这个属性持有一个数组,数组里有其他手势Handler的引用。手势Handler可以通过这种方式同时处于激活状态。
使用场景
当我们实现图片预览组件的时候就需要这种同时识别,在图片预览中我们可以缩放、旋转而且可以在它缩放时移动它。在这个场景中我们需要使用PinchGestureHandler
, RotationGestureHandler
和PanGestureHandler
并让它们能够被同时识别。
示例
可以查看官方示例App中的“Scale, rotate & tilt” example部分,以下是其中的片段:
1 | class PinchableBox extends React.Component { |
等待其他手势完成
使用场景
这种手势交互方式最好的例子就是当我们在一个视图上同时注册了单次点击和双击事件的情况。这种情况下就需要单击事件等待双击事件识别完成后才识别,否则就会出现只识别单击事件而双击事件无法触发的情况。
示例
参考官方示例App中的“Multitap” example部分,以下是部分片段:
1 | const doubleTap = React.createRef(); |
总结
至此,React Native Gesture Handler的基本使用就介绍完了。关于React Native优化,本文介绍的手势库只是解决了手势方面的性能问题,一般来说,手势都是配合了相应的动画使用的,比如手势拖拽功能,后面的文章会继续讲解动画的性能优化库React Native Reanimated以及这两个库如何配合使用。