添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
React Native Gesture System 浅析

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进行比较:

代码地址:

  • Responder + Animated Demo
  • react-native-gesture-handler + react-native-reanimated Demo

Demo都加上提高程序负载的循环代码:

useEffect(() => {
    setInterval(() => {
      for (let i = 0; i < 1000; i++) {