23.React Native觸摸事件處理學習;

目錄

1.什麼是觸摸事件

2.基礎觸摸事件組件

3.React Native單組件觸摸響應

3.1申請成爲響應者和釋放響應者

3.2合成事件介紹-evt

4.嵌套組件事件傳遞

4.1正常事件響應順序

4.2父視圖攔截事件傳遞

5.手勢識別-PanResponder

5.1PanResponder類介紹

5.2gestureState參數介紹

5.3PanResponder基本用法 

5.4PanResponder使用示例

6.總結


1.什麼是觸摸事件

簡單理解就是我們手指在屏幕上的觸摸動作-移動應用上的用戶交互基本靠“摸”。當然,“摸”也是有各種姿勢的:在一個按鈕上點擊,在一個列表上滑動,或是在一個地圖上縮放

觸摸是手機的核心功能,也是每個開發應用的交互基礎,Android和IOS都有完善的事件處理機制;React Native(RN)也有完善 的事件處理機制,能方便處理的用戶的觸摸事件和手勢操作;

2.基礎觸摸事件組件

通常需要監聽簡單的點擊或者長安只需要使用"Touchable"開頭的一系列組件,這些組件通過onPress屬性接受一個點擊事件的處理函數。當一個點擊操作開始並且終止於本組件時(即在本組件上按下手指並且擡起手指時也沒有移開到組件外),此函數會被調用。

常用的觸摸組件TouchableHighlight,TouchableNativeFeedback,TouchableWithoutFeedback,TouchableOpacity可以根據按下顯示效果選擇不同的Touchable組件;

可點擊的組件需要給用戶提供視覺反饋,例如是哪個組件正在響應用戶的操作,以及當用戶擡起手指後會發生什麼。用戶也應可以通過把手指移到一邊來取消點擊操作。

具體使用哪種組件,取決於你希望給用戶什麼樣的視覺反饋:
a.一般來說,你可以使用TouchableHighlight來製作按鈕或者鏈接。注意此組件的背景會在用戶手指按下時變暗。
b.在Android上還可以使用TouchableNativeFeedback,它會在用戶手指按下時形成類似墨水漣漪的視覺效果。
c.TouchableOpacity會在用戶手指按下時降低按鈕的透明度,而不會改變背景的顏色。
d.如果你想在處理點擊事件的同時不顯示任何視覺反饋,則需要使用TouchableWithoutFeedback;

示例:

class MyButton extends Component {
  _onPressButton() {
    console.log("You onPress the button!");
  }

  _onLongPress() {
    console.log("You onLongPress the button!");
  }

  _onPressIn() {
    console.log("You onPressIn the button!");
  }
  _onPressOut() {
    console.log("You onPressOut the button!");
  }

  render() {
    return (
      <TouchableHighlight 
            onPress={this._onPressButton}    //按下擡起時觸發
            onLongPress={this._onLongPress}    //長按監聽
            //當觸摸操作結束時調用,但如果被取消了則不調用(譬如響應者被一個滾動操作取代)
            onPressIn={this._onPressIn}        //按下監聽
            onPressOut={this._onPressOut}      //擡起監聽
            >
        <Text>Button</Text>
      </TouchableHighlight>
    );
  }
}

RN提供相關Touchable**觸摸事件的完整監聽和處理,可以參考RN中文網;

https://reactnative.cn/docs/0.50/touchablewithoutfeedback.html

3.React Native單組件觸摸響應

一個View並不是自動成爲事件響應者,一個View只要實現了正確的協商方法,就可以成爲觸摸事件的響應者。一個完整的事件從手指觸摸屏幕開始,系統通過回調詢問View是否願意成爲事件響應者,成爲響應者,處理觸摸事件,響應者釋放,結束觸摸事件處理;

單個組件響應者生命的生命週期處理流程圖:

單組件分爲兩種狀態,非響應者狀態和響應者狀態 ,非響應狀態指未接收處理觸摸事件之前,響應者狀態接收並開始處理觸摸 事件;

