【React Native进阶】React Native Gesture Handler的使用

宫俊远
2023-12-01

背景

说到React Navtive的性能优化,首先要了解React Native的运行机制。React Native程序主要运行在三个并行的线程上:

  • JS Thread:我们写的JS代码逻辑都是在这个线程上执行;
  • UI Thread:即原生线程,当我们需要调用原生的渲染或者能力时会运行到这个线程上;
  • Shadow Thread:这个线程创建和管理着Shadow Tree,它类似于虚拟DOM。它通过Yoga引擎着Flexbox布局转化为原生的布局方式。

这三个线程独立运行的情况下,性能良好,但如果存在和UI线程有交互的情况,就可能出现性能瓶颈。由于UI线程与其他线程通信存在序列化和反序列化这个比较消耗性能的步骤,而且这些通信都是异步的,当UI线程与其他线程交互比较频繁或者其他线程负荷较大计算结果有延迟,就容易出现掉桢的现象。

我们的RN代码逻辑都是用JS写的,JS线程也是负荷最大的线程。因此在RN的性能优化上主要要考虑两个方面:

  • 减少与UI线程的通信;
  • 减少JS线程的负荷;

React Native Gesture Handler正是从这两个方面优化RN在手势操作方面的性能。它旨在替换RN自带的手势处理系统。如果你使用过系统自带的手势处理系统,会发现在JS线程会有大量的计算,这些计算也会频繁与UI线程通信,对性能影响较大。具体代码可以自行比较,这里不再赘述。

功能

React Native Gesture Handler提供了以下功能:

  • 提供了包括缩放、旋转、屏蔽滑动等手势的处理系统;
  • 能够定义多个手势之间的关系。例如:当你在ScrollView里面加入一个滑动手势(pan handler)时,可以让滑动手势响应结束后再响应ScrollView
  • 提供了让手势运行在原生线程(UI线程)上并遵从原生平台默认行为机制;
  • 由于使用了原生的动画驱动,即便在JS线程已经超负荷的情况,也能够提供顺滑的手势交互。

安装

整个安装分为三个部分: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的方法,让这个库的根视图包裹安卓的主活动。注意在文件顶部需要导入ReactActivityDelegateReactRootViewRNGestureHandlerEnabledRootView

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.swmansion.gesturehandler.react.example;

import com.facebook.react.ReactActivity;
+ import com.facebook.react.ReactActivityDelegate;
+ import com.facebook.react.ReactRootView;
+ import com.swmansion.gesturehandler.react.RNGestureHandlerEnabledRootView;

public class MainActivity extends ReactActivity {

  @Override
  protected String getMainComponentName() {
    return "Example";
  }

+  @Override
+  protected ReactActivityDelegate createReactActivityDelegate() {
+    return new ReactActivityDelegate(this, getMainComponentName()) {
+      @Override
+      protected ReactRootView createRootView() {
+       return new RNGestureHandlerEnabledRootView(MainActivity.this);
+      }
+    };
+  }
}

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原生代码不同,你需要在JS代码中将每个页面的组件用gestureHandlerRootHOC包裹起来。可以像下面这样配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { gestureHandlerRootHOC } from 'react-native-gesture-handler';
import { Navigation } from 'react-native-navigation';

import FirstTabScreen from './FirstTabScreen';
import SecondTabScreen from './SecondTabScreen';
import PushedScreen from './PushedScreen';

// register all screens of the app (including internal ones)
export function registerScreens() {
  Navigation.registerComponent('example.FirstTabScreen', () =>
    gestureHandlerRootHOC(FirstTabScreen)
  );
  Navigation.registerComponent('example.SecondTabScreen', () =>
    gestureHandlerRootHOC(SecondTabScreen)
  );
  Navigation.registerComponent('example.PushedScreen', () =>
    gestureHandlerRootHOC(PushedScreen)
  );
}

这部分的配置也可以参考官方的示例项目

记住你需要把每一个页面的组件(也就是导航库里管理的每个页面)都包裹在gestureHandlerRootHOC下,只包裹主页面是不行的。

