React-Native之手勢基礎篇
好的東西都要分享,尊重原版原連結。
React-Native是一款由Facebook開發並開源的框架,主要賣點是使用JavaScript編寫原生的移動應用。從2015年3月份開源到現在,已經差不多有半年。目前,React-Native正在以幾乎每週一個版本的速度進行快速迭代,開源社群非常活躍。2015年9月15日,React-Native正式宣佈支援安卓,並在專案主頁中更新了相關文件,這意味著React-Native已經完全覆蓋了目前主流的iOS和Android系統,做到了“learn once,write everywhere”。React-Native能否顛覆傳統的APP開發方式,現在下結論還為時尚早,但從微博和Twitter對React-Native相關訊息的轉發數量和評論來看,React-Native在未來的一段時間內都將是移動開發的熱點。
從我自己的實際使用經歷出發,在使用React-Native寫了幾個Demo之後,我覺得React-Native是一個非常有前途的框架。雖然目前文件並不能做到面面俱到,實際使用過程中坑也略多,但是填坑的速度也非常快。在專案的issues中提一個issue,基本都能在幾個小時之內獲得解答或者解決方案。因此,我決定比較系統的學習一下React-Native。
在移動應用開發中,手勢是不可忽視的一個重要組成部分,React-Native針對應用中的手勢處理,提供了gesture responder system,從最基本的點選手勢,到複雜的滑動,都有現成的解決方案。
和以往的Hybrid應用相比,使用React-Native開發的原生應用的一大優勢就是可以流暢的響應使用者的手勢操作,這也是使用React-Native相比以往在原生應用中插入webview控制元件的一個優勢,因此,相比web端的手勢,React-Native應用中的手勢要複雜得多。我在初次接觸React-Native手勢之初也是看的一頭霧水,經過搜尋也發現相關的資料比較少,因此萌發了寫一篇相關文章的想法。這也是寫作本文的初衷,一方面總結自己學習和摸索的經驗,以作為後來使用中的備忘錄,另一方面也作為交流分享之用。
Touch*手勢
移動應用中最簡單的手勢,就是touch手勢,而這也是應用中最常使用的手勢,類比web開發中的事件,就好比web開發中的click。在web開發中,瀏覽器內部實現了click事件,我們可以通過onclick
或者addEventListener('click',callback)
來繫結click
事件。React-Native也針對Touch手勢進行了類似的實現,在React-Native中,一共有四個和Touch相關的元件:
- TouchableHighlight
- TouchableNativeFeedback
- TouchableOpacity
- TouchableWithoutFeedback
使用這四個元件,我們就可以在應用的某個部分繫結上Touch事件,來個簡單的例子:
/** * Sample React Native App * https://github.com/facebook/react-native */ 'use strict'; var React = require('react-native'); var { AppRegistry, StyleSheet, Text, View, TouchableHighlight } = React; var gesture = React.createClass({ _onPressIn(){ this.start = Date.now() console.log("press in") }, _onPressOut(){ console.log("press out") }, _onPress(){ console.log("press") }, _onLonePress(){ console.log("long press "+(Date.now()-this.start)) }, render: function() { return ( <View style={styles.container}> <TouchableHighlight style={styles.touchable} onPressIn={this._onPressIn} onPressOut={this._onPressOut} onPress={this._onPress} onLongPress={this._onLonePress}> <View style={styles.button}> </View> </TouchableHighlight> </View> ); } }); var styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#F5FCFF', }, button:{ width: 200, height: 200, borderRadius: 100, backgroundColor: 'red' }, touchable: { borderRadius: 100 } }); AppRegistry.registerComponent('gesture', () => gesture);
在上面的程式碼中,主要部分就是一個作為容器的View,一個作為按鈕的View,因為想要給這個按鈕繫結Touch手勢,因此使用了TouchableHighlight這個和Touch相關的元件,將它作為按鈕的一個包裹,在這個包裹的props中規定相應的回撥即可。
前面提到了和Touch相關的元件一共有四個,它們的基本用法都很類似,只是實現的功能不太相同。先說最常用的TouchableHighlight,這個元件的作用,除了給內部元素增加繫結事件之外,還負責給內部元素增加“點選態”。所謂的“點選態”,就是在使用者在點選的時候,會產生一個短暫出現覆蓋層,用來告訴使用者這個區塊被點選到了。TouchableNativeFeedback這個元件只能用在安卓上,它可以針對點選在點選區域中顯示不同的效果,例如最新安卓系統中的Material Design的點選波紋效果。TouchableOpacity這個元件用來給為內部元素在點選時新增透明度。TouchableWithoutFeedback這個元件只響應touch手勢,不增加點選態,不推薦使用。
四個元件的用法大致相同,具體的細節方法可以參看具體文件。回頭看上面的程式碼,在iOS模擬器中執行的效果圖如下:
在上面的程式碼中,TouchableHighlight元件上綁定了4個方法:
- onPress
- onPressIn
- onPressOut
- onLonePress
這4個方法也是React-Native幫助使用者實現的4個手勢,通過在4個相應的回撥函式中輸出不同的內容,我們可以研究4個手勢出現的條件和順序。開啟chrome debug模式,點選模擬器中的按鈕,可以看到瀏覽器控制檯裡面的輸出內容:
原生應用之所以為原生,和web應用相比,有兩個比較主要的區別:
- 原生應用會對觸控事件作出響應,也就是“點選態”;
- 原生應用可以選擇中途撤銷觸控事件;
前面一點比較清楚,第二點選擇中途撤銷是什麼意思呢?舉個最簡單的例子,用微信聊天的時候,點選了一個好友,可以進入聊天介面,但是如果我點中了一個好友,突然又不想和他聊天了,我會多按一會,然後將手指劃開,這樣就可以撤銷剛才的觸控事件,就好像根本就沒有點選過一樣。平時使用得太習慣,可能沒有意識到原來這個操作是撤消了觸控事件,現在回過頭一想,還真是這麼一回事。
通過前面的實驗,我們可以對press,pressIn,pressOut,longPress事件的觸發條件和觸發順序有一個比較清晰的瞭解:
- 快速點選,只會觸發press事件
- 只要在點選時有一個“按”的操作,就是比快速點選要按的久一點,就會觸發pressin事件
- 如果同時綁定了pressIn, pressOut和press事件,那麼當pressIn事件觸發之後,如果使用者的手指在繫結的元件中釋放,那麼接著會連續觸發pressOut和press事件,此時的順序是pressIn -> pressOut -> press。而如果使用者的手指滑到了繫結元件之外才釋放,那麼此時將會不觸發press事件,只會觸發pressOut事件,此時的順序是pressIn -> pressOut。後一種情況就是前面所說的中途取消,如果我們將回調函式繫結給press事件,那麼後一種情況中回撥函式並不會被觸發,相當於"被取消"。
- 如果綁定了longPress事件,那麼在pressIn事件被觸發之後,press事件不會被觸發,通過打點計時,可以發現longPress事件的觸發時間大概是在pressIn事件發生383ms之後,當longPress事件觸發之後,無論使用者的手指在哪裡釋放,都會接著觸發pressOut事件,此時的觸發順序是 pressIn -> longPress -> pressOut
以上內容就是對React-Native對Touch事件的實現和用法分析,對於大部分應用來說,使用這四個Touch*元件再配合4個press事件就能對使用者的手勢進行響應。但是對於比較複雜的互動,還是得使用React-Native中的gesture responder system。
gesture responder system
在React Native中,響應手勢的基本單位是responder
,具體來說,就是最常見的View
元件。任何的View元件,都是潛在的responder,如果某個View元件沒有響應手勢操作,那是因為它還沒有被“開發”。
將一個普通的View元件開發成為一個能響應手勢操作的responder,非常簡單,只需要按照React Native的gesture responder system的規範,在props上設定幾個方法即可。具體如下:
- View.props.onStartShouldSetResponder
- View.props.onMoveShouldSetResponder
- View.props.onResponderGrant
- View.props.onResponderReject
- View.props.onResponderMove
- View.props.onResponderRelease
- View.props.onResponderTerminationRequest
- View.props.onResponderTerminate
乍看之下,這幾個方法名字又長有奇怪,但是當了解了React Native對手勢響應的流程之後,記憶這幾個方法也非常容易。
要理解React Native的手勢操作過程,首先要記住一點:
一個React Native應用中只能存在一個responder
正因為如此,gesture responder system中才存在_reject和_terminate方法。React Native事件響應的基本步驟如下:
- 使用者通過觸控或者滑動來“啟用”某個responder,這個步驟由View.props.onStartShouldSetResponder以及View.props.onMoveShouldSetResponder這兩個方法負負責處理,如果返回值為true,則表示這個View能夠響應觸控或者滑動手勢被啟用
- 如果元件被啟用,View.props.onResponderGrant方法被呼叫,一般來說,這個時候需要去改變組建的底色或者透明度,來表示元件已經被啟用
- 接下來,使用者開始滑動手指,此時View.props.onResponderMove方法被呼叫
- 當用戶的手指離開螢幕之後,View.props.onResponderRelease方法被呼叫,此時元件恢復被觸控之前的樣式,例如底色和透明度恢復之前的樣式,完成一次手勢操作
綜上所述,一次正常的手勢操作的流程如下所示:
響應touch或者move手勢 -> grant(被啟用) -> move -> release(結束事件)
來段簡單的示例程式碼:
/** * Sample React Native App * https://github.com/facebook/react-native */ 'use strict'; var React = require('react-native'); var { AppRegistry, StyleSheet, Text, View, } = React; var pan = React.createClass({ getInitialState(){ return { bg: 'white' } }, componentWillMount(){ this._gestureHandlers = { onStartShouldSetResponder: () => true, onMoveShouldSetResponder: ()=> true, onResponderGrant: ()=>{this.setState({bg: 'red'})}, onResponderMove: ()=>{console.log(123)}, onResponderRelease: ()=>{this.setState({bg: 'white'})} } }, render: function() { return ( <View style={styles.container}> <View {...this._gestureHandlers} style={[styles.rect,{ "backgroundColor": this.state.bg }]}></View> </View> ); } }); var styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#F5FCFF', }, rect: { width: 200, height: 200, borderWidth: 1, borderColor: 'black' } }); AppRegistry.registerComponent('pan', () => pan);
執行這段程式碼,當中間的正方形被啟用時,底色變為紅色,release之後,底色又變為白色。
上面是正常事件響應流程,但是當應用中存在不止一個手勢responder的時候,事情可能就複雜起來了。比如應用中存在兩個responder,當使用一個手指啟用一個responder之後,又去啟用另一個responder會怎麼樣?因為React Native應用中只存在一個Responder,此時就會出現responder互斥的情況。具體來說過程如下:
- 一個responder已經被啟用
- 第一個responder還沒有被release,使用者去嘗試去啟用第另一個responder
- 後面將要被啟用的responder去和前面還沒有被釋放的responder“協商”:兄弟,你都被啟用這麼久了,讓我也活動一下唄?結果兩種情況:
- 前面的responder比較“強硬”,非要佔據唯一的responder的位置
- 前面的responder比較“好說話”,主動release
- 前面一種情況,後面的responder的onResponderReject方法被呼叫,後面的responder沒有被啟用
- 後面一種情況,後面的responder被啟用,onResponderGrant方法被呼叫 ,前面的responder的onResponderTerminate方法被呼叫,前面的responder的狀態被釋放
上面的步驟中,比較重要的部分是第三步“協商”,這個步驟由onResponderTerminationRequest這個方法的返回值決定,如果一個responder的這個方法的返回值是true,那麼說明這個responder是“好說話”的方法,反之則是“強硬”的方法。
由於在iOS simulator上不好模擬這個過程,大家可以自行編寫應用在真機上對這個“協商”的步驟進行嘗試。
和web的事件的冒泡過程類似,React Native中的事件遵循的也是冒泡機制。預設情況下,當潛在的responder的互相巢狀時,最頂部的responder將會響應事件。大部分時候,這也是開發者想要的邏輯。但是我們可以來自定義響應事件的responder。具體來說,通過:
- View.props.onStartShouldSetResponderCapture
- View.props.onMoveShouldSetResponderCapture
兩個方法來進行設定。當某個潛在responder的這兩個方法的其中一個返回值為true時,即使當前的View元件不在最頂部,唯一一個responder的位置也會由它佔據。看下面的例子:
/** * Sample React Native App * https://github.com/facebook/react-native */ 'use strict'; var React = require('react-native'); var { AppRegistry, StyleSheet, Text, View, } = React; var pan = React.createClass({ getInitialState(){ return { bg: 'white', bg2: 'white' } }, componentWillMount(){ this._gestureHandlers = { onStartShouldSetResponder: () => true, onMoveShouldSetResponder: ()=> true, onResponderGrant: ()=>{this.setState({bg: 'red'})}, onResponderMove: ()=>{console.log(123)}, onResponderRelease: ()=>{this.setState({bg: 'white'})}, } this._gestureHandlers2 = { onStartShouldSetResponder: () => true, onMoveShouldSetResponder: ()=> true, onResponderGrant: ()=>{this.setState({bg2: 'green'})}, onResponderMove: ()=>{console.log(123)}, onResponderRelease: ()=>{this.setState({bg2: 'white'})} } }, render: function() { return ( <View style={styles.container}> <View {...this._gestureHandlers} style={[styles.rect,{ "backgroundColor": this.state.bg }]}> <View {...this._gestureHandlers2} style={[styles.rect2,{ "backgroundColor": this.state.bg2 }]} > </View> </View> </View> ); } }); var styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#F5FCFF', }, rect: { width: 200, height: 200, borderWidth: 1, borderColor: 'black', justifyContent: 'center', alignItems: 'center', }, rect2: { width: 100, height: 100, borderWidth: 1, borderColor: 'black' } }); AppRegistry.registerComponent('pan', () => pan);
這是正常的情況,當用戶觸控最頂部的正方形時,最頂部的正方形會響應觸控事件,底色變為綠色,外層的正方形則不會響應觸控事件:
而當在外層的View中加入
onStartShouldSetResponderCapture: () => true, onMoveShouldSetResponderCapture: ()=> true,
兩個方法之後,即使點選最頂部的小正方形,響應的responder也變為了外層的正方形:
和web開發中的事件引數類似,以上的每個方法都有一個evt引數,在事件發生的過程中,這個evt引數的nativeEvent屬性的各個值能夠標示手勢進行的狀態,如下所示:
引數的具體含義可以參看React Native文件。
PanResponder
除了gesture responder system之外,React Native還抽象出了一套PanResponder方法,和gesture responder system相比,PanResponder方法的抽象程度更高,使用起來也更為方便。在使用PanResponder的時候,相應手勢的邏輯和流程都不變,只需要根據文件對幾個方法名稱作修改即可。PanResponder的好處是:對於每個方法,除了第一個evt引數之外,開發者還可以使用第二個引數gestureState,這個gestureState是一個物件,包含手勢進行過程中更多的資訊,其中比較常用的幾個是:
- dx/dy:手勢進行到現在的橫向/縱向相對位移
- vx/vy:此刻的橫向/縱向速度
- numberActiveTouches:responder上的觸控的個數
通過使用PanResponder,我們可以非常方便的實現drag & drop的效果。程式碼如下所示:
/** * Sample React Native App * https://github.com/facebook/react-native */ 'use strict'; var React = require('react-native'); var { AppRegistry, StyleSheet, Text, View, PanResponder } = React; var pan = React.createClass({ getInitialState(){ return { bg: 'white', top: 0, left: 0 } }, componentWillMount(){ this._panResponder = PanResponder.create({ onStartShouldSetPanResponder: () => true, onMoveShouldSetPanResponder: ()=> true, onPanResponderGrant: ()=>{ this._top = this.state.top this._left = this.state.left this.setState({bg: 'red'}) }, onPanResponderMove: (evt,gs)=>{ console.log(gs.dx+' '+gs.dy) this.setState({ top: this._top+gs.dy, left: this._left+gs.dx }) }, onPanResponderRelease: (evt,gs)=>{ this.setState({ bg: 'white', top: this._top+gs.dy, left: this._left+gs.dx })} }) }, render: function() { return ( <View style={styles.container}> <View {...this._panResponder.panHandlers} style={[styles.rect,{ "backgroundColor": this.state.bg, "top": this.state.top, "left": this.state.left }]}></View> </View> ); } }); var styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#F5FCFF', }, rect: { width: 200, height: 200, borderWidth: 1, borderColor: 'black', position: 'absolute', } }); AppRegistry.registerComponent('pan', () => pan);
總結
以上的內容就是我近一段事件來對React Native手勢的學習和理解。講了一些基本原理,但是要實現一些更加複雜的手勢,例如pinch、rotate、zoom,還需要更進一步的研究和學習。
===============
關於手勢響應系統更多的學習資料: