React Native Gesture System 浅析
触摸事件响应机制
在App中从触摸开始、移动到触摸结束或取消,Native端都会传递一个Event 对象到RN端,用于描述触摸的各个不同阶段。rn端接收到事件后,会通过rn端自己的手势响应系统进行处理。
在RN端,当一个触摸事件开始时,会进入事件的捕获阶段,根据点击的视图区域,找到能响应事件的最深的子组件,然后从子组件开始冒泡事件(子组件可以判断是否成为响应者响应事件,不响应则往上冒泡)。需要注意的是,一次事件只会有一个视图组件成为事件的响应者,然后触发对应的事件。
点击事件
父子组件嵌套点击
两个
Touchable
组件嵌套,如下:
<TouchableWithoutFeedback onPress={() => alert('Click view 111')}>
<View style={styles.view1}>
<Text>View 1</Text>
<TouchableWithoutFeedback onPress={() => alert('Click view 222')}>
<View style={styles.view2}>
<Text>View 2</Text>
</View>
</TouchableWithoutFeedback>
</View>
</TouchableWithoutFeedback>
点击父组件响应事件,打印'Click view 1'。然后点击子组件视图时,因为事件的冒泡机制,子组件会优先成为响应者响应事件,打印'Click View 2'。 因为一次触摸事件只有一个响应者,所以父组件事件不会再触发。
如果只想触发父组件的事件响应,可以设置View组件的pointerEvents属性为none,使子组件视图不能作为触控事件的目标。
兄弟组件重叠点击
通过相对定位使两个兄弟
Touchable
组件部分重叠
<View>
<TouchableWithoutFeedback onPress={() => alert('Click view 111')}>
<View style={styles.view1}>
<Text>Touchable view 1</Text>
</View>
</TouchableWithoutFeedback>
<TouchableWithoutFeedback onPress={() => alert('Click view 222')}>
<View style={styles.view2}>
<Text>Touchable view 2</Text>
</View>
</TouchableWithoutFeedback>
</View>
点击各自
View
的区域时,分别触发各自的点击事件。但当点击重叠区域也只响应
Touchable view 2
事件,这是因为
View2
的视图层级更高,
View1
的视图被遮挡了,组件在这里是触摸不到的,所以
View1
事件不会被响应,可以通过
zIndex
改变组件层级使
View1
事件得到响应。
所以这里需要注意,在一些场景下为模块增加图层效果时,可能会导致模块内的点击事件都失效,如通过绝对定位为选中卡片加边框:
<View style={styles.wrapper}>
<TouchableWithoutFeedback
onPress={() => {
alert('select')
setIsSelect(true)
<View style={styles.button}>
<Text style={{ color: '#fff' }}>Select Card</Text>
</View>
</TouchableWithoutFeedback>
{isSelect && (
style={{ position: 'absolute', top: 0, left: 0, bottom: 0, right: 0, borderColor: 'red', borderWidth: 2 }}
</View>
选中卡片之后,为卡片增加边框选中效果,但是因为边框效果图层已经遮挡了整个卡片内容,这时点击卡片中的按钮将不再触发点击事件。这种情况可以修改按钮的
zIndex
样式,提升按钮组件的层级,使其可被触摸。
PanResponder
PanResponder
类可以将多点触摸操作协调成一个手势。它使得一个单点触摸可以接受更多的触摸操作,也可以用于识别简单的多点触摸手势。
PanResponder
类是对触摸系统
Responder
的封装,有着和
Responder
相同的生命周期(事件回调),只是回调函数增加了一个新的
gestureState
对象参数,提供更多手势过程的信息,详情的api解析可以查阅
官方文档
。
这里主要想说一下两个生命周期:
- onStartShouldSetPanResponderCapture: (evt, gestureState) => boolean: 事件捕获阶段执行,返回true表示停止事件捕获,子组件将不会触发任何事件,返回false则表示子组件可以捕获事件。
-
onStartShouldSetPanResponder: (evt, gestureState) => boolean
:事件冒泡阶段执行,返回true
表示组件成为事件响应者并终止事件冒泡,返回false
则不响应事件(后续的move和release事件都不会执行),然后把事件往上冒泡。
Click
假设已经有一个按钮卡片,需要在上面增加
双指点击
的效果事件。 这里我们使用
PanResponder
类把
View
组件变成可响应触摸事件的组件。
const doubleTapResponder = useRef(
PanResponder.create({
onStartShouldSetPanResponderCapture: evt => {
if (evt.nativeEvent.touches.length >= 2) {
return true
return false
onStartShouldSetPanResponder: () => {
return true
onPanResponderRelease: () => {
alert('Double Tap')
).current
return (
<View {...parentTapResponder.panHandlers}>
<TouchableOpacity onPress={() => alert('select')}>
<View style={styles.button}>
<Text style={{ color: '#fff' }}>Select Card</Text>
</View>
</TouchableOpacity>
</View>
当单指点击按钮时,触发按钮的onPress事件,打印Select。当双指点击按钮时,则触发外层View组件的点击事件。因为在View组件的
onStartShouldSetPanResponderCapture
回调中进行触摸点数量的判断,如果大于或等于
2
则返回
true
,阻止了子组件的事件捕获,所以
双指点击
卡片时,不会触发子组件的
onPress
事件。
Gesture
PanResponder
类配合动画可以实现一些手势效果,比如
双指滑动
放大图片。
const scale = useRef(new Animated.Value(1)).current
const panResponder = useRef(
PanResponder.create({
onStartShouldSetPanResponder: () => true,
onPanResponderMove: evt => {
const touches = evt.nativeEvent.touches
if (touches.length === 2) {
const touch1 = touches[0]
const touch2 = touches[1]
const distance = twoPointsDistance([touch1.pageX, touch1.pageY], [touch2.pageX, touch2.pageY])
const movePercent = distance / Dimensions.get('window').width
scale.setValue(1 + movePercent * 3)
).current
return (
<Animated.Image
{...panResponder.panHandlers}
style={[styles.image, { transform: [{ scale }] }]}
source={{
uri: imageUrl
性能
在React Native架构中,JavaScript代码是跑在一个单独的线程叫JS线程,而Native(Android/IOS)的渲染逻辑是跑在主线程叫UI线程,两者通过Bridge层进行通信,且通信都是异步的。
所以当用户触摸屏交互时,RN中默认情况下所有的更新都会延迟。而且交互过程中,JS 线程还需要执行应用的业务逻辑,处理网络请求等,通常情况下,都不能立即处理交互事件,会导致动画延迟丢帧等情况。如果是运行在性能较差的设备上,情况可能会更严重。
目前社区提出了解决方案,就是使用
react-native-gesture-handler
和
react-native-reanimated
代替现有的响应系统
Responder
和动画
Animated
。前者可以将动画和事件处理逻辑从JS线程转移到UI线程执行,实现同步更新UI,具体实现可以查看官方文档。
这里分别使用上述两个拖拽Demo进行比较:
代码地址:
Demo都加上提高程序负载的循环代码:
useEffect(() => {
setInterval(() => {
for (let i = 0; i < 1000; i++) {