核心概念

Gesture Handlers

Gesture Handler是这个手势库的核心,它用来描述原生触控系统里的元素,这些元素能够被JS代码使用React的组件进行实例化和控制。

每一个Handler类型都代表了一种手势(例如:滑动、缩放),也包含了每种手势特有的事件(例如:translation, scale)。

这些Handler可以在UI线程同步地解析触摸事件流,即便在JS线程阻塞的情况下也能保证手势交互不被打断。

Gesture Handler的组件并不会在原生的视图层级里面创建一个视图,它仅仅是在自己库里面注册然后连接到原生的视图里。所以当我们在使用这些Handler组件的时候,一定要记得 在内部添加一个对应着原生视图的子组件。

这个库提供了以下几种手势:

手势分类

这个手势库将手势分为两种:连续的和非连续的。

连续的手势被激活后会持续一段较长的时间,它会产生一个手势事件流。例如像滑动手势(PanGestureHandler),它被激活后就会开始持续为translation和其他属性提供更新。

而非连续性的手势一旦被激活就会立即结束。长按手势(LongPressGestureHandler)就是一个非连续的手势,它只在手指按住持续一段时间后会被激活,并不会追踪手指的移动。

记住只有连续的手势才能使用onGestureEvent,非连续性的手势Handler没有这个属性。

onGestureEvent

onGestureEvent参数接收Animated.event方法,这个方法是React Native系统自带的动画处理库的事件处理方法,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
const circleRadius = 30;
class Circle extends Component {
  _touchX = new Animated.Value(windowWidth / 2 - circleRadius);
  _onPanGestureEvent = Animated.event([{ nativeEvent: { x: this._touchX } }], {
    useNativeDriver: true,
  });
  render() {
    return (
      <PanGestureHandler onGestureEvent={this._onPanGestureEvent}>
        <Animated.View
          style={{
            height: 150,
            justifyContent: 'center',
          }}>
          <Animated.View
            style={[
              {
                backgroundColor: '#42a5f5',
                borderRadius: circleRadius,
                height: circleRadius * 2,
                width: circleRadius * 2,
              },
              {
                transform: [
                  {
                    translateX: Animated.add(
                      this._touchX,
                      new Animated.Value(-circleRadius)
                    ),
                  },
                ],
              },
            ]}
          />
        </Animated.View>
      </PanGestureHandler>
    );
  }
}

Animated.event会持续将nativeEvent里的x属性的值同步到对应的_touchX,而_touchX的改变会同步到Animated.ViewtranslateX的改变,从而导致Animated.View的位移。上面就是一个简单的跟随手势移动的小球的例子。

这里其实也可以配合React Native Reanimated库使用,直接传入useAnimatedGestureHandler即可,在使用上也更简单,具体的使用方法以后的文章会讲到。

Handler的嵌套

Handler只是锚定了它的子组件,并没有在原生视图层级里创建新的视图,因此这些手势Handler并不支持直接嵌套,需要在两个手势Handler之间放入<Animated.View>组件。

下面这种是不支持的:

1
2
3
4
5
6
7
const PanAndRotate = () => (
  <PanGestureHandler onGestureEvent={Animated.event({ ... }, { useNativeDriver: true })}>
    <RotationGestureHandler onGestureEvent={Animated.event({ ... }, { useNativeDriver: true })}>
      <Animated.View style={animatedStyles}/>
    </RotationGestureHandler>
  </PanGestureHandler>
);

需要在两个Handler之间放入<Animated.View>

1
2
3
4
5
6
7
8
9
const PanAndRotate = () => (
  <PanGestureHandler onGestureEvent={Animated.event({ ... }, { useNativeDriver: true })}>
    <Animated.View>
      <RotationGestureHandler onGestureEvent={Animated.event({ ... }, { useNativeDriver: true })}>
        <Animated.View style={animatedStyles}/>
      </RotationGestureHandler>
    </Animated.View>
  </PanGestureHandler>
);

