標籤:
手勢識別在行動裝置上比在網路上要複雜得多。當應用程式確定使用者的意圖時,一個觸摸可能要經曆幾個階段。
例如,應用程式需要確定觸摸是否是滾動,滑動組件還是輕擊,地圖上的縮放。這甚至可以在觸摸期間發生改變,也可以有多個同時觸摸。
要想使組件在沒有任何額外的關於它們的父組件或子組件的認知的情況下處理這些觸摸互動,需要觸摸應答系統。這個系統在ResponderEventPlugin.js 中實現了,其中包含更多細節和文檔。
最佳實務
使用者在 web app與native app的可用性上可以感覺到巨大的差異,並且這是最大的原因之一。每一個動作都應該有以下屬性:
- 反饋/高亮 – 顯示給使用者是什麼正在處理他們的觸摸,以及當他們釋放手勢時,會發生什麼
- 撤銷的能力 – 當做一個動作時,使用者應該能夠在觸摸過程中通過移動手指中止該動作
這些特性讓使用者使用一個應用程式時更舒適,因為它允許人們在實驗和互動時不用擔心犯錯誤。
TouchableHighlight 和 Touchable*
應答系統在使用時可能是複雜的。所以我們為應該“可以輕擊的”東西提供了一個抽象的 Touchable 實現。這使用了應答系統,並且使你以聲明的方式可以輕鬆地識別輕擊互動。在網路中任何你會用到按鈕或連結的地方使用 TouchableHighlight。
響應器生命週期
通過實施正確的處理方法,視圖可以成為接觸響應器。有兩種方法來詢問視圖是否想成為響應器:
- View.props.onStartShouldSetResponder: (evt) => true,——這個視圖是否在觸摸開始時想成為響應器?
- View.props.onMoveShouldSetResponder: (evt) => true,——當視圖不是響應器時,該指令被在視圖上移動的觸摸調用:這個視圖想“聲明”觸摸響應嗎?
如果視圖返回 true 並且想成為響應器,那麼下述的情況之一就會發生:
- View.props.onResponderGrant:(evt)= > { } ——視圖現在正在響應觸摸事件。這個時候要高亮標明並顯示給使用者正在發生的事情。
- View.props.onResponderReject:(evt)= > { }——當前有其他的東西成為響應器並且沒有釋放它。
如果視圖正在響應,那麼可以調用以下處理常式:
- View.props.onResponderMove:(evt)= > { }——使用者正移動他們的手指
- View.props.onResponderRelease:(evt)= > { }——在觸摸最後被引發,即“touchUp”
- View.props.onResponderTerminationRequest:(evt)= >true——其他的東西想成為響應器。這種視圖應該釋放應答嗎?返回 true 就是允許釋放
- View.props.onResponderTerminate:(evt)= > { }——響應器已經從該視圖抽離了。可能在調用onResponderTerminationRequest 之後被其他視圖擷取,也可能是被作業系統在沒有請求的情況下擷取了(發生在 iOS 的 control center/notification center)。
evt 是一個綜合的觸摸事件,有以下形式:
- changedTouches:自從上個事件之後,所有發生改變的觸摸事件的數組
- identifier——觸摸的 ID
- locationX ——觸摸相對於元素的 X 位置
- locationY——觸摸相對於元素的 Y 位置
- pageX——觸摸相對於螢幕的 X 位置
- pageY——觸摸相對於螢幕的 Y 位置
- target——接收觸摸事件的元素的節點 id
- timestamp——觸摸的時間標識符,用於速度計算
- touches——所有當前在螢幕上觸摸的數組
捕捉 ShouldSet 處理常式
在冒泡模式,即最深的節點最先被調用的情況下,onStartShouldSetResponder 和 onMoveShouldSetResponder 被調用。這意味著,當多個視圖為 * ShouldSetResponder 處理常式返回 true 時,最深的組件會成為響應器。在大多數情況下,這是可取的,因為它確保了所有控制項和按鈕是可用的。
然而,有時父組件會想要確保它成為響應器。這可以通過使用捕獲階段進行處理。在應答系統從最深的組件冒泡時,它將進行一個捕獲階段,引發 * ShouldSetResponderCapture。所以如果一個父視圖要防止子視圖在觸摸開始時成為響應器,它應該有一個 onStartShouldSetResponderCapture 處理常式,返回 true。
- View.props.onStartShouldSetResponderCapture: (evt) => true,
- View.props.onMoveShouldSetResponderCapture: (evt) => true,
PanResponder全景響應器
PanResponder 將幾個觸發調節成一個單一的觸發動作。該方法可以使單一觸發動作對額外的觸發具有彈性,可以用來識別簡單的多點觸發動作。
它為響應處理常式提供了一個可預測包,這個相應處理常式是由動作應答系統提供的。對每一個處理常式,在正常事件旁提供了一個新的 gestureState 對象。 一個 gestureState 對象有以下屬性:
- stateID-gestureState 的ID-在螢幕上保持至少一個觸發動作的時間
- moveX-最近動態觸發的最新的螢幕座標
- x0-應答器橫向的螢幕座標
- y0-應答器縱向的螢幕座標
- dx-觸發開始後累積的橫向動作距離
- dy-觸發開始後累積的縱向動作距離
- vx-當前手勢的橫向速度
- vy-當前手勢的縱向速度
- numberActiveTouch-螢幕上當前觸發的數量
基本用法
componentWillMount: function() { this._panResponder = PanResponder.create({ // Ask to be the responder: onStartShouldSetPanResponder: (evt, gestureState) => true, onStartShouldSetPanResponderCapture: (evt, gestureState) => true, onMoveShouldSetPanResponder: (evt, gestureState) => true, onMoveShouldSetPanResponderCapture: (evt, gestureState) => true, onPanResponderGrant: (evt, gestureState) => { // The guesture has started. Show visual feedback so the user knows // what is happening! // gestureState.{x,y}0 will be set to zero now }, onPanResponderMove: (evt, gestureState) => { // The most recent move distance is gestureState.move{X,Y} // The accumulated gesture distance since becoming responder is // gestureState.d{x,y} }, onPanResponderTerminationRequest: (evt, gestureState) => true, onPanResponderRelease: (evt, gestureState) => { // The user has released all touches while this view is the // responder. This typically means a gesture has succeeded }, onPanResponderTerminate: (evt, gestureState) => { // Another component has become the responder, so this gesture // should be cancelled }, onShouldBlockNativeResponder: (evt, gestureState) => { // Returns whether this component should block native components from becoming the JS // responder. Returns true by default. Is currently only supported on android. return true; }, }); }, render: function() { return ( <View {...this._panResponder.panHandlers} /> ); },
方法:
- static create(config: object)
@param {object} 所有應答器回調配置的增強版本,應答器回調不僅可以提供典型的 ResponderSyntheticEvent,還可以提供 PanResponder 動作狀態。在每一個典型的 onResponder* 回調中,用 PanResponder 對 Responder 做簡單的替換。例如, config對象可能看起來像如下形式:
- onMoveShouldSetPanResponder: (e, gestureState) => {…}
- onMoveShouldSetPanResponderCapture: (e, gestureState) => {…}
- onMoveShouldSetPanResponderCapture: (e, gestureState) => {…}
- onStartShouldSetPanResponder: (e, gestureState) => {…}
- onStartShouldSetPanResponderCapture: (e, gestureState) => {…}
- onPanResponderReject: (e, gestureState) => {…}
- onPanResponderGrant: (e, gestureState) => {…}
- onPanResponderStart: (e, gestureState) => {…}
- onPanResponderEnd: (e, gestureState) => {…}
- onPanResponderRelease: (e, gestureState) => {…}
- onPanResponderMove: (e, gestureState) => {…}
- onPanResponderTerminate: (e, gestureState) => {…}
- onPanResponderTerminationRequest: (e, gestureState) => {…}
一般來說,對於那些捕獲的等價事件,我們在捕獲階段更新一次 gestureState ,並且也可以在冒泡階段使用。
在 onStartShould* 回調時需要注意一點。在對節點的捕獲/冒泡階段的開始/結束事件中,它們只對更新後的 gestureState 做出反應。一旦節點成為應答器,你可以依靠每一個被動作和 gestureState 處理後相應更新的開始/結束事件。 (numberActiveTouches) 可能不完全準確,除非你是應答器。
執行個體:
‘use strict‘;import React, { Component } from ‘react‘;import { PanResponder, StyleSheet, View, Text, processColor,} from ‘react-native‘;var CIRCLE_SIZE = 80;export default class PanResponderMazouri extends Component { constructor(props) { super(props); this._handleStartShouldSetPanResponder = this._handleStartShouldSetPanResponder.bind(this); this._handleMoveShouldSetPanResponder = this._handleMoveShouldSetPanResponder.bind(this); this._handlePanResponderGrant = this._handlePanResponderGrant.bind(this); this._handlePanResponderMove = this._handlePanResponderMove.bind(this); this._handlePanResponderEnd = this._handlePanResponderEnd.bind(this); this._handlePanResponderEnd = this._handlePanResponderEnd.bind(this); this._highlight = this._highlight.bind(this); this.state = { _panResponder: {}, _previousLeft: 0, _previousTop: 0, backgroundColor: ‘green‘, } } componentWillMount() { console.log("MAZOURI_LOG componentDidMount"); this._panResponder = PanResponder.create({ onStartShouldSetPanResponder: this._handleStartShouldSetPanResponder, onMoveShouldSetPanResponder: this._handleMoveShouldSetPanResponder, onPanResponderGrant: this._handlePanResponderGrant, onPanResponderMove: this._handlePanResponderMove, onPanResponderRelease: this._handlePanResponderEnd, onPanResponderTerminate: this._handlePanResponderEnd, }); this.setState({ _previousLeft: 20, _previousTop: 84, left: 20, top: 84, backgroundColor: ‘green‘, }); } componentDidMount() { console.log("MAZOURI_LOG componentDidMount"); } render() { console.log("MAZOURI_LOG left:"+ this.state.left + "top:" + this.state.top + "backgroundColor:" + this.state.backgroundColor); return ( <View style={styles.container}> <View ref={circle => this.circle = circle} style={[styles.circle, {left: this.state.left, top: this.state.top, backgroundColor: this.state.backgroundColor}]} {...this._panResponder.panHandlers} /> </View> ); } _handleStartShouldSetPanResponder(e, gestureState) { console.log("MAZOURI_LOG _handleStartShouldSetPanResponder"); return true; } _handleMoveShouldSetPanResponder(e, gestureState) { console.log("MAZOURI_LOG _handleMoveShouldSetPanResponder"); return true; } _handlePanResponderGrant(e, gestureState) { console.log("MAZOURI_LOG _handlePanResponderGrant"); this._highlight; } _handlePanResponderMove(e, gestureState) { console.log("MAZOURI_LOG _handlePanResponderMove"); var _left = this.state._previousLeft + gestureState.dx; var _top = this.state._previousTop + gestureState.dy; this.setState({ left: _left, top: _top, }); } _handlePanResponderEnd(e, gestureState) { console.log("MAZOURI_LOG _handlePanResponderEnd"); this._unHighlight; var _previousLeft = this.state._previousLeft + gestureState.dx; var _previousTop = this.state._previousTop + gestureState.dy; this.setState({ _previousLeft: _previousLeft, _previousTop: _previousTop, }); } _highlight() { this.setState({ backgroundColor: ‘blue‘, }); } _unHighlight() { this.setState({ backgroundColor: ‘green‘, }); }}var styles = StyleSheet.create({ circle: { width: CIRCLE_SIZE, height: CIRCLE_SIZE, borderRadius: CIRCLE_SIZE / 2, position: ‘absolute‘, left: 0, top: 0, }, container: { flex: 1, paddingTop: 64, },});
[深入剖析React Native]手勢響應講解