在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(结束事件)
来段简单的示例代码:
import React, { Component } from 'react';
import {
Text,
View,
StyleSheet,
ScrollView,
TouchableOpacity,
TouchableHighlight,
TouchableWithoutFeedback
} from 'react-native';
import { Navigation } from 'react-native-navigation';
/**
* 验证父View与子View事件的分发与拦截(有图有真相)
*/
class TouchViewScreen extends Component {
static navigatorStyle = {
drawUnderNavBar: false,
tabBarHidden: true
};
constructor(props) {
super(props);
this.state={
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'})},
//----------------------外层View拦截了点击事件------------------------
// onStartShouldSetResponderCapture: () => true,
// onMoveShouldSetResponderCapture: ()=> true,
};
this._gestureHandlers2 = {
onStartShouldSetResponder: () => true,
onMoveShouldSetResponder: ()=> true,
onResponderGrant: ()=>{this.setState({bg2: 'green'})},
onResponderMove: ()=>{console.log(123)},
onResponderRelease: ()=>{this.setState({bg2: 'white'})}
}
}
render() {
return (
this._gestureHandlers}
style={[styles.rect,{
backgroundColor: this.state.bg
}]}>
this._gestureHandlers2}
style={[styles.rect2,{
backgroundColor: this.state.bg2
}]}
>
)
}
}
const 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'
}
});
export default TouchViewScreen;
上一小节介绍的都是针对单个组件来说,事件处理的流程和机制。但是前面也提到了,当组件需要作为事件处理响应者时,需要通过 onStartShouldSetResponder 或者 onMoveShouldSetResponder 回调返回值为 true 来申请。假如当多个组件嵌套的时候,这两个回调都返回了 true 的时候,但是同一个只能有一个事件处理响应者,这种情况怎么处理呢?为了便于描述,假设我们的组件布局如下:
在 RN 中,默认情况下使用冒泡机制,响应最深的组件最先开始响应,所以前面描述的这种情况,如图中,如果 A、B、C 三个组件的 on*ShouldSetResponder 都返回为 true,那么只有 C 组件会得到响应成为响应者。这种机制才能保证了界面所有的组件才能得到响应。但是有些情况下,可能父组件可能需要处理事件,而禁止子组件响应。RN 提供了一个劫持机制,也就是在触摸事件往下传递的时候,先询问父组件是否需要劫持,不给子组件传递事件,也就是如下两个回调:
View.props.onStartShouldSetResponderCapture:这个属性接收一个回调函数,函数原型是 function(evt): bool,在触摸事件开始(touchDown)的时候,RN 容器组件会回调此函数,询问组件是否要劫持事件响应者设置,自己接收事件处理,如果返回 true,表示需要劫持;
View.props.onStartShouldSetResponder,这个属性接收一个回调函数,函数原型是 function(evt): bool,在触摸事件开始(touchDown)的时候,RN 会回调此函数,询问组件是否需要成为事件响应者,接收事件处理,如果返回 true,表示需要成为响应者;
上如代码:
我们也是三层嵌套,只不过最外层是一个大View。其实我们在触摸最里层的View,三个View都会有感知,但是为什么只会响应最里层View的事件呢?
也就是说,当我们触摸C组件的时候,先去问一下A组件是否要拦截,如果A不拦截,在去问一下B组件,如果B不拦截,再去问一下C是否拦截,最终三个都不拦截(默认都不拦截)。
/**
* If a parent `View` wants to prevent a child `View` from becoming responder on a touch start,
* it should have this handler which returns `true`.
*
* `View.props.onStartShouldSetResponderCapture: (event) => [true | false]`, where `event` is a
* synthetic touch event as described above.
*/
onStartShouldSetResponderCapture: PropTypes.func,
接下来,顺序反过来,去问C是否想成为事件响应者,恰巧C想成为事件响应者(默认应该是true)。
/**
* Does this view want to become responder on the start of a touch?
*
* `View.props.onStartShouldSetResponder: (event) => [true | false]`, where `event` is a
* synthetic touch event as described above.
*/
onStartShouldSetResponder: PropTypes.func,
现在,只是C这个组件需要成为事件响应者,但是不一定能够成功。只有在调用了onResponderGrant才是真正成为响应者,并且会去处理后面的事件。
如果在B里面使用onStartShouldSetResponderCapture: () => true , 说明B想拦截事件,下面便会执行onResponder***方法。