另外一个特别需要注意的是当你在Animated.event中使用了useNativeDriver,它里面嵌套的子节点必须是Animated.API类型的。比例像View就必须被替换成Animated.View

Handler State

手势Handler可以被看作是一个状态机,每个Handler在有新的手势事件触发或者手势系统状态变更时都会更新当前的状态。

Handler的状态分为以下几种:

  • UNDETERMINED
  • FAILED
  • BEGAN
  • CANCELLED
  • ACTIVE
  • END

顾名思义,这里就不作过多解释了。

获取状态

我们可以通过onHandlerStateChange来监听Handler的状态。状态可以通过nativeEventstate属性获取到,然后与这个手势库中的State对象里的常量进行对比:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { State, LongPressGestureHandler } from 'react-native-gesture-handler';

class Demo extends Component {
  _handleStateChange = ({ nativeEvent }) => {
    if (nativeEvent.state === State.ACTIVE) {
      Alert.alert('Longpress');
    }
  };
  render() {
    return (
      <LongPressGestureHandler onHandlerStateChange={this._handleStateChange}>
        <Text style={styles.buttonText}>Longpress me</Text>
      </LongPressGestureHandler>
    );
  }
}

状态转换顺序

最典型的状态转换顺序就是手势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可以通过这种方式同时处于激活状态。

使用场景

当我们实现图片预览组件的时候就需要这种同时识别,在图片预览中我们可以缩放、旋转而且可以在它缩放时移动它。在这个场景中我们需要使用PinchGestureHandlerRotationGestureHandlerPanGestureHandler并让它们能够被同时识别。

示例

可以查看官方示例App中的“Scale, rotate & tilt” example部分,以下是其中的片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class PinchableBox extends React.Component {
  // ...take a look on full implementation in an Example app
  render() {
    const imagePinch = React.createRef();
    const imageRotation = React.createRef();
    return (
      <RotationGestureHandler
        ref={imageRotation}
        simultaneousHandlers={imagePinch}
        onGestureEvent={this._onRotateGestureEvent}
        onHandlerStateChange={this._onRotateHandlerStateChange}>
        <Animated.View>
          <PinchGestureHandler
            ref={imagePinch}
            simultaneousHandlers={imageRotation}
            onGestureEvent={this._onPinchGestureEvent}
            onHandlerStateChange={this._onPinchHandlerStateChange}>
            <Animated.View style={styles.container} collapsable={false}>
              <Animated.Image
                style={[
                  styles.pinchableImage,
                  {
                    /* events-related transformations */
                  },
                ]}
              />
            </Animated.View>
          </PinchGestureHandler>
        </Animated.View>
      </RotationGestureHandler>
    );
  }
}

等待其他手势完成

使用场景

这种手势交互方式最好的例子就是当我们在一个视图上同时注册了单次点击和双击事件的情况。这种情况下就需要单击事件等待双击事件识别完成后才识别,否则就会出现只识别单击事件而双击事件无法触发的情况。

示例

参考官方示例App中的“Multitap” example部分,以下是部分片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const doubleTap = React.createRef();
const PressBox = () => (
  <TapGestureHandler
    onHandlerStateChange={({ nativeEvent }) =>
      nativeEvent.state === State.ACTIVE && Alert.alert('Single tap!')
    }
    waitFor={doubleTap}>
    <TapGestureHandler
      ref={doubleTap}
      onHandlerStateChange={({ nativeEvent }) =>
        nativeEvent.state === State.ACTIVE && Alert.alert("You're so fast")
      }
      numberOfTaps={2}>
      <View style={styles.box} />
    </TapGestureHandler>
  </TapGestureHandler>
);

总结

至此,React Native Gesture Handler的基本使用就介绍完了。关于React Native优化,本文介绍的手势库只是解决了手势方面的性能问题,一般来说,手势都是配合了相应的动画使用的,比如手势拖拽功能,后面的文章会继续讲解动画的性能优化库React Native Reanimated以及这两个库如何配合使用。

 类似资料: