1. 程式人生 > >React-Native之手勢基礎篇

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模擬器中執行的效果圖如下:

2015-09-16 1 04 19

在上面的程式碼中,TouchableHighlight元件上綁定了4個方法:

  • onPress
  • onPressIn
  • onPressOut
  • onLonePress

這4個方法也是React-Native幫助使用者實現的4個手勢,通過在4個相應的回撥函式中輸出不同的內容,我們可以研究4個手勢出現的條件和順序。開啟chrome debug模式,點選模擬器中的按鈕,可以看到瀏覽器控制檯裡面的輸出內容:

rn

原生應用之所以為原生,和web應用相比,有兩個比較主要的區別:

  1. 原生應用會對觸控事件作出響應,也就是“點選態”;
  2. 原生應用可以選擇中途撤銷觸控事件;

前面一點比較清楚,第二點選擇中途撤銷是什麼意思呢?舉個最簡單的例子,用微信聊天的時候,點選了一個好友,可以進入聊天介面,但是如果我點中了一個好友,突然又不想和他聊天了,我會多按一會,然後將手指劃開,這樣就可以撤銷剛才的觸控事件,就好像根本就沒有點選過一樣。平時使用得太習慣,可能沒有意識到原來這個操作是撤消了觸控事件,現在回過頭一想,還真是這麼一回事。

通過前面的實驗,我們可以對press,pressIn,pressOut,longPress事件的觸發條件和觸發順序有一個比較清晰的瞭解:

  1. 快速點選,只會觸發press事件
  2. 只要在點選時有一個“按”的操作,就是比快速點選要按的久一點,就會觸發pressin事件
  3. 如果同時綁定了pressIn, pressOut和press事件,那麼當pressIn事件觸發之後,如果使用者的手指在繫結的元件中釋放,那麼接著會連續觸發pressOut和press事件,此時的順序是pressIn -> pressOut -> press。而如果使用者的手指滑到了繫結元件之外才釋放,那麼此時將會不觸發press事件,只會觸發pressOut事件,此時的順序是pressIn -> pressOut。後一種情況就是前面所說的中途取消,如果我們將回調函式繫結給press事件,那麼後一種情況中回撥函式並不會被觸發,相當於"被取消"。
  4. 如果綁定了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事件響應的基本步驟如下:

  1. 使用者通過觸控或者滑動來“啟用”某個responder,這個步驟由View.props.onStartShouldSetResponder以及View.props.onMoveShouldSetResponder這兩個方法負負責處理,如果返回值為true,則表示這個View能夠響應觸控或者滑動手勢被啟用
  2. 如果元件被啟用,View.props.onResponderGrant方法被呼叫,一般來說,這個時候需要去改變組建的底色或者透明度,來表示元件已經被啟用
  3. 接下來,使用者開始滑動手指,此時View.props.onResponderMove方法被呼叫
  4. 當用戶的手指離開螢幕之後,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之後,底色又變為白色。

rn1

上面是正常事件響應流程,但是當應用中存在不止一個手勢responder的時候,事情可能就複雜起來了。比如應用中存在兩個responder,當使用一個手指啟用一個responder之後,又去啟用另一個responder會怎麼樣?因為React Native應用中只存在一個Responder,此時就會出現responder互斥的情況。具體來說過程如下:

  1. 一個responder已經被啟用
  2. 第一個responder還沒有被release,使用者去嘗試去啟用第另一個responder
  3. 後面將要被啟用的responder去和前面還沒有被釋放的responder“協商”:兄弟,你都被啟用這麼久了,讓我也活動一下唄?結果兩種情況:
    • 前面的responder比較“強硬”,非要佔據唯一的responder的位置
    • 前面的responder比較“好說話”,主動release
  4. 前面一種情況,後面的responder的onResponderReject方法被呼叫,後面的responder沒有被啟用
  5. 後面一種情況,後面的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);

這是正常的情況,當用戶觸控最頂部的正方形時,最頂部的正方形會響應觸控事件,底色變為綠色,外層的正方形則不會響應觸控事件:

rn2

而當在外層的View中加入

      onStartShouldSetResponderCapture: () => true,
      onMoveShouldSetResponderCapture: ()=> true,

兩個方法之後,即使點選最頂部的小正方形,響應的responder也變為了外層的正方形:

rn3

和web開發中的事件引數類似,以上的每個方法都有一個evt引數,在事件發生的過程中,這個evt引數的nativeEvent屬性的各個值能夠標示手勢進行的狀態,如下所示:

2015-09-16 11 23 24

引數的具體含義可以參看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);

rn4

總結

以上的內容就是我近一段事件來對React Native手勢的學習和理解。講了一些基本原理,但是要實現一些更加複雜的手勢,例如pinch、rotate、zoom,還需要更進一步的研究和學習。

===============

關於手勢響應系統更多的學習資料: