简介
在诸多移动应用中,需要有许多手势识别的使用场景。比如当涂抹、上滑、下滑、侧滑、拖动等,都涉及到方方面面的功能交互。而在react native中,就有提供处理这些手势的API,下面就来一一介绍相关的响应方法。
PanResponder类
当我们在移动设备上进行手势交互时,所进行对操作会比web上更复杂。而PanResponder类可以将多点触摸操作协调成一个手势,使得一个单点触摸可以接受更多的触摸操作。
我们可以通过PanResponder.create({options})方法,去创建手势响应事件。
手势事件一览:
// 在用户开始触摸的时候(手指刚刚接触屏幕的瞬间),是否愿意成为响应者?
onStartShouldSetPanResponder?: (e: GestureResponderEvent, gestureState: PanResponderGestureState) => boolean;
// 如果 View 不是响应者,那么在每一个触摸点开始移动(没有停下也没有离开屏幕)时再询问一次:是否愿意响应触摸交互呢?
onMoveShouldSetPanResponder?: (e: GestureResponderEvent, gestureState: PanResponderGestureState) => boolean;
// 如果某个父 View 想要在触摸操作开始时阻止子组件成为响应者,那就应该处理onStartShouldSetResponderCapture事件并返回 true 值。
onMoveShouldSetPanResponderCapture?: (e: GestureResponderEvent, gestureState: PanResponderGestureState) => boolean;
onStartShouldSetPanResponderCapture?: (e: GestureResponderEvent, gestureState: PanResponderGestureState) => boolean;
// 手势开始响应
onPanResponderGrant?: (e: GestureResponderEvent, gestureState: PanResponderGestureState) => void;
// 响应者现在“另有其人”而且暂时不会“放权”,请另作安排。
onPanResponderReject?: (e: GestureResponderEvent, gestureState: PanResponderGestureState) => void;
// View 现在要开始响应触摸事件了。这也是需要做高亮的时候,使用户知道他到底点到了哪里。
onPanResponderStart?: (e: GestureResponderEvent, gestureState: PanResponderGestureState) => void;
// 手势移动
onPanResponderMove?: (e: GestureResponderEvent, gestureState: PanResponderGestureState) => void;
// 手势释放
onPanResponderRelease?: (e: GestureResponderEvent, gestureState: PanResponderGestureState) => void;
// 手势结束
onPanResponderEnd?: (e: GestureResponderEvent, gestureState: PanResponderGestureState) => void;
// 另一个容器已经成为了新的响应者,当前手势将被取消。
onPanResponderTerminate?: (e: GestureResponderEvent, gestureState: PanResponderGestureState) => void;
// 当别的想成为响应者,是否放权
onPanResponderTerminationRequest?: (e: GestureResponderEvent, gestureState: PanResponderGestureState) => boolean;
// 当前组件是否应该阻止原生组件成为JS响应者
onShouldBlockNativeResponder?: (e: GestureResponderEvent, gestureState: PanResponderGestureState) => boolean;
1、在使用手势之前,首先应赋予相应的容器为触摸事件响应者。
可以通过onStartShouldSetResponder和onMoveShouldSetResponder去判断触摸开始时或触摸移动时,是否成为响应者。
举个🌰:
this._panResponder = PanResponder.create({
onStartShouldSetPanResponder: this.handleStartPanResponder,
onMoverShouldSetPanResponder: this.handleSetPanResponder,
})
...
handleStartPanResponder = (e:GestureResponderEvent) => {
// 如果传进来给组件的参数为disabled,则不响应
if(this.props.disabled){
return false;
}
return true;
};
handleSetPanResponder = (e:GestureResponderEvent, gesture:PanResponderGestureState)=> {
const { disabled } = this.props;
const { dx, dy } = gesture;
if (disabled) {
return false;
}
// 如果横向移动距离和纵向移动距离都小于4,则不响应
if(Math.abs(dx)<4 && Math.abs(dy)<4){
return false;
}
}
...
// 加上手势属性
<View {...this.panResponder.panHandlers}>
...
</View>
第一个参数传入的是原生事件nativeEvent,有以下属性:
changedTouches- 在上一次事件之后,所有发生变化的触摸事件的数组集合(即上一次事件后,所有移动过的触摸点)identifier- 触摸点的 IDlocationX- 触摸点相对于父元素的横坐标locationY- 触摸点相对于父元素的纵坐标pageX- 触摸点相对于根元素的横坐标pageY- 触摸点相对于根元素的纵坐标target- 触摸点所在的元素 IDtimestamp- 触摸事件的时间戳,可用于移动速度的计算touches- 当前屏幕上的所有触摸点的集合
第二个参数是当前的手势状态gestureState,有以下字段:
stateID- 触摸状态的 ID。在屏幕上有至少一个触摸点的情况下,这个 ID 会一直有效。moveX- 最近一次移动时的屏幕横坐标moveY- 最近一次移动时的屏幕纵坐标x0- 当响应器产生时的屏幕坐标y0- 当响应器产生时的屏幕坐标dx- 从触摸操作开始时的累计横向路程dy- 从触摸操作开始时的累计纵向路程vx- 当前的横向移动速度vy- 当前的纵向移动速度numberActiveTouches- 当前在屏幕上的有效触摸点的数量
熟悉以上属性,可以帮助我们更流畅地写出交互逻辑~
2、设置完响应者之后,就会开始尝试成为响应者
此时可以通过onResponderGrant、onResponderReject两个方法设置刚开始响应触摸事件、拒绝响应触摸事件的相应事件
onPanResponderGrant: (evt, gestureState) => {
// 开始手势操作。给用户一些视觉反馈,让他们知道发生了什么事情!
// gestureState.{x,y} 现在会被设置为0
},
onResponderReject: (evt, gestureState) => {
// 现在已有响应者,不会响应
},
3、成为响应者后,容器就可以响应触摸事件
此时就可以给容器添加
-
View.props.onResponderMove: (evt) => {}- 用户正在屏幕上移动手指时(没有停下也没有离开屏幕)。 -
View.props.onResponderRelease: (evt) => {}- 触摸操作结束时触发,比如"touchUp"(手指抬起离开屏幕)。 -
View.props.onResponderTerminationRequest: (evt) => true- 有其他组件请求接替响应者,当前的 View 是否“放权”?返回 true 的话则释放响应者权力。 -
View.props.onResponderTerminate: (evt) => {}- 响应者权力已经交出。这可能是由于其他 View 通过onResponderTerminationRequest请求的,也可能是由操作系统强制夺权(比如 iOS 上的控制中心或是通知中心)。最后贴一个官方对于PanResponder的🌰:github.com/facebook/re…
const React = require('react'); const {PanResponder, StyleSheet, View} = require('react-native'); const RNTesterPage = require('../../components/RNTesterPage'); import type { PanResponderInstance, GestureState, } from 'react-native/Libraries/Interaction/PanResponder'; import type {PressEvent} from 'react-native/Libraries/Types/CoreEventTypes'; type CircleStyles = { backgroundColor?: string, left?: number, top?: number, ... }; const CIRCLE_SIZE = 80; type Props = $ReadOnly<{||}>; type State = {| left: number, top: number, pressed: boolean, |}; class PanResponderExample extends React.Component<Props, State> { _previousLeft: number = 20; _previousTop: number = 84; _circleStyles: {|style: CircleStyles|} = {style: {}}; circle: ?React.ElementRef<typeof View> = null; state: State = { left: 20, top: 84, pressed: false, }; _handleStartShouldSetPanResponder = ( event: PressEvent, gestureState: GestureState, ): boolean => { // Should we become active when the user presses down on the circle? return true; }; _handleMoveShouldSetPanResponder = ( event: PressEvent, gestureState: GestureState, ): boolean => { // Should we become active when the user moves a touch over the circle? return true; }; _handlePanResponderGrant = ( event: PressEvent, gestureState: GestureState, ) => { this.setState({ pressed: true, }); }; _handlePanResponderMove = (event: PressEvent, gestureState: GestureState) => { this.setState({ left: this._previousLeft + gestureState.dx, top: this._previousTop + gestureState.dy, }); }; _handlePanResponderEnd = (event: PressEvent, gestureState: GestureState) => { this.setState({ pressed: false, }); this._previousLeft += gestureState.dx; this._previousTop += gestureState.dy; }; _panResponder: PanResponderInstance = PanResponder.create({ onStartShouldSetPanResponder: this._handleStartShouldSetPanResponder, onMoveShouldSetPanResponder: this._handleMoveShouldSetPanResponder, onPanResponderGrant: this._handlePanResponderGrant, onPanResponderMove: this._handlePanResponderMove, onPanResponderRelease: this._handlePanResponderEnd, onPanResponderTerminate: this._handlePanResponderEnd, }); render(): React.Node { return ( <RNTesterPage noSpacer={true} noScroll={true} title="Basic gesture handling"> <View style={styles.container}> <View ref={circle => { this.circle = circle; }} style={[ styles.circle, { transform: [ {translateX: this.state.left}, {translateY: this.state.top}, ], backgroundColor: this.state.pressed ? 'blue' : 'green', }, ]} {...this._panResponder.panHandlers} /> </View> </RNTesterPage> ); } } const styles = StyleSheet.create({ circle: { width: CIRCLE_SIZE, height: CIRCLE_SIZE, backgroundColor: 'green', borderRadius: CIRCLE_SIZE / 2, position: 'absolute', left: 0, top: 0, }, container: { flex: 1, height: 500, }, });