相對於原生的觸摸事件處理機制,RN也有一套自己的處理機制,大體上和原生差不多,但是基於RN是應用在iOS和Android的兩個平臺,有時候會存在一些手勢上的衝突,比如,在一個橫向滑動的ScrollView上放置兩個TouchableXXX組件,有時我們橫向滑動的時候會觸發TouchableXXX的點擊事件。當然如果我們瞭解了RN的觸摸處理機制,可以很簡單地解決上面的問題,首先我們瞭解一下RN的觸摸事件機制。
RN基本觸摸組件
RN 的組件除了 Text,其他組件默認是不支持點擊事件,也不能響應基本觸摸事件,所以 RN 中提供了幾個直接處理響應事件的組件,基本上能夠滿大部分的點擊處理需求TouchableHighlight, TouchableNativeFeedback, TouchableOpacity 和 TouchableWithoutFeedback。因爲這幾個組件的功能和使用方法基本類似,只是 Touch 的反饋效果不一樣,所以一般我們用 Touchable** 代替。Touchable** 有如下幾個回調方法:
- onPressIn:點擊開始;
- onPressOut:點擊結束或者離開;
- onPress:單擊事件回調;
- onLongPress:長按事件回調。
- RN 中提供的觸摸組件使用非常簡單。接下來我們瞭解下單個組件的觸摸事件處理機制
單組件觸摸事件處理
RN 的組件默認不進行處理觸摸事件。組件要處理觸摸事件,首先要“申請”成爲摸事件的響應者(Responder),完成事件處理以後,會釋放響應者的角色。一個觸摸事件處理週期,是從用戶手指按下屏幕,到用戶擡起手指擡起結束,這是用戶的一次完整觸摸操作。
單個組件的單次操作交互處理的生命週期如下:
我們來詳細分析一下事件處理的生命週期,在整個事件處理的過程中,組件有可能處於兩種身份中的一種,並且可以相互切換:非事件響應者和事件響應者。
非事件響應者
默認情況下,觸摸事件輸入不會直接傳遞給組件,不能進行事件響應處理,也就是非事件響應者。如果組件要進行觸摸事件處理,首先要申請成爲事件響應者,組件有如下兩個屬性可以做這樣的申請:
非事件響應者
默認情況下,觸摸事件輸入不會直接傳遞給組件,不能進行事件響應處理,也就是非事件響應者。如果組件要進行觸摸事件處理,首先要申請成爲事件響應者,組件有如下兩個屬性可以做這樣的申請:
View.props.onStartShouldSetResponder,這個屬性接收一個回調函數,函數原型是 function(evt): bool,在觸摸事件開始(touchDown)的時候,RN 會回調此函數,詢問組件是否需要成爲事件響應者,接收事件處理,如果返回 true,表示需要成爲響應者;
View.props.onMoveShouldSetResponder,它和前一個屬性類似,不過這是觸摸是進行過程中(touchMove),RN 詢問組件是否要成爲響應者,返回 true 表示是。
假如組件通過上面的方法返回了 true,表示發出了申請要成爲事件響應者請求,想要接收後續的事件輸入。因爲同一時刻,只能有一個事件處理響應者,RN 還需要協調所有組件的事件處理請求,所以不是每個組件申請都能成功,RN 通過如下兩個回調來通知告訴組件它的申請結果,:
View.props.onResponderGrant: (evt) => {}:表示申請成功,組件成爲了事件處理響應者,這時組件就開始接收後序的觸摸事件輸入。一般情況下,這時開始,組件進入了激活狀態,並進行一些事件處理或者手勢識別的初始化。
View.props.onResponderReject: (evt) => {}:表示申請失敗了,這意味者其他組件正在進行事件處理,並且它不想放棄事件處理,所以你的申請被拒絕了,後續輸入事件不會傳遞給本組件進行處理。
事件響應者
如果通過上面的步驟,組件申請成爲了事件響應者,後續的事件輸入都會通過回調函數通知到組件,如下:
- View.props.onResponderStart: (evt) => {}:表示手指按下時,成功申請爲事件響應者的回調;
- View.props.onResponderMove: (evt) => {}:表示觸摸手指移動的事件,這個回調可能非常頻繁,所以這個回調函數的內容需要儘量簡單;
- View.props.onResponderRelease: (evt) => {}:表示觸摸完成(touchUp)的時候的回調,表示用戶完成了本次的觸摸交互,這裏應該完成手勢識別的處理,這以後,組件不再是事件響應者,組件取消激活。
- View.props.onResponderEnd: (evt) => {}:表示組件結束事件響應的回調。
從前面的圖中也看到,在組件成爲事件響應者期間,其他組件也可能會申請觸摸事件處理。此時 RN 會通過回調詢問你是否可以釋放響應者角色讓給其他組件。回調如下:
View.props.onResponderTerminationRequest: (evt) => bool
如果回調函數返回爲 true,則表示同意釋放響應者角色,同時會回調如下函數,通知組件事件響應處理被終止了:
View.props.onResponderTerminate: (evt) => {}
這個回調也會發生在系統直接終止組件的事件處理,例如用戶在觸摸操作過程中,突然來電話的情況。
事件數據結構
從前面我們看到,觸摸事件處理的回調都有一個 evt 參數,包含一個觸摸事件數據 nativeEvent。nativeEvent 的詳細內容如下:
identifier:觸摸的 ID,一般對應手指,在多點觸控的時候,用來區分是哪個手指的觸摸事件;
locationX 和 locationY:觸摸點相對組件的位置;
pageX 和 pageY:觸摸點相對於屏幕的位置;
timestamp:當前觸摸的事件的時間戳,可以用來進行滑動計算;
target:接收當前觸摸事件的組件 ID;
changedTouches:evt 數組,從上次回調上報的觸摸事件,到這次上報之間的所有事件數組。因爲用戶觸摸過程中,會產生大量事件,有時候可能沒有及時上報,系統用這種方式批量上報;
touches:evt 數組,多點觸摸的時候,包含當前所有觸摸點的事件。
這些數據中,最常用的是 locationX 和 locationY 數據,需要注意的是,因爲這裏是 Native 的數據,所以他們的單位是實際像素。如果要轉換爲 RN 中的邏輯單位,可以示使用如下方法:
var pX = evt.nativeEvent.locationX / PixelRatio.get();
嵌套組件事件處理
上一小節介紹的都是針對單個組件來說,事件處理的流程和機制。但是前面也提到了,當組件需要作爲事件處理響應者時,需要通過 onStartShouldSetResponder 或者 onMoveShouldSetResponder 回調返回值爲 true 來申請。假如當多個組件嵌套的時候,這兩個回調都返回了 true 的時候,但是同一個只能有一個事件處理響應者,這種情況怎麼處理呢?爲了便於描述,假設我們的組件佈局如下:
在 RN 中,默認情況下使用冒泡機制,響應最深的組件最先開始響應,所以前面描述的這種情況,如圖中,如果 A、B、C 三個組件的 on*ShouldSetResponder 都返回爲 true,那麼只有 C 組件會得到響應成爲響應者。這種機制才能保證了界面所有的組件才能得到響應。但是有些情況下,可能父組件可能需要處理事件,而禁止子組件響應。RN 提供了一個劫持機制,也就是在觸摸事件往下傳遞的時候,先詢問父組件是否需要劫持,不給子組件傳遞事件,也就是如下兩個回調:
View.props.onStartShouldSetResponderCapture:這個屬性接收一個回調函數,函數原型是 function(evt): bool,在觸摸事件開始(touchDown)的時候,RN 容器組件會回調此函數,詢問組件是否要劫持事件響應者設置,自己接收事件處理,如果返回 true,表示需要劫持;
View.props.onMoveShouldSetResponderCapture:此函數類似,不過是在觸摸移動事件(touchMove)詢問容器組件是否劫持。
可以把這種劫持機制看成是一種下沉機制,與上面的冒泡機制對應,我們可以總結 RN 事件處理流程如下圖
注,圖中的 * 表示可以爲 Start 或者 Move,例如 on*ShouldSetResponderCapture 表示 onStartShouldSetResponderCapture 或者 onMoveShouldSetResponderCapture,其他的類似。
觸摸事件開始,首先調用 A 組件的 onStartShouldSetResponderCapture,若此回調返回 false,則按照圖傳遞到 B 組件,然後調用 B 組件 onStartShouldSetResponderCapture,若返回 true,則事件不再傳遞給 C 組件,直接調用本組件的 onResponderStart,則 B 組件就成爲事件響應者,後續事件直接傳遞給它。其他的分析類似。
注意到,圖中還有 onTouchStart/onTouchStop 回調,這個回調並不受響應者的影響,在範圍內的組件都會回調此函數,而且調用順序是從最深層組件到最上層組件。
手勢識別
前面只是介紹了簡單的觸摸事件處理機制及其使用方法,其實連續的觸摸事件,可以組成一些更高級手勢,例如我們最常見的滑動屏幕內容,雙指縮放(Pinch)或者旋轉圖片都是通過手勢識別完成的。
因爲有些手勢是很常用的,RN 也提供了內置的手勢識別庫 PanResponder,它封裝了上面的事件回調函數,對觸摸事件數據進行加工,完成滑動手勢識別,向我們提供更加高級有意義的接口,如下:(如果理解了上面的手勢相應原理,可以直接用內置的PanResponder來處理了)
- onMoveShouldSetPanResponder: (e, gestureState) => bool
- onMoveShouldSetPanResponderCapture: (e, gestureState) => bool
- onStartShouldSetPanResponder: (e, gestureState) => bool
- onStartShouldSetPanResponderCapture: (e, gestureState) => bool
- 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) => bool
可以看到,這些接口與前面接收的基礎回調基本上是一一對應的,其功能也是類似,這裏就不再贅述。這裏有一個特別的回調 onShouldBlockNativeResponder 表示是否用 Native 平臺的事件處理,默認是禁用的,全部使用 JS 中的事件處理,注意此函數目前只能在 Android 平臺上使用。不過這裏回調函數都有一個新的參數 gestureState,這是與滑動相關的數據,是對基本觸摸數據的分析處理,它的內容如下:
stateID:滑動手勢的 ID,在一次完整的交互中此 ID 保持不變;
moveX 和 moveY:自上次回調,手勢移動距離;
x0 和 y0:滑動手勢識別開始的時候的在屏幕中的座標;
dx 和 dy:從手勢開始時,到當前回調是移動距離;
vx 和 vy:當前手勢移動的速度;
numberActiveTouches:當期觸摸手指數量。
那麼對於開篇提到的手勢衝突問題我們可以這麼解決:
import React, {Component} from 'react';
import {View, PanResponder} from 'react-native';
export default class TouchableView extends Component{
constructor(props){
super(props);
this.pressStatus = false;
}
componentWillMount() {
this._panResponder = PanResponder.create({
onStartShouldSetPanResponder: (evt, gestureState) => true,
onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
onMoveShouldSetPanResponder: (evt, gestureState) => true,
onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
onPanResponderGrant: (evt, gestureState) => {
this.pressStatus = true;
},
onPanResponderMove: (evt, gestureState) => {
if (gestureState.dx > 5 || gestureState.dx < -5){
this.pressStatus = false;
}
},
onPanResponderTerminationRequest: (evt, gestureState) => true,
onPanResponderRelease: (evt, gestureState) => {
if (this.pressStatus) {
this.props.onPress && this.props.onPress();
}
this.pressStatus = false;
},
onPanResponderTerminate: (evt, gestureState) => {
// 另一個組件已經成爲了新的響應者,所以當前手勢將被取消。
},
onShouldBlockNativeResponder: (evt, gestureState) => {
// 返回一個布爾值,決定當前組件是否應該阻止原生組件成爲JS響應者
// 默認返回true。目前暫時只支持android。
//基於業務交互場景,如果這裏使用js事件處理,會導致容器不能左右滑動。所以設置成false.
return false;
},
});
}
render(){
const {children} = this.props;
return (
<View {...this._panResponder.panHandlers}>//把創建好的pandGesture賦值給View的屬性
</View>
);
}
}
小知識
通過上面的介紹,可以看到 RN 中提供了類似 Native 平臺的事件處理機制,所以也可以實現各種的觸摸事件處理,甚至也可以實現複雜的手勢識別。
在嵌套組件的事件處理中,RN 中提供了“冒泡”和“下沉”兩個方向的事件處理,這有點類似於 Android Native 上不久前才支持的 NestedScrolling,這就提供更加強大的事件處理機制。
另外需要注意,因爲 RN 的異步通信和執行機制,前面描述的所有回調函數都是在 JS 線程中,並不是 Native 的 UI 線程,而 Native 平臺的 Touch 事件都是在 UI 線程中。所以在 JS 中通過 Touch 或者手勢實現動畫,可能會延遲的問題。
作者:nuannuan_nuan
鏈接:https://www.jianshu.com/p/ad7cee7f9011
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯繫作者獲得授權並註明出處。