目錄
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