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

語言: CN / TW / HK

說到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 安裝:

yarn add react-native-gesture-handler

或者你也可以選擇使用 npm

npm install --save react-native-gesture-handler

Android

如果在專案中使用了導航庫(例如: react-native-navigation ),直接跳過這部分看後面的小節。

更新 MainActivity.java 檔案(或者你在其他地方建立的 ReactActivityDelegate 例項的內部),重寫建立 ReactRootView 的方法,讓這個庫的根檢視包裹安卓的主活動。注意在檔案頂部需要匯入 ReactActivityDelegateReactRootViewRNGestureHandlerEnabledRootView

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:

cd ios && pod install

如果React Native版本為0.61或更高,則需要在index.js檔案頂部匯入庫檔案:

import 'react-native-gesture-handler';

配合導航庫使用

如果你在專案中使用了像 react-native-navigation 這樣的導航庫,由於本地導航庫和Gesture Handler庫都需要它們自己的 ReactRootView 子類,在安卓不能使用上述配置,需要如下單獨配置。

與上面的修改Java原生程式碼不同,你需要在JS程式碼中將每個頁面的元件用 gestureHandlerRootHOC 包裹起來。可以像下面這樣配置:

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系統自帶的動畫處理庫的事件處理方法,例如:

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> 元件。

下面這種是不支援的:

const PanAndRotate = () => (
  <PanGestureHandler onGestureEvent={Animated.event({ ... }, { useNativeDriver: true })}>
    <RotationGestureHandler onGestureEvent={Animated.event({ ... }, { useNativeDriver: true })}>
      <Animated.View style={animatedStyles}/>
    </RotationGestureHandler>
  </PanGestureHandler>
);

需要在兩個Handler之間放入 <Animated.View>

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 物件裡的常量進行對比:

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可以通過這種方式同時處於啟用狀態。

使用場景

當我們實現圖片預覽元件的時候就需要這種同時識別,在圖片預覽中我們可以縮放、旋轉而且可以在它縮放時移動它。在這個場景中我們需要使用 PinchGestureHandler , RotationGestureHandlerPanGestureHandler 並讓它們能夠被同時識別。

示例

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

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 部分,以下是部分片段:

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