React Native 觸摸事件處理詳解

觸控是移動設(shè)備的核心功能毡代,也移動應(yīng)用交互的基礎(chǔ)阅羹,Android 和 iOS 各自都有完善的觸摸事件處理機制。React Native(以下簡稱 RN)提供了一套統(tǒng)一的處理方式教寂,能夠方便的處理界面中組件的觸摸事件捏鱼、用戶手勢等。本文嘗試介紹 RN 中觸摸事件處理酪耕。

1. RN 基本觸摸組件

RN 的組件除了 Text导梆,其他組件默認是不支持點擊事件,也不能響應(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/rnlogo.png')} />
</TouchableHighlight>  

RN 中提供的觸摸組件使用非常簡單,可以參考 官方文檔 塞关,這里也不做詳細的介紹了抬探。下面主要介紹用戶觸摸事件處理。

2. 單組件觸摸事件處理

我們知道描孟,RN 的組件默認不進行處理觸摸事件驶睦。組件要處理觸摸事件,首先要“申請”成為摸事件的響應(yīng)者(Responder)匿醒,完成事件處理以后,會釋放響應(yīng)者的角色缠导。一個觸摸事件處理周期廉羔,是從用戶手指按下屏幕,到用戶抬起手指抬起結(jié)束僻造,這是用戶的一次完整觸摸操作憋他。

單個組件的單次操作交互處理的生命周期如下:

事件的生命周期.jpg

React Native 觸摸事件處理詳解

我們來詳細分析一下事件處理的生命周期孩饼,在整個事件處理的過程中镀娶,組件有可能處于兩種身份中的一種梯码,并且可以相互切換: 非事件響應(yīng)者 和 事件響應(yīng)者 好啰。

非事件響應(yīng)者

默認情況下框往,觸摸事件輸入不會直接傳遞給組件椰弊,不能進行事件響應(yīng)處理,也就是非事件響應(yīng)者贤重。如果組件要進行觸摸事件處理游桩,首先要申請成為事件響應(yīng)者借卧,組件有如下兩個屬性可以做這樣的申請:

onStartShouldSetResponder 铐刘,這個屬性接收一個回調(diào)函數(shù)影晓,函數(shù)原型是 function(evt, gestureState): bool 挂签,在觸摸事件開始(touchDown)的時候,RN 會回調(diào)此函數(shù)勺馆,詢問組件是否需要成為事件響應(yīng)者草穆,接收事件處理悲柱,如果返回 true ,表示需要成為響應(yīng)者嘿般;
onMoveShouldSetResponder 博个,它和前一個屬性類似盆佣,不過這是觸摸是進行過程中(touchMove)共耍,RN 詢問組件是否要成為響應(yīng)者吨瞎,返回 true 表示是颤诀。
假如組件通過上面的方法返回了 true ,表示發(fā)出了申請要成為事件響應(yīng)者請求遗淳,想要接收后續(xù)的事件輸入屈暗。因為同一時刻养叛,只能有一個事件處理響應(yīng)者弃甥,RN 還需要協(xié)調(diào)所有組件的事件處理請求汁讼,所以不是每個組件申請都能成功掉缺,RN 通過如下兩個回調(diào)來通知告訴組件它的申請結(jié)果,:

onResponderGrant: (evt, gestureState) => {} :表示申請成功艰毒,組件成為了事件處理響應(yīng)者丑瞧,這時組件就開始接收后序的觸摸事件輸入蜀肘。一般情況下扮宠,這時開始,組件進入了激活狀態(tài)获雕,并進行一些事件處理或者手勢識別的初始化届案。
onResponderReject: (evt, gestureState) => {} :表示申請失敗了楣颠,這意味者其他組件正在進行事件處理童漩,并且它不想放棄事件處理仓蛆,所以你的申請被拒絕了看疙,后續(xù)輸入事件不會傳遞給本組件進行處理。

事件響應(yīng)者

如果通過上面的步驟施禾,組件申請成為了事件響應(yīng)者弥搞,后續(xù)的事件輸入都會通過回調(diào)函數(shù)通知到組件,如下:

onResponderStart: (evt, gestureState) => {} :表示手指按下時船逮,成功申請為事件響應(yīng)者的回調(diào)挖胃;
onResponderMove: (evt, gestureState) => {} :表示觸摸手指移動的事件酱鸭,這個回調(diào)可能非常頻繁凹髓,所以這個回調(diào)函數(shù)的內(nèi)容需要盡量簡單怯屉;
onResponderRelease: (evt, gestureState) => {} :表示觸摸完成(touchUp)的時候的回調(diào)蚀之,表示用戶完成了本次的觸摸交互足删,這里應(yīng)該完成手勢識別的處理,這以后讶泰,組件不再是事件響應(yīng)者痪署,組件取消激活狼犯。
onResponderEnd: (evt, gestureState) => {} :表示組件結(jié)束事件響應(yīng)的回調(diào)领铐。
從前面的圖中也看到绪撵,在組件成為事件響應(yīng)者期間,其他組件也可能會申請觸摸事件處理幻碱。此時 RN 會通過回調(diào)詢問你是否可以釋放響應(yīng)者角色讓給其他組件褥傍。回調(diào)如下:

onResponderTerminationRequest: (evt, gestureState) => bool
如果回調(diào)函數(shù)返回為 true 社付,則表示同意釋放響應(yīng)者角色,同時會回調(diào)如下函數(shù)兄世,通知組件事件響應(yīng)處理被終止了:

onResponderTerminate: (evt, gestureState) => {}
這個回調(diào)也會發(fā)生在系統(tǒng)直接終止組件的事件處理啊研,例如用戶在觸摸操作過程中党远,突然來電話的情況沟娱。

事件數(shù)據(jù)結(jié)構(gòu)

從前面我們看到,觸摸事件處理的回調(diào)都有一個 evt, gestureState 參數(shù)矫废,包含一個觸摸事件數(shù)據(jù) nativeEvent 蓖扑。 nativeEvent 的詳細內(nèi)容如下:
identifier :觸摸的 ID律杠,一般對應(yīng)手指柜去,在多點觸控的時候碰声,用來區(qū)分是哪個手指的觸摸事件胰挑;
locationX 和 locationY :觸摸點相對組件的位置椿肩;
pageX 和 pageY:觸摸點相對于屏幕的位置郑象;
timestamp:當(dāng)前觸摸的事件的時間戳厂榛,可以用來進行滑動計算击奶;
target:接收當(dāng)前觸摸事件的組件 ID柜砾;
changedTouches :evt, gestureState 數(shù)組换衬,從上次回調(diào)上報的觸摸事件瞳浦,到這次上報之間的所有事件數(shù)組叫潦。因為用戶觸摸過程中,會產(chǎn)生大量事件四敞,有時候可能沒有及時上報忿危,系統(tǒng)用這種方式批量上報铺厨;
touches :evt, gestureState 數(shù)組硬纤,多點觸摸的時候筝家,包含當(dāng)前所有觸摸點的事件。
這些數(shù)據(jù)中腮鞍,最常用的是 locationX 和 locationY 數(shù)據(jù),需要注意的是吱瘩,因為這里是 Native 的數(shù)據(jù)使碾,所以他們的單位是實際像素票摇。如果要轉(zhuǎn)換為 RN 中的邏輯單位兄朋,可以示使用如下方法:
const pX = evt, gestureState.nativeEvent.locationX / PixelRatio.get();

3. 嵌套組件事件處理

上一小節(jié)介紹的都是針對單個組件來說怜械,事件處理的流程和機制缕允。但是前面也提到了障本,當(dāng)組件需要作為事件處理響應(yīng)者時驾霜,需要通過 onStartShouldSetResponder 或者 onMoveShouldSetResponder 回調(diào)返回值為 true 來申請粪糙。假如當(dāng)多個組件嵌套的時候忿项,這兩個回調(diào)都返回了 true 的時候轩触,但是同一個只能有一個事件處理響應(yīng)者,這種情況怎么處理呢伐弹?為了便于描述惨好,假設(shè)我們的組件布局如下:

ABC組件.jpg

React Native 觸摸事件處理詳解

在 RN 中昧狮,默認情況下使用冒泡機制,響應(yīng)最深的組件最先開始響應(yīng)合住,所以前面描述的這種情況透葛,如圖中僚害,如果 A繁调、B蹄胰、C 三個組件的 on*ShouldSetResponder 都返回為 true ,那么只有 C 組件會得到響應(yīng)成為響應(yīng)者浩蓉。這種機制才能保證了界面所有的組件才能得到響應(yīng)捻艳。但是有些情況下认轨,可能父組件可能需要處理事件阅悍,而禁止子組件響應(yīng)节视。RN 提供了一個劫持機制寻行,也就是在觸摸事件往下傳遞的時候,先詢問父組件是否需要劫持杆烁,不給子組件傳遞事件兔魂,也就是如下兩個回調(diào):

onStartShouldSetResponderCapture :這個屬性接收一個回調(diào)函數(shù)析校,函數(shù)原型是 (evt, gestureState): bool ,在觸摸事件開始(touchDown)的時候遂唧,RN 容器組件會回調(diào)此函數(shù)盖彭,詢問組件是否要劫持事件響應(yīng)者設(shè)置召边,自己接收事件處理掌实,如果返回 true 邦马,表示需要劫持滋将;
onMoveShouldSetResponderCapture :此函數(shù)類似随闽,不過是在觸摸移動事件(touchMove)詢問容器組件是否劫持掘宪。
可以把這種劫持機制看成是一種下沉機制魏滚,與上面的冒泡機制對應(yīng)坟漱,我們可以總結(jié) RN 事件處理流程如下圖:

RN事件流程.jpg

React Native 觸摸事件處理詳解

注,圖中的 * 表示可以為 Start 或者 Move 成翩,例如 on*ShouldSetResponderCapture 表示 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. 手勢識別

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

因為有些手勢是很常用的雁乡,RN 也提供了內(nèi)置的手勢識別庫 PanResponder ,它封裝了上面的事件回調(diào)函數(shù)曲饱,對觸摸事件數(shù)據(jù)進行加工渔工,完成滑動手勢識別,向我們提供更加高級有意義的接口梁丘,如下:

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 平臺的事件處理羊娃,默認是禁用的蕊玷,全部使用 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';
const CIRCLESIZE = 80;  
const CIRCLECOLOR = 'blue';  
const CIRCLEHIGHLIGHTCOLOR = 'green';
const 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, gestureState) => true,
      onMoveShouldSetPanResponder: (evt, gestureState, 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(CIRCLEHIGHLIGHTCOLOR)
      }
    });
  },
  unHighlight: function() {
    const circle = this.circle;
    circle && circle.setNativeProps({
      style: {
        backgroundColor: processColor(CIRCLECOLOR)
      }
    });
  },
  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;
  },
});
const styles = StyleSheet.create({  
  circle: {
    width: CIRCLESIZE,
    height: CIRCLESIZE,
    borderRadius: CIRCLESIZE / 2,
    backgroundColor: CIRCLECOLOR,
    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 平臺的事件處理機制靠胜,所以也可以實現(xiàn)各種的觸摸事件處理浪漠,甚至也可以實現(xiàn)復(fù)雜的手勢識別。

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

另外需要注意嬉探,因為 RN 的異步通信和執(zhí)行機制,前面描述的所有回調(diào)函數(shù)都是在 JS 線程中眷蜓,并不是 Native 的 UI 線程吁系,而 Native 平臺的 Touch 事件都是在 UI 線程中汽纤。所以在 JS 中通過 Touch 或者手勢實現(xiàn)動畫蕴坪,可能會延遲的問題辞嗡。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市谒养,隨后出現(xiàn)的幾起案子买窟,更是在濱河造成了極大的恐慌始绍,老刑警劉巖学赛,帶你破解...
    沈念sama閱讀 211,123評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異盏浇,居然都是意外死亡变丧,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評論 2 384
  • 文/潘曉璐 我一進店門绢掰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來痒蓬,“玉大人,你說我怎么就攤上這事滴劲」ド梗” “怎么了?”我有些...
    開封第一講書人閱讀 156,723評論 0 345
  • 文/不壞的土叔 我叫張陵哑芹,是天一觀的道長炎辨。 經(jīng)常有香客問我,道長聪姿,這世上最難降的妖魔是什么末购? 我笑而不...
    開封第一講書人閱讀 56,357評論 1 283
  • 正文 為了忘掉前任擎场,我火速辦了婚禮宅静,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己逃糟,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,412評論 5 384
  • 文/花漫 我一把揭開白布矛辕。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上厘贼,一...
    開封第一講書人閱讀 49,760評論 1 289
  • 那天,我揣著相機與錄音串述,去河邊找鬼。 笑死耕姊,一個胖子當(dāng)著我的面吹牛欣簇,可吹牛的內(nèi)容都是我干的莫鸭。 我是一名探鬼主播,決...
    沈念sama閱讀 38,904評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼粥鞋,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了窝稿?” 一聲冷哼從身側(cè)響起踪少,我...
    開封第一講書人閱讀 37,672評論 0 266
  • 序言:老撾萬榮一對情侶失蹤集漾,失蹤者是張志新(化名)和其女友劉穎凌埂,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,118評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡鹰霍,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,456評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,599評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡次氨,死狀恐怖犀呼,靈堂內(nèi)的尸體忽然破棺而出挑童,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 34,264評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響岗屏,放射性物質(zhì)發(fā)生泄漏隅很。R本人自食惡果不足惜绒尊,卻給世界環(huán)境...
    茶點故事閱讀 39,857評論 3 312
  • 文/蒙蒙 一瘟裸、第九天 我趴在偏房一處隱蔽的房頂上張望佛呻。 院中可真熱鬧泛范,春花似錦惭缰、人聲如沸昂羡。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春巫玻,著一層夾襖步出監(jiān)牢的瞬間凰浮,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評論 1 264
  • 我被黑心中介騙來泰國打工贰逾, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 46,286評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親染服。 傳聞我的和親對象是個殘疾皇子秉颗,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,465評論 2 348

推薦閱讀更多精彩內(nèi)容