React Native 觸摸事件處理詳解

觸控是移動設備的核心功能揣非,也移動應用交互的基礎累奈,Android 和 iOS 各自都有完善的觸摸事件處理機制。React Native(以下簡稱 RN)提供了一套統(tǒng)一的處理方式,能夠方便的處理界面中組件的觸摸事件唱较、用戶手勢等哥纫。本文嘗試介紹 RN 中觸摸事件處理霉旗。

1. RN 基本觸摸組件

RN 的組件除了 Text,其他組件默認是不支持點擊事件蛀骇,也不能響應基本觸摸事件厌秒,所以 RN 中提供了幾個直接處理響應事件的組件,基本上能夠滿大部分的點擊處理需求 TouchableHighlight , TouchableNativeFeedback , TouchableOpacity 和 TouchableWithoutFeedback 擅憔。因為這幾個組件的功能和使用方法基本類似鸵闪,只是 Touch 的反饋效果不一樣,所以一般我們用 Touchable** 代替暑诸。 Touchable** 有如下幾個回調方法:

  • onPressIn :點擊開始岛马;
  • onPressOut :點擊結束或者離開棉姐;
  • onPress :單擊事件回調;
  • onLongPress :長按事件回調啦逆。

她們的基本使用方法如下伞矩,這里以 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 中提供的觸摸組件使用非常簡單,可以參考 官方文檔 夏志,這里也不做詳細的介紹了乃坤。下面主要介紹用戶觸摸事件處理。

2. 組件觸摸事件處理

我們知道沟蔑,RN 的組件默認不進行處理觸摸事件湿诊。組件要處理觸摸事件,首先要“申請”成為摸事件的響應者(Responder)瘦材,完成事件處理以后厅须,會釋放響應者的角色。一個觸摸事件處理周期食棕,是從用戶手指按下屏幕朗和,到用戶抬起手指抬起結束,這是用戶的一次完整觸摸操作簿晓。

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

我們來詳細分析一下事件處理的生命周期眶拉,在整個事件處理的過程中,組件有可能處于兩種身份中的一種憔儿,并且可以相互切換: 非事件響應者 和 事件響應者 忆植。

非事件響應者

默認情況下,觸摸事件輸入不會直接傳遞給組件谒臼,不能進行事件響應處理朝刊,也就是非事件響應者。如果組件要進行觸摸事件處理蜈缤,首先要申請成為事件響應者拾氓,有組件有如下兩個屬性可以做這樣的申請:

  • View.props.onStartShouldSetResponder ,這個屬性接收一個回調函數(shù)劫樟,函數(shù)原型是 function(evt): bool 痪枫,在觸摸事件開始(touchDown)的時候,RN 會回調此函數(shù)叠艳,詢問組件是否需要成為事件響應者奶陈,接收事件處理,如果返回 true 附较,表示需要成為響應者吃粒;
  • View.props.onMoveShouldSetResponder ,它和前一個屬性類似拒课,不過這是觸摸是進行過程中(touchMove)徐勃,RN 詢問組件是否要成為響應者事示,返回 true 表示是。

假如組件通過上面的方法返回了 true 僻肖,表示發(fā)出了申請要成為事件響應者肖爵,想要接收后續(xù)的事件輸入。因為同一時刻臀脏,只能有一個事件處理響應者劝堪,RN 還需要協(xié)調所有組件的事件處理請求,所以不是每個組件申請都能成功揉稚,所以 RN 需要告訴組件它的申請結果秒啦,通過如下兩個屬性來通知:

  • View.props.onResponderGrant: (evt) => {} :表示申請成功,組件成為了事件處理響應者搀玖,這時組件就開始接收后序的觸摸事件輸入余境。一般情況下,這時開始灌诅,組件進入了激活狀態(tài)芳来,并進行一些事件處理或者手勢識別的初始化。
  • View.props.onResponderReject: (evt) => {} :表示申請失敗了延塑,這意味者其他組件正在進行事件處理绣张,并且它不想放棄事件處理答渔,所以你的申請被拒絕了关带,后續(xù)輸入事件不會傳遞給本組件進行處理。

事件響應者

如果通過上面的步驟沼撕,組件申請成為了事件響應者宋雏,后續(xù)的事件輸入都會通過回調函數(shù)通知到組件,如下:

  • View.props.onResponderStart: (evt) => {} :表示手指按下時务豺,成功申請為事件響應者的回調磨总;
  • View.props.onResponderMove: (evt) => {}:表示觸摸手指移動的事件,這個回調可能非常頻繁笼沥,所以這個回調函數(shù)的內容需要盡量簡單蚪燕;
  • View.props.onResponderRelease: (evt) => {} :表示觸摸完成(touchUp)的時候的回調,表示用戶完成了本次的觸摸交互奔浅,這里應該完成手勢識別的處理馆纳,這以后,組件不再是事件響應者汹桦,組件取消激活鲁驶。
  • View.props.onResponderEnd: (evt) => {} :表示組件結束事件響應的回調。