默認情況下,組件屬於非響應者狀態,即不接收觸摸事件,需要通過onStartShouldSetResponder和onMoveShouldSetResponder回調方法返回bool值決定是否成爲事件響應者;

3.1申請成爲響應者和釋放響應者

分爲三個階段:

a.“詢問”成爲響應者階段

我們通過兩個方法去“詢問”一個View是否願意成爲響應者:

1)View.props.onStartShouldSetResponder: (event) => [true | false], 其中 event 是一個合成觸摸事件,onStartShouldSetResponder屬性是視圖觸摸監聽的回調事件,在按下(touchDown)時,會調用onStartShouldSetResponder屬性,詢問是否想要成爲響應者,返回true,成爲響應者,接收觸摸事件,返回false不成爲事件響應者;

2)View.props.onMoveShouldSetResponder: (event) => [true | false], 其中 event 是一個合成觸摸事件,和onStartShouldSetResponder類似處理,如果onStartShouldSetResponder返回 false爲成爲響應者,那麼在每一個觸摸點開始移動(沒有停下也沒有離開屏幕)時再詢問一次:是否願意響應觸摸交互呢?返回true,成爲響應者,接收觸摸事件,返回false不成爲事件響應者;

View回調”詢問“返回true嘗試開始成爲響應者,會觸發下面兩個事件回調之一:

1)申請響應者成功:View開始響應觸摸事件;執行回調View.props.onResponderGrant: (evt) => {},這也是需要做高亮的時候,使用戶知道他到底點到了哪裏。View開始處理觸摸事件或者做一些初始化處理;

2)申請響應者失敗:響應者現在“另有其人”而且暫時不會“放權”,請另作安排;執行回調View.props.onResponderReject: (evt) => {};不接受和處理觸摸事件;

b.成爲響應者階段

1)View.props.onResponderMove: (evt) => {} - 用戶正在屏幕上移動手指時(沒有停下也沒有離開屏幕),接收處理觸摸事件;

2)View.props.onResponderRelease: (evt) => {} - 觸摸操作結束時觸發,比如"touchUp"(手指擡起離開屏幕),停止處理觸摸事件並釋放相關資源;

c.釋放響應者階段

1)View.props.onResponderTerminationRequest: (evt) => true - 有其他組件請求接替響應者,當前的View是否“放權”?返回true的話則釋放響應者權力。則不再接收處理觸摸事件

2)View.props.onResponderTerminate: (evt) => {} - 響應者權力已經交出。這可能是由於其他View通過onResponderTerminationRequest請求的,也可能是由操作系統強制奪權(比如iOS上的控制中心或是通知中心)。

3.2合成事件介紹-evt

從觸摸回調監聽看到,通常都會返回evt觸摸事件,下面是觸摸事件內部的相關參數說明:

nativeEvent
    changedTouches - 在上一次事件之後,所有發生變化的觸摸事件的數組集合(即上一次事件後,所有移動過的觸摸點)
    identifier - 觸摸點的ID
    locationX - 觸摸點相對於父元素的橫座標
    locationY - 觸摸點相對於父元素的縱座標
    pageX - 觸摸點相對於根元素的橫座標
    pageY - 觸摸點相對於根元素的縱座標
    target - 觸摸點所在的元素ID
    timestamp - 觸摸事件的時間戳,可用於移動速度的計算
    touches - 當前屏幕上的所有觸摸點的集合

4.嵌套組件事件傳遞

4.1正常事件響應順序

onStartShouldSetResponder與onMoveShouldSetResponder是以冒泡的形式調用的,即嵌套最深的節點最先調用。這意味着當多個View同時在*ShouldSetResponder中返回true時,最底層的View將優先“奪權”。在多數情況下這並沒有什麼問題,因爲這樣可以確保所有控件和按鈕是可用的。

4.2父視圖攔截事件傳遞

但是有些時候,某個父View會希望能先成爲響應者。我們可以利用“捕獲期”來解決這一需求。響應系統在從最底層的組件開始冒泡之前,會首先執行一個“捕獲期”,在此期間會觸發on*ShouldSetResponderCapture系列事件。因此,如果某個父View想要在觸摸操作開始時阻止子組件成爲響應者,那就應該處理onStartShouldSetResponderCapture事件並返回true值

View.props.onStartShouldSetResponderCapture: (evt) => true,攔截事件繼續傳遞
View.props.onMoveShouldSetResponderCapture: (evt) => true,攔截事件繼續傳遞

嵌套組件示例:A父組件,B兒子組件,C孫子組件;

on*Capture事件攔截處理傳遞順序是A-B-C,返回true,則調用相應組件的onResponder*方法;on*Responder申請成爲響應者的順序是C-B-A,返回true,則調用相應組件的onResponder*方法;

注意到,圖中還有 onTouchStart/onTouchStop 回調,這個回調並不受響應者的影響,在範圍內的組件都會回調此函數,而且調用順序是從最深層組件到最上層組件。

5.手勢識別-PanResponder

5.1PanResponder類介紹

PanResponder類可以將多點觸摸操作協調成一個手勢。它使得一個單點觸摸可以接受更多的觸摸操作,也可以用於識別簡單的多點觸摸手勢。

前面只是介紹了簡單的觸摸事件處理機制及其使用方法,其實連續的觸摸事件,可以組成一些更高級手勢,例如我們最常見的滑動屏幕內容,雙指縮放(Pinch)或者旋轉圖片都是通過手勢識別完成的。

因爲有些手勢是很常用的,RN 也提供了內置的手勢識別庫 PanResponder ,它封裝了上面的事件回調函數,對觸摸事件數據進行加工,完成滑動手勢識別,向我們提供更加高級有意義的接口,如下:

static create(config: object) #
@param {object} 配置所有響應器回調的加強版本,不僅僅包括原本的ResponderSyntheticEvent,還包括PanResponder手勢狀態的
回調。你只要簡單的把onResponder*回調中的Responder替換爲PanResponder。舉例來說,這個config對象可能看起來像這樣:

onMoveShouldSetPanResponder: (e, gestureState) => {...}
onMoveShouldSetPanResponderCapture: (e, gestureState) => {...}
onStartShouldSetPanResponder: (e, gestureState) => {...}
onStartShouldSetPanResponderCapture: (e, gestureState) => {...}
onPanResponderReject: (e, gestureState) => {...}
onPanResponderGrant: (e, gestureState) => {...}
onPanResponderStart: (e, gestureState) => {...}
onPanResponderEnd: (e, gestureState) => {...}
onPanResponderRelease: (e, gestureState) => {...}
onPanResponderMove: (e, gestureState) => {...}
onPanResponderTerminate: (e, gestureState) => {...}
onPanResponderTerminationRequest: (e, gestureState) => {...}
onShouldBlockNativeResponder: (e, gestureState) => {...}

通常來說,對那些有對應捕獲事件的事件來說,我們在捕獲階段更新gestureState一次,然後在冒泡階段直接使用即可。

注意onStartShould* 回調。他們只會在此節點冒泡/捕獲的開始/結束事件中提供已經更新過的gestureState。一旦這個節點成爲
了事件的響應者,則所有的開始/結束事件都會被手勢正確處理,並且gestureState也會被正確更新。(numberActiveTouches)有可能沒
有包含所有的觸摸點,除非你就是觸摸事件的響應者。

5.2gestureState參數介紹

onPanResponderMove: (event, gestureState) => {}

 

一個gestureState對象有如下的字段:
    stateID - 觸摸狀態的ID。在屏幕上有至少一個觸摸點的情況下,這個ID會一直有效。
    moveX - 最近一次移動時的屏幕橫座標
    moveY - 最近一次移動時的屏幕縱座標
    x0 - 當響應器產生時的屏幕座標
    y0 - 當響應器產生時的屏幕座標
    dx - 從觸摸操作開始時的累計橫向路程
    dy - 從觸摸操作開始時的累計縱向路程
    vx - 當前的橫向移動速度
    vy - 當前的縱向移動速度
    numberActiveTouches - 當前在屏幕上的有效觸摸點的數量

5.3PanResponder基本用法 

 componentWillMount: function() {
    this._panResponder = PanResponder.create({
      // 要求成爲響應者:
      onStartShouldSetPanResponder: (evt, gestureState) => true,
      onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
      onMoveShouldSetPanResponder: (evt, gestureState) => true,
      onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,

      onPanResponderGrant: (evt, gestureState) => {
        // 開始手勢操作。給用戶一些視覺反饋,讓他們知道發生了什麼事情!

        // gestureState.{x,y} 現在會被設置爲0
      },
      onPanResponderMove: (evt, gestureState) => {
        // 最近一次的移動距離爲gestureState.move{X,Y}

        // 從成爲響應者開始時的累計手勢移動距離爲gestureState.d{x,y}
      },
      onPanResponderTerminationRequest: (evt, gestureState) => true,
      onPanResponderRelease: (evt, gestureState) => {
        // 用戶放開了所有的觸摸點,且此時視圖已經成爲了響應者。
        // 一般來說這意味着一個手勢操作已經成功完成。
      },
      onPanResponderTerminate: (evt, gestureState) => {
        // 另一個組件已經成爲了新的響應者,所以當前手勢將被取消。
      },
      onShouldBlockNativeResponder: (evt, gestureState) => {
        // 返回一個布爾值,決定當前組件是否應該阻止原生組件成爲JS響應者
        // 默認返回true。目前暫時只支持android。
        return true;
      },
    });
  },

  render: function() {
    return (
      <View {...this._panResponder.panHandlers} />
    );
  },

5.4PanResponder使用示例

import React, { Component } from 'react';
import {
    Alert,
    StyleSheet,
    Text,
    View,
    Navigator,
    PanResponder
} from 'react-native';

export default class NavTwo extends Component{
    constructor(props){
        super(props);
        this.state = {
            eventName:'',
            pos: '',
        };
        this.myPanResponder={}
    }

    componentWillMount() {
        this.myPanResponder = PanResponder.create({
            //要求成爲響應者:
            onStartShouldSetPanResponder: (evt, gestureState) => true,
            onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
            onMoveShouldSetPanResponder: (evt, gestureState) => true,
            onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
            onPanResponderTerminationRequest: (evt, gestureState) => true,

            //響應對應事件後的處理:
            onPanResponderGrant: (evt, gestureState) => {
                this.state.eventName='觸摸開始';
                this.forceUpdate();
            },
            onPanResponderMove: (evt, gestureState) => {
                let _pos = 'x:' + gestureState.moveX + ',y:' + gestureState.moveY;
                this.setState( {eventName:'移動',pos : _pos} );
            },
            onPanResponderRelease: (evt, gestureState) => {
                this.setState( {eventName:'擡手'} );
            },
            onPanResponderTerminate: (evt, gestureState) => {
                this.setState( {eventName:'另一個組件已經成爲了新的響應者'} )
            },
        });
    }

    render(){
        return (
            <View style={styles.container} {...this.myPanResponder.panHandlers}>
                <Text>eventName:{this.state.eventName}|{this.state.pos}</Text>
            </View>
        );
    }
}

const styles = StyleSheet.create({
    container:{
        backgroundColor:"#fff",
        flex:1
    }
});

6.總結

通過上面的介紹,可以看到 RN 中提供了類似 Native 平臺的事件處理機制,所以也可以實現各種的觸摸事件處理,甚至也可以實現複雜的手勢識別。

在嵌套組件的事件處理中,RN 中提供了“冒泡”和“下沉”兩個方向的事件處理,這有點類似於 Android Native 上不久前才支持的 NestedScrolling,這就提供更加強大的事件處理機制。

另外需要注意,因爲 RN 的異步通信和執行機制,前面描述的所有回調函數都是在 JS 線程中,並不是 Native 的 UI 線程,而 Native 平臺的 Touch 事件都是在 UI 線程中。所以在 JS 中通過 Touch 或者手勢實現動畫,可能會延遲的問題。

 

參考:

https://reactnative.cn/docs/0.50/panresponder.html

https://www.jianshu.com/p/98e0b21473be

 

 

 

 

 

 

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