首页 > 代码库 > [深入剖析React Native]手势响应讲解
[深入剖析React Native]手势响应讲解
手势识别在移动设备上比在网络上要复杂得多。当应用程序确定用户的意图时,一个触摸可能要经历几个阶段。
例如,应用程序需要确定触摸是否是滚动,滑动部件还是轻击,地图上的缩放。这甚至可以在触摸期间发生改变,也可以有多个同时触摸。
要想使组件在没有任何额外的关于它们的父组件或子组件的认知的情况下处理这些触摸交互,需要触摸应答系统。这个系统在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]手势响应讲解