【React Native進階】React Native Gesture Handler的使用

背景

說到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以及這兩個庫如何配合使用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章