觸控是移動設(shè)備的核心功能山害,也移動應(yīng)用交互的基礎(chǔ),Android 和 iOS 各自都有完善的觸摸事件處理機(jī)制长已。React Native(以下簡稱 RN)提供了一套統(tǒng)一的處理方式梨与,能夠方便的處理界面中組件的觸摸事件秉继、用戶手勢等。本文嘗試介紹 RN 中觸摸事件處理盗棵。
1. RN 基本觸摸組件
RN 的組件除了 Text壮韭,其他組件默認(rèn)是不支持點擊事件,也不能響應(yīng)基本觸摸事件纹因,所以 RN 中提供了幾個直接處理響應(yīng)事件的組件喷屋,基本上能夠滿大部分的點擊處理需求TouchableHighlight
, TouchableNativeFeedback
, TouchableOpacity
和 TouchableWithoutFeedback
。因為這幾個組件的功能和使用方法基本類似瞭恰,只是 Touch 的反饋效果不一樣屯曹,所以一般我們用 Touchable**
代替。Touchable**
有如下幾個回調(diào)方法:
-
onPressIn
:點擊開始惊畏; -
onPressOut
:點擊結(jié)束或者離開恶耽; -
onPress
:單擊事件回調(diào); -
onLongPress
:長按事件回調(diào)颜启。
它們的基本使用方法如下偷俭,這里以 TouchableHighlight 為例:
<TouchableHighlight
onPressIn={() => console.log("onPressIn")}
onPressOut={() => console.log("onPressOut")}
onPress={() => console.log("onPress")}
onLongPress={() => console.log("onLongPress")}
>
<Image
style={styles.button}
source={require('./img/rn_logo.png')} />
</TouchableHighlight>
RN 中提供的觸摸組件使用非常簡單,可以參考 官方文檔缰盏,這里也不做詳細(xì)的介紹了涌萤。下面主要介紹用戶觸摸事件處理。
2. 單組件觸摸事件處理
我們知道乳规,RN 的組件默認(rèn)不進(jìn)行處理觸摸事件形葬。組件要處理觸摸事件,首先要“申請”成為摸事件的響應(yīng)者(Responder)暮的,完成事件處理以后笙以,會釋放響應(yīng)者的角色。一個觸摸事件處理周期冻辩,是從用戶手指按下屏幕猖腕,到用戶抬起手指抬起結(jié)束,這是用戶的一次完整觸摸操作恨闪。
單個組件的單次操作交互處理的生命周期如下:
我們來詳細(xì)分析一下事件處理的生命周期倘感,在整個事件處理的過程中,組件有可能處于兩種身份中的一種咙咽,并且可以相互切換:非事件響應(yīng)者和事件響應(yīng)者老玛。
非事件響應(yīng)者
默認(rèn)情況下,觸摸事件輸入不會直接傳遞給組件,不能進(jìn)行事件響應(yīng)處理蜡豹,也就是非事件響應(yīng)者麸粮。如果組件要進(jìn)行觸摸事件處理,首先要申請成為事件響應(yīng)者镜廉,組件有如下兩個屬性可以做這樣的申請:
-
View.props.onStartShouldSetResponder
弄诲,這個屬性接收一個回調(diào)函數(shù),函數(shù)原型是function(evt): bool
娇唯,在觸摸事件開始(touchDown)的時候齐遵,RN 會回調(diào)此函數(shù),詢問組件是否需要成為事件響應(yīng)者塔插,接收事件處理梗摇,如果返回true
,表示需要成為響應(yīng)者想许; -
View.props.onMoveShouldSetResponder
留美,它和前一個屬性類似,不過這是觸摸是進(jìn)行過程中(touchMove)伸刃,RN 詢問組件是否要成為響應(yīng)者谎砾,返回true
表示是。
假如組件通過上面的方法返回了 true
捧颅,表示發(fā)出了申請要成為事件響應(yīng)者請求景图,想要接收后續(xù)的事件輸入。因為同一時刻碉哑,只能有一個事件處理響應(yīng)者挚币,RN 還需要協(xié)調(diào)所有組件的事件處理請求,所以不是每個組件申請都能成功扣典,RN 通過如下兩個回調(diào)來通知告訴組件它的申請結(jié)果妆毕,:
-
View.props.onResponderGrant: (evt) => {}
:表示申請成功,組件成為了事件處理響應(yīng)者贮尖,這時組件就開始接收后序的觸摸事件輸入笛粘。一般情況下,這時開始湿硝,組件進(jìn)入了激活狀態(tài)薪前,并進(jìn)行一些事件處理或者手勢識別的初始化。 -
View.props.onResponderReject: (evt) => {}
:表示申請失敗了关斜,這意味者其他組件正在進(jìn)行事件處理示括,并且它不想放棄事件處理,所以你的申請被拒絕了痢畜,后續(xù)輸入事件不會傳遞給本組件進(jìn)行處理垛膝。
事件響應(yīng)者
如果通過上面的步驟鳍侣,組件申請成為了事件響應(yīng)者,后續(xù)的事件輸入都會通過回調(diào)函數(shù)通知到組件吼拥,如下:
-
View.props.onResponderStart: (evt) => {}
:表示手指按下時拱她,成功申請為事件響應(yīng)者的回調(diào); -
View.props.onResponderMove: (evt) => {}
:表示觸摸手指移動的事件扔罪,這個回調(diào)可能非常頻繁,所以這個回調(diào)函數(shù)的內(nèi)容需要盡量簡單桶雀; -
View.props.onResponderRelease: (evt) => {}
:表示觸摸完成(touchUp)的時候的回調(diào)矿酵,表示用戶完成了本次的觸摸交互,這里應(yīng)該完成手勢識別的處理矗积,這以后全肮,組件不再是事件響應(yīng)者,組件取消激活棘捣。 -
View.props.onResponderEnd: (evt) => {}
:表示組件結(jié)束事件響應(yīng)的回調(diào)辜腺。
從前面的圖中也看到,在組件成為事件響應(yīng)者期間乍恐,其他組件也可能會申請觸摸事件處理评疗。此時 RN 會通過回調(diào)詢問你是否可以釋放響應(yīng)者角色讓給其他組件∫鹆遥回調(diào)如下:
View.props.onResponderTerminationRequest: (evt) => bool
如果回調(diào)函數(shù)返回為 true
百匆,則表示同意釋放響應(yīng)者角色,同時會回調(diào)如下函數(shù)呜投,通知組件事件響應(yīng)處理被終止了:
View.props.onResponderTerminate: (evt) => {}
這個回調(diào)也會發(fā)生在系統(tǒng)直接終止組件的事件處理加匈,例如用戶在觸摸操作過程中,突然來電話的情況仑荐。
事件數(shù)據(jù)結(jié)構(gòu)
從前面我們看到雕拼,觸摸事件處理的回調(diào)都有一個 evt
參數(shù),包含一個觸摸事件數(shù)據(jù) nativeEvent
粘招。nativeEvent
的詳細(xì)內(nèi)容如下:
-
identifier
:觸摸的 ID啥寇,一般對應(yīng)手指,在多點觸控的時候洒扎,用來區(qū)分是哪個手指的觸摸事件示姿; -
locationX
和locationY
:觸摸點相對組件的位置; -
pageX
和pageY
:觸摸點相對于屏幕的位置逊笆; -
timestamp
:當(dāng)前觸摸的事件的時間戳栈戳,可以用來進(jìn)行滑動計算; -
target
:接收當(dāng)前觸摸事件的組件 ID难裆; -
changedTouches
:evt 數(shù)組子檀,從上次回調(diào)上報的觸摸事件镊掖,到這次上報之間的所有事件數(shù)組。因為用戶觸摸過程中褂痰,會產(chǎn)生大量事件亩进,有時候可能沒有及時上報,系統(tǒng)用這種方式批量上報缩歪; -
touches
:evt 數(shù)組归薛,多點觸摸的時候,包含當(dāng)前所有觸摸點的事件匪蝙。
這些數(shù)據(jù)中主籍,最常用的是 locationX
和 locationY
數(shù)據(jù),需要注意的是逛球,因為這里是 Native 的數(shù)據(jù)千元,所以他們的單位是實際像素。如果要轉(zhuǎn)換為 RN 中的邏輯單位颤绕,可以示使用如下方法:
var pX = evt.nativeEvent.locationX / PixelRatio.get();
3. 嵌套組件事件處理
上一小節(jié)介紹的都是針對單個組件來說幸海,事件處理的流程和機(jī)制。但是前面也提到了奥务,當(dāng)組件需要作為事件處理響應(yīng)者時物独,需要通過 onStartShouldSetResponder
或者 onMoveShouldSetResponder
回調(diào)返回值為 true
來申請。假如當(dāng)多個組件嵌套的時候氯葬,這兩個回調(diào)都返回了 true
的時候议纯,但是同一個只能有一個事件處理響應(yīng)者,這種情況怎么處理呢溢谤?為了便于描述瞻凤,假設(shè)我們的組件布局如下:
在 RN 中,默認(rèn)情況下使用冒泡機(jī)制世杀,響應(yīng)最深的組件最先開始響應(yīng)阀参,所以前面描述的這種情況,如圖中瞻坝,如果 A蛛壳、B、C 三個組件的 on*ShouldSetResponder
都返回為 true
所刀,那么只有 C 組件會得到響應(yīng)成為響應(yīng)者衙荐。這種機(jī)制才能保證了界面所有的組件才能得到響應(yīng)。但是有些情況下浮创,可能父組件可能需要處理事件忧吟,而禁止子組件響應(yīng)。RN 提供了一個劫持機(jī)制斩披,也就是在觸摸事件往下傳遞的時候溜族,先詢問父組件是否需要劫持讹俊,不給子組件傳遞事件,也就是如下兩個回調(diào):
-
View.props.onStartShouldSetResponderCapture
:這個屬性接收一個回調(diào)函數(shù)煌抒,函數(shù)原型是function(evt): bool
仍劈,在觸摸事件開始(touchDown)的時候,RN 容器組件會回調(diào)此函數(shù)寡壮,詢問組件是否要劫持事件響應(yīng)者設(shè)置贩疙,自己接收事件處理,如果返回true
况既,表示需要劫持这溅; -
View.props.onMoveShouldSetResponderCapture
:此函數(shù)類似,不過是在觸摸移動事件(touchMove)詢問容器組件是否劫持坏挠。
可以把這種劫持機(jī)制看成是一種下沉機(jī)制,與上面的冒泡機(jī)制對應(yīng)邪乍,我們可以總結(jié) RN 事件處理流程如下圖:
注降狠,圖中的 ***** 表示可以為 Start 或者 Move,例如 onShouldSetResponderCapture* 表示 onStartShouldSetResponderCapture
或者 onMoveShouldSetResponderCapture
庇楞,其他的類似榜配。
觸摸事件開始,首先調(diào)用 A 組件的 onStartShouldSetResponderCapture
吕晌,若此回調(diào)返回 false
蛋褥,則按照圖傳遞到 B 組件,然后調(diào)用 B 組件 onStartShouldSetResponderCapture
睛驳,若返回 true
烙心,則事件不再傳遞給 C 組件,直接調(diào)用本組件的 onResponderStart
乏沸,則 B 組件就成為事件響應(yīng)者淫茵,后續(xù)事件直接傳遞給它。其他的分析類似蹬跃。
注意到匙瘪,圖中還有 onTouchStart/onTouchStop
回調(diào),這個回調(diào)并不受響應(yīng)者的影響蝶缀,在范圍內(nèi)的組件都會回調(diào)此函數(shù)丹喻,而且調(diào)用順序是從最深層組件到最上層組件。
4. 手勢識別
前面只是介紹了簡單的觸摸事件處理機(jī)制及其使用方法翁都,其實連續(xù)的觸摸事件碍论,可以組成一些更高級手勢,例如我們最常見的滑動屏幕內(nèi)容柄慰,雙指縮放(Pinch)或者旋轉(zhuǎn)圖片都是通過手勢識別完成的骑冗。
因為有些手勢是很常用的赊瞬,RN 也提供了內(nèi)置的手勢識別庫 PanResponder
,它封裝了上面的事件回調(diào)函數(shù)贼涩,對觸摸事件數(shù)據(jù)進(jìn)行加工巧涧,完成滑動手勢識別,向我們提供更加高級有意義的接口遥倦,如下:
- 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
可以看到谤绳,這些接口與前面接收的基礎(chǔ)回調(diào)基本上是一一對應(yīng)的,其功能也是類似袒哥,這里就不再贅述缩筛。這里有一個特別的回調(diào) onShouldBlockNativeResponder
表示是否用 Native 平臺的事件處理,默認(rèn)是禁用的堡称,全部使用 JS 中的事件處理瞎抛,注意此函數(shù)目前只能在 Android 平臺上使用。不過這里回調(diào)函數(shù)都有一個新的參數(shù) gestureState
却紧,這是與滑動相關(guān)的數(shù)據(jù)桐臊,是對基本觸摸數(shù)據(jù)的分析處理,它的內(nèi)容如下:
-
stateID
:滑動手勢的 ID晓殊,在一次完整的交互中此 ID 保持不變断凶; -
moveX
和moveY
:自上次回調(diào),手勢移動距離巫俺; -
x0
和y0
:滑動手勢識別開始的時候的在屏幕中的坐標(biāo)认烁; -
dx
和dy
:從手勢開始時,到當(dāng)前回調(diào)是移動距離介汹; -
vx
和vy
:當(dāng)前手勢移動的速度却嗡; -
numberActiveTouches
:當(dāng)期觸摸手指數(shù)量假残。
下面介紹一個簡單的實例柠新,本例實現(xiàn)可以使用手指拖動界面的圓形控件,使用實例如下:
import React from 'react';
import {
AppRegistry,
PanResponder,
StyleSheet,
View,
processColor,
} from 'react-native';
var CIRCLE_SIZE = 80;
var CIRCLE_COLOR = 'blue';
var CIRCLE_HIGHLIGHT_COLOR = 'green';
var PanResponderExample = React.createClass({
statics: {
title: 'PanResponder Sample',
description: 'Shows the use of PanResponder to provide basic gesture handling.',
},
_panResponder: {},
_previousLeft: 0,
_previousTop: 0,
_circleStyles: {},
circle: (null : ?{ setNativeProps(props: Object): void }),
componentWillMount: function() {
this._panResponder = PanResponder.create({
onStartShouldSetPanResponder: (evt, gestureState) => true,
onMoveShouldSetPanResponder: (evt, gestureState) => true,
onPanResponderGrant: this._handlePanResponderGrant,
onPanResponderMove: this._handlePanResponderMove,
onPanResponderRelease: this._handlePanResponderEnd,
onPanResponderTerminate: this._handlePanResponderEnd,
});
this._previousLeft = 20;
this._previousTop = 84;
this._circleStyles = {
style: {
left: this._previousLeft,
top: this._previousTop
}
};
},
componentDidMount: function() {
this._updatePosition();
},
render: function() {
return (
<View style={styles.container}>
<View
ref={(circle) => {
this.circle = circle;
}}
style={styles.circle}
{...this._panResponder.panHandlers}
/>
</View>
);
},
_highlight: function() {
const circle = this.circle;
circle && circle.setNativeProps({
style: {
backgroundColor: processColor(CIRCLE_HIGHLIGHT_COLOR)
}
});
},
_unHighlight: function() {
const circle = this.circle;
circle && circle.setNativeProps({
style: {
backgroundColor: processColor(CIRCLE_COLOR)
}
});
},
_updatePosition: function() {
this.circle && this.circle.setNativeProps(this._circleStyles);
},
_handlePanResponderGrant: function(e: Object, gestureState: Object) {
this._highlight();
},
_handlePanResponderMove: function(e: Object, gestureState: Object) {
this._circleStyles.style.left = this._previousLeft + gestureState.dx;
this._circleStyles.style.top = this._previousTop + gestureState.dy;
this._updatePosition();
},
_handlePanResponderEnd: function(e: Object, gestureState: Object) {
this._unHighlight();
this._previousLeft += gestureState.dx;
this._previousTop += gestureState.dy;
},
});
var styles = StyleSheet.create({
circle: {
width: CIRCLE_SIZE,
height: CIRCLE_SIZE,
borderRadius: CIRCLE_SIZE / 2,
backgroundColor: CIRCLE_COLOR,
position: 'absolute',
left: 0,
top: 0,
},
container: {
flex: 1,
paddingTop: 64,
},
});
可見米丘,在 componentWillMount
中創(chuàng)建一個 PanResponder
實例赶撰,并設(shè)置想好相關(guān)的屬性舌镶,然后把這個對象設(shè)置給 View 的屬性,如下:
<View
{...this._panResponder.panHandlers}
/>
其余的代碼也比較簡單豪娜,這里就不詳述了餐胀。
5. 總結(jié)
通過上面的介紹,可以看到 RN 中提供了類似 Native 平臺的事件處理機(jī)制瘤载,所以也可以實現(xiàn)各種的觸摸事件處理否灾,甚至也可以實現(xiàn)復(fù)雜的手勢識別。
在嵌套組件的事件處理中鸣奔,RN 中提供了“冒泡”和“下沉”兩個方向的事件處理墨技,這有點類似于 Android Native 上不久前才支持的 NestedScrolling惩阶,這就提供更加強(qiáng)大的事件處理機(jī)制。
另外需要注意扣汪,因為 RN 的異步通信和執(zhí)行機(jī)制断楷,前面描述的所有回調(diào)函數(shù)都是在 JS 線程中,并不是 Native 的 UI 線程崭别,而 Native 平臺的 Touch 事件都是在 UI 線程中冬筒。所以在 JS 中通過 Touch 或者手勢實現(xiàn)動畫,可能會延遲的問題茅主。