從前面的圖中也看到舞骆,在組件成為事件響應者期間钥弯,其他組件也可能會申請觸摸事件處理径荔。此時 RN 會通過回調詢問你是否可以釋放響應者角色讓給其他組件〈圉回調如下:

View.props.onResponderTerminationRequest: (evt) => bool  

如果回調函數(shù)返回為 true 总处,則表示同意釋放響應者角色,同時會回調如下函數(shù)睛蛛,通知組件事件響應處理被終止了:

View.props.onResponderTerminate: (evt) => {}  

這個回調也會發(fā)生在系統(tǒng)直接終止組件的事件處理辨泳,例如用戶在觸摸操作過程中,突然來電話的情況玖院。

事件數(shù)據(jù)結構

從前面我們看到菠红,觸摸事件處理的回調都有一個 evt 參數(shù),包含一個觸摸事件數(shù)據(jù) nativeEvent 难菌。 nativeEvent 的詳細內容如下:

-identifier :觸摸的 ID试溯,一般對應手指,在多點觸控的時候郊酒,用來區(qū)分是哪個手指的觸摸事件遇绞;
-locationXlocationY :觸摸點相對組件的位置;

  • pageXpageY :觸摸點相對于屏幕的位置燎窘;
  • timestamp :當前觸摸的事件的時間戳摹闽,可以用來進行滑動計算;
  • target :接收當前觸摸事件的組件 ID褐健;
  • changedTouches :evt 數(shù)組付鹿,從上次回調上報的觸摸事件,到這次上報之間的所有事件數(shù)組蚜迅。因為用戶觸摸過程中舵匾,會產生大量事件,有時候可能沒有及時上報谁不,系統(tǒng)用這種方式批量上報坐梯;
  • touches :evt 數(shù)組,多點觸摸的時候刹帕,包含當前所有觸摸點的事件吵血。

這些數(shù)據(jù)中,最常用的是 locationX 和 locationY 數(shù)據(jù)偷溺,需要注意的是蹋辅,因為這里是 Native 的數(shù)據(jù),所以他們的單位是實際像素亡蓉。如果要轉換為 RN 中的邏輯單位晕翠,可以示使用如下方法:

var pX = evt.nativeEvent.locationX / PixelRatio.get();  

3. 嵌套組件事件處理

上一節(jié)我們介紹的,都是針對單個組件來說,事件處理的流程和機制淋肾。但是前面也提到了硫麻,當組件需要作為事件處理響應者時,需要通過 onStartShouldSetResponder 或者 onMoveShouldSetResponder 回調返回值為 true 來申請樊卓。假如當多個組件嵌套的時候拿愧,這兩個回調都返回了 true 的時候,但是同一個只能有一個事件處理響應者碌尔,這種情況怎么處理呢浇辜?為了便于描述,假設我們的組件布局如下:

在 RN 中唾戚,默認情況下使用冒泡機制柳洋,響應最深的組件最先開始響應,所以前面描述的這種情況叹坦,如圖中熊镣,如果 A、B募书、C 三個組件的 on*ShouldSetResponder 都返回為 true 绪囱,那么只有 C 組件會得到響應成為響應者。這種機制才能保證了界面所有的組件才能得到響應莹捡。但是有些情況下鬼吵,可能父組件可能需要處理事件,而禁止子組件響應篮赢。RN 提供了一個劫持機制齿椅,也就是在觸摸事件往下傳遞的時候,先詢問父組件是否需要劫持荷逞,不給子組件傳遞事件媒咳,也就是如下兩個回調:

  • View.props.onStartShouldSetResponderCapture :這個屬性接收一個回調函數(shù)粹排,函數(shù)原型是 function(evt): bool 种远,在觸摸事件開始(touchDown)的時候,RN 容器組件會回調此函數(shù)顽耳,詢問組件是否要劫持事件響應者設置坠敷,自己接收事件處理,如果返回 true 射富,表示需要劫持膝迎;
  • View.props.onMoveShouldSetResponderCapture :此函數(shù)類似,不過是在觸摸移動事件(touchMove)詢問容器組件是否劫持胰耗。

可以把這種劫持機制看成是一種下沉機制限次,與上面的冒泡機制對應,我們可以總結 RN 事件處理流程如下圖:

注:圖中的 *表示可以為 Start 或者 Move ,例如 on*ShouldSetResponderCapture 表示 onStartShouldSetResponderCapture 或者 onMoveShouldSetResponderCapture 卖漫,其他的類似费尽。觸摸事件開始,首先調用 A 組件的 onStartShouldSetResponderCapture 羊始,若此回調返回 false 旱幼,則按照圖傳遞到 B 組件,然后調用 B 組件 onStartShouldSetResponderCapture 突委,若返回 true 柏卤,則事件不再傳遞給 C 組件,直接調用本組件的 onResponderStart 匀油,則 B 組件就成為事件響應者缘缚,后續(xù)事件直接傳遞給它。其他的分析類似敌蚜。

注意到忙灼,圖中還有 onTouchStart/onTouchStop 回調,這個回調并不受響應者的影響钝侠,在范圍內的組件都會回調此函數(shù)该园,而且調用順序是從最深層組件到最上層組件。

4. 手勢識別

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

因為有些手勢是非常常用的,RN 也提供了內置的手勢識別庫 PanResponder 叮阅,發(fā)封裝了上面的事件回調函數(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

可以看到,這些接口與前面接收的基礎回調基本上是一一對應的勒叠,其功能也是類似兜挨,這里就不再贅述。這里有一個特別的回調 onShouldBlockNativeResponder 表示是否用 Native 平臺的事件處理眯分,默認是禁用的拌汇,全部使用 JS 中的事件處理,注意此函數(shù)目前只能在 Android 平臺上使用弊决。不過這里回調函數(shù)都有一個新的參數(shù) gestureState 噪舀,這是與滑動相關的數(shù)據(jù),是對基本觸摸數(shù)據(jù)的分析處理,它的內容如下:

  • stateID :滑動手勢的 ID与倡,在一次完整的交互中此 ID 保持不變先改;
  • moveXmoveY :自上次回調,手勢移動距離蒸走;
  • x0y0 :滑動手勢識別開始的時候的在屏幕中的坐標仇奶;
  • dxdy :從手勢開始時,到當前回調是移動距離比驻;
  • vxvy :當前手勢移動的速度该溯;
  • numberActiveTouches :當期觸摸手指數(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 實例掸掸,并設置想好相關的屬性氯庆,然后把這個對象設置給 View 的屬性,如下:

<View  
  {...this._panResponder.panHandlers}
/>

其余的代碼也比較簡單扰付,這里就不詳述了堤撵。

5. 總結

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

在嵌套組件的事件處理中盐固,RN 中提供了“冒泡”和“下沉”兩個方向的事件處理荒给,這有點類似于 Android Native 上最近才支持的NestedScrolling,這就提供更加強大的事件處理機制刁卜。

另外需要注意志电,因為 RN 的異步通信和執(zhí)行機制,前面描述的所有回調函數(shù)都是在 JS 線程中蛔趴,并不是 Native 的 UI 線程挑辆,而 Native 平臺的 Touch 事件都是在 UI 線程中。所以在 JS 中通過 Touch 或者手勢實現(xiàn)動畫夺脾,可能會延遲的問題之拨。

參考:
http://www.tuicool.com/articles/IreaYfv

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市咧叭,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌烁竭,老刑警劉巖菲茬,帶你破解...
    沈念sama閱讀 222,104評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡婉弹,警方通過查閱死者的電腦和手機睬魂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來镀赌,“玉大人氯哮,你說我怎么就攤上這事∩谭穑” “怎么了喉钢?”我有些...
    開封第一講書人閱讀 168,697評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長良姆。 經常有香客問我肠虽,道長,這世上最難降的妖魔是什么玛追? 我笑而不...
    開封第一講書人閱讀 59,836評論 1 298
  • 正文 為了忘掉前任税课,我火速辦了婚禮,結果婚禮上痊剖,老公的妹妹穿的比我還像新娘韩玩。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 68,851評論 6 397
  • 文/花漫 我一把揭開白布誊涯。 她就那樣靜靜地躺著缘挑,像睡著了一般。 火紅的嫁衣襯著肌膚如雪叮雳。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,441評論 1 310
  • 那天妇汗,我揣著相機與錄音帘不,去河邊找鬼。 笑死杨箭,一個胖子當著我的面吹牛寞焙,可吹牛的內容都是我干的。 我是一名探鬼主播互婿,決...
    沈念sama閱讀 40,992評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼捣郊,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了慈参?” 一聲冷哼從身側響起呛牲,我...
    開封第一講書人閱讀 39,899評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎驮配,沒想到半個月后娘扩,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體着茸,經...
    沈念sama閱讀 46,457評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,529評論 3 341
  • 正文 我和宋清朗相戀三年琐旁,在試婚紗的時候發(fā)現(xiàn)自己被綠了涮阔。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,664評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡灰殴,死狀恐怖敬特,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情牺陶,我是刑警寧澤伟阔,帶...
    沈念sama閱讀 36,346評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站义图,受9級特大地震影響减俏,放射性物質發(fā)生泄漏。R本人自食惡果不足惜碱工,卻給世界環(huán)境...
    茶點故事閱讀 42,025評論 3 334
  • 文/蒙蒙 一娃承、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧怕篷,春花似錦历筝、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至蒸痹,卻和暖如春春弥,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背叠荠。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評論 1 272
  • 我被黑心中介騙來泰國打工匿沛, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人榛鼎。 一個月前我還...
    沈念sama閱讀 49,081評論 3 377
  • 正文 我出身青樓逃呼,卻偏偏與公主長得像,于是被迫代替她去往敵國和親者娱。 傳聞我的和親對象是個殘疾皇子抡笼,可洞房花燭夜當晚...
    茶點故事閱讀 45,675評論 2 359

推薦閱讀更多精彩內容