1、基本的触摸组件
- Text
- Button
- Touchable系列组件
TouchableHighlight
TouchableNativeFeedback
TouchableOpacity
TouchableWithoutFeedback
这些组件都支持onPressIn
、onPressOut
、onPress
、onLongPress
方法。
使用TouchableOpacity
示例:
<TouchableOpacity
onPressIn={() => console.log("onPressIn")}
onPressOut={() => console.log("onPressOut")}
onPress={() => console.log("onPress")}
onLongPress={() => console.log("onLongPress")}
>
<Image
style={styles.btn_login}
source={require('./img/btn_login.png')} />
</TouchableHighlight>
复制代码
Touch*手势的区别:
1、
TouchableHighlight
,除了给内部元素增加绑定事件之外,还负责给内部元素增加点击的效果;
2、TouchableNativeFeedback
只能用在安卓平台上,它可以针对点击在点击区域中显示不同的效果,例如安卓系统中的点击波纹效果;
3、TouchableOpacity
这个组件用来给为内部元素在点击时添加透明度;
4、TouchableWithoutFeedback
这个组件只响应touch手势,不增加点击态,不推荐使用。
2、手势响应系统
2.1单组件触摸事件处理
RN的组件默认不进行处理触摸事件,但是我们可以将一个普通的View组件变成一个能响应手势操作的responder,只需要在props上设置如下方法:
View.props.onStartShouldSetResponder
View.props.onMoveShouldSetResponder
View.props.onResponderGrant
View.props.onResponderReject
View.props.onResponderStart
View.props.onResponderMove
View.props.onResponderEnd
View.props.onResponderRelease
View.props.onResponderTerminationRequest
View.props.onResponderTerminate
复制代码
React Native事件响应的基本步骤如下:
1、如果组件要进行触摸事件处理,首先要申请成为事件响应者,通过如下两个属性询问组件是否成为响应者:
- View.props.onStartShouldSetResponder: (evt) => true,在触摸开始的时候(TouchDown),ReactNative会先调用此函数,询问组件是否需要成为事件的响应者;
- View.props.onMoveShouldSetResponder: (evt) => true,如果View不是响应者,那么在每一个触摸点开始移动;
2、因为同一时刻,只能有一个事件处理响应者,所以并不是每个组件都能申请成功,RN通过如下两个回调告知申请结果:
- View.props.onResponderGrant: (evt) => {}:表示申请成功,组件成为了事件处理响应者,这时组件就开始接收后序的触摸事件输入。一般情况下,这时开始,组件进入了激活状态,并进行一些事件处理或者手势识别的初始化。
- View.props.onResponderReject: (evt) => {}:表示申请失败了,这意味者其他组件正在进行事件处理,并且它不想放弃事件处理,所以你的申请被拒绝了,后续输入事件不会传递给本组件进行处理。
3、如果组件申请成为了响应者,会执行如下回调方法:
- View.props.onResponderStart: (evt) => {}:表示手指按下时,成功申请为事件响应者的回调;
- View.props.onResponderMove: (evt) => {}:表示触摸手指移动的事件,这个回调可能非常频繁,所以这个回调函数的内容需要尽量简单;
- View.props.onResponderEnd: (evt) => {}:表示组件结束事件响应。
- View.props.onResponderRelease: (evt) => {}:表示触摸完成(touchUp)的时候的回调,表示用户完成了本次的触摸交互,这里应该完成手势识别的处理,这以后,组件不再是事件响应者,组件取消激活。
4、当组件处于响应者期间,并且其他组件也在申请触摸事件处理时,RN会询问是否可以释放响应让给其他组件:
- View.props.onResponderTerminationRequest: (evt) => bool
5、最终释放响应者的角色后,回调onResponderTerminate
,通知组件事件响应处理被终止:
可能是由于其他 View 通过onResponderTerminationRequest请求的,也可能是由操作系统强制夺权(比如 iOS 上的控制中心或是通知中心)
- View.props.onResponderTerminate: (evt) => {}
示例代码:
import React, {Component} from 'react';
import {
StyleSheet,
View,
Button,
Text
} from 'react-native';
export default class GestureScreen extends Component {
constructor(props) {
super(props);
this.state = {
backgroundColor: 'blue'
};
this.gestureHandlers = {
onStartShouldSetResponder: () => true,
onMoveShouldSetResponder: () => true,
onResponderStart:() => {
console.log('onResponderStart')
},
onResponderGrant: () => {
console.log('onResponderGrant');
this.setState({backgroundColor: 'green'})
},
onResponderMove: () => {
console.log('onResponderMove')
},
onResponderEnd:() => {
console.log('onResponderEnd')
},
onResponderRelease: () => {
console.log('onResponderRelease')
this.setState({backgroundColor: 'blue'})
},
onResponderTerminationRequest: () => {
console.log('onResponderTerminationRequest');
return true;
},
onResponderTerminate: (evt) => {
console.log('onResponderTerminate')
}
};
}
render() {
return (
<View style={styles.container}>
<View
{...this.gestureHandlers}
style={[styles.gesview,{backgroundColor:this.state.backgroundColor}]}>
<Text style={styles.text}>View 1</Text>
</View>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
backgroundColor:'rgb(236,236,236)'
},
gesview:{
width:160,
height:80,
borderRadius:20,
borderColor:'red',
borderWidth:2,
alignItems:'center',
justifyContent:'center',
},
text:{
color:'white',
fontSize:20,
fontWeight:'bold'
}
});
复制代码
执行这段代码,responderGrant
响应,中间绿色的View变为蓝色,responderRelease
响应,重新变为绿色; 点击一次View,然后离开屏幕,调用顺序:
- 1、onResponderGrant
- 2、onResponderStart
- 3、onResponderEnd
- 4、onResponderRelease
2.2嵌套组件触摸事件处理
如果父View A中嵌套有一个子组件View B,
正常的情况,当用户触摸View B时,子组件会显示:B 子组件 被点击
,父View则不会有反应;当点击View A时,View A会显示:A 组件 被点击
,子View B没有反应,以保证视图都能够正常处理触摸事件。
示例
import React, {Component} from 'react';
import {
StyleSheet,
View,
Button,
Text
} from 'react-native';
export default class GestureScreen extends Component {
constructor(props) {
super(props);
this.state = {
text1: 'A 父组件',
text2:'B 子组件'
};
this.gestureHandlers = {
onStartShouldSetResponder: () => true,
onMoveShouldSetResponder: () => true,
onResponderStart: () => {
console.log('onResponderStart')
},
onResponderGrant: () => {
console.log('onResponderGrant');
this.setState({text1: 'A 父组件 被点击'})
},
onResponderMove: () => {
console.log('onResponderMove')
},
onResponderEnd: () => {
console.log('onResponderEnd')
},
onResponderRelease: () => {
console.log('onResponderRelease')
this.setState({text1:'A 父组件'})
},
};
this.gestureHandlers2 = {
onStartShouldSetResponder: () => true,
onMoveShouldSetResponder: () => true,
onResponderStart: () => {
console.log('onResponderStart---->2')
},
onResponderGrant: () => {
console.log('onResponderGrant---->2');
this.setState({text2: 'B 子组件 被点击'})
},
onResponderMove: () => {
console.log('onResponderMove---->2')
},
onResponderEnd: () => {
console.log('onResponderEnd---->2')
},
onResponderRelease: () => {
console.log('onResponderRelease---->2')
this.setState({text2: 'B 子组件'})
},
};
}
render() {
return (
<View style={styles.container}>
<View
{...this.gestureHandlers}
style={styles.gesview}>
<Text style={styles.text}>{this.state.text1}</Text>
<View
style={styles.sonview}
{...this.gestureHandlers2}>
<Text style={styles.text}>{this.state.text2}</Text>
</View>
</View>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'rgb(236,236,236)'
},
gesview: {
width: 200,
height: 200,
backgroundColor:'#B34D76',
},
text: {
color: 'white',
fontSize: 20,
fontWeight: 'bold'
},
sonview: {
width:100,
height:100,
backgroundColor:'#33AECC',
alignSelf:'center',
marginTop:20,
}
});
复制代码
但是,在 RN 中,默认情况下使用冒泡机制,响应最深的组件最先开始响应。所以当多个 View 同时在*ShouldSetResponder中返回 true 时,最底层的 View 将最先响应。 RN 提供了一个劫持机制,在触摸事件往下传递的时候,先询问父组件是否需要劫持,不给子组件传递事件,也就是如下两个回调:
View.props.onStartShouldSetResponderCapture: (evt) => true
View.props.onMoveShouldSetResponderCapture: (evt) => true
我们设置View A的以上两个属性为true时,效果:
可见点击B子View时,A父View显示被点击,点击A View,依然是A被点击;B的事件被父View拦截并处理,并且不再继续传递。
2.3事件数据结构
触摸事件处理的回调都有一个 evt 参数,包含一个触摸事件数据 nativeEvent。nativeEvent 的详细内容如下
- changedTouches - 在上一次事件之后,所有发生变化的触摸事件的数组集合(即上一次事件后,所有移动过的触摸点)
- identifier - 触摸点的 ID
- locationX - 触摸点相对于当前元素的横坐标
- locationY - 触摸点相对于当前元素的纵坐标
- pageX - 触摸点相对于根元素的横坐标
- pageY - 触摸点相对于根元素的纵坐标
- target - 触摸点所在的元素 ID
- timestamp - 触摸事件的时间戳,可用于移动速度的计算
- touches - 当前屏幕上的所有触摸点的集合
3、PanResponder
RN提供了内置的手势识别库PanResponder,它封装了上面的事件回调函数,对触摸事件数据进行加工,完成滑动手势识别: PanResponder的每个方法,除了第一个evt参数之外,还可以使用第二个参数gestureState,gestureState是一个对象,包含手势进行过程中更多的信息
3.1 一个gestureState对象有如下的字段:
- stateID - 触摸状态的 ID。在屏幕上有至少一个触摸点的情况下,这个 ID 会一直有效。
- moveX - 最近一次移动时的屏幕横坐标
- moveY - 最近一次移动时的屏幕纵坐标
- x0 - 当响应器产生时的屏幕坐标
- y0 - 当响应器产生时的屏幕坐标
- dx - 从触摸操作开始时的累计横向路程
- dy - 从触摸操作开始时的累计纵向路程
- vx - 当前的横向移动速度
- vy - 当前的纵向移动速度
- numberActiveTouches - 当前在屏幕上的有效触摸点的数量
3.2 PanResponder.create()方法中,可配置的属性:
onMoveShouldSetPanResponder: (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) => {...}
onShouldBlockNativeResponder: (e, gestureState) => {...}
复制代码
3.3 使用PanResponder,简单实现一个拖拽视图:
import React, {Component} from 'react';
import {
StyleSheet,
View,
PanResponder
} from 'react-native';
export default class GestureScreen extends Component {
constructor(props) {
super(props);
this.state = {
top:0,
left:0
};
this.panResponder = PanResponder.create({
onStartShouldSetPanResponder: () => true,
onMoveShouldSetPanResponder: ()=> true,
onPanResponderGrant: ()=>{
this.top = this.state.top;
this.left = this.state.left;
},
onPanResponderMove: (evt,gestureState)=>{
this.setState({
top: this.top+gestureState.dy,
left: this.left+gestureState.dx
})
},
onPanResponderRelease: (evt,gestureState)=>{
this.setState({
top: this.top+gestureState.dy,
left: this.left+gestureState.dx
})}
})
}
render() {
return (
<View style={styles.container}>
<View
{...this.panResponder.panHandlers}
style={[styles.dragview,{
top: this.state.top,
left: this.state.left
}]}/>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
dragview: {
width: 100,
height: 100,
backgroundColor:'red',
borderRadius:50,
}
});
复制代码
4、总结
通过上面的介绍,可以看到 RN 中提供了类似 Native 平台的事件处理机制,所以也可以实现各种的触摸事件处理,甚至也可以实现复杂的手势识别。
另外需要注意,因为 RN 的异步通信和执行机制,前面描述的所有回调函数都是在 JS 线程中,并不是 Native 的 UI 线程,而 Native 平台的 Touch 事件都是在 UI 线程中。所以在 JS 中通过 Touch 或者手势实现动画,可能会延迟的问题。
本文是浅析,介绍比较简单。可能存在诸多错误和不足之处,欢迎交流。