ReactNative手势触摸事件解析

做ReactNative有一段时间了,今天系统的学习了一下RN的响应事件,事件分发,事件执行回调,之前有研究过Android 的事件分发原理,带着同样的思想来看RN的触摸事件处理,个人感觉更容易理解

用户的一次触摸操作的真实意图是什么,App 要经过好几个阶段才能判断。比如 App 需要判断用户的触摸到底是在滚动页面,还是滑动一个 widget,或者只是一个单纯的点击,亦或是拖拽。甚至随着持续时间的不同,这些操作还会转化。此外,还有多点同时触控的情况。

  作为与用户交互的第一层,触摸事件直接影响着用户行为体验。在Android 和 iOS 平台设备中,对于触摸机制做了非常完善的封装,能够很方便的帮助开发者处理基本的触摸行为操作,原生平台通过注册Listener的方式可以轻松的实现单击,双击等操作。在RN中同样提供了与Native触摸事件映射一致的处理方式,方便React Native开发者处理触摸行为,定义触摸操作。

当自定义跟手势相关比如拖拽效果的view时,是很有帮助的,今天就把学到的记录一下,用来以后可以温故而知新,哈哈

下面就由浅入深的记录

(一)控件加监听——Touchable系列

原生是使用setListener的方式监控控件,RN使用Touchable设置监听,除了Text和Button,其他组件默认是不支持点击事件,也不能响应基本触摸事件,但是RN给提供了几个处理点击事件的组件,基本上满足大部分的点击需求

 1.TouchableOpacity

  当按下的时候,封装的视图的不透明度会降低。(此组件与TouchableHighlight的区别在于并没有额外的颜色变化,更适于一般场景)

  设置按下效果的透明度activeOpacity设置值为0-1

2.TouchableHighlight

 当按下的时候,封装的视图的不透明度会降低,同时会有一个底层的颜色透过而被用户看到,使得视图变暗或变亮

 设置按钮按下的效果,onHideUnderlay 设置抬起,onShowUnderlay 设置按下

3.TouchableWithoutFeedback

 点击不显示任何视觉效果

4.TouchableNativeFeedback

安卓特有的效果,类似墨水涟漪的视觉效果

共通特性:都只支持一个子节点,如果希望包含多个子组件,用一个View来包装它们

继承了所有TouchableWithoutFeedback的属性。

总结一下这几个组件的功能和使用方法基本类似,只是就 Touch 的效果反馈上有所差异,他们有如下几个回调方法:

onPressIn:用户触摸开始的时候,也就是手指刚落在 Touch 点击区域内的时触发

onPressOut:用户触摸结束的时候,也就是手指从 Touch 点击区域内抬起的时触发

onPress:用户完成一次从 onPressIn 到 onPressOut 的过程,且时间很短,即一次快速点击操作时触发

onLongPress:用户触发 onPressIn 且手指一段时间内没有抬起时触发

拿TouchableHighlight为例,看一下

<TouchableHighlight
          onHideUnderlay={this._onHideUnderlay.bind(this)}
          onShowUnderlay={this._onShowUnderlay.bind(this)}
          underlayColor='transparent'
          onPress={(event) => console.log("=====onPress======")}
          onPressIn={() => console.log("=====onPressIn======")}
          onPressOut={() => console.log("=====onPressOut======")}
          onLongPress={() => console.log("=====onLongPress======")}>
          <View style={[styles.background, this.state.pressStatus ? { backgroundColor: "#32b9aa" } : { backgroundColor: '#F03D37' }]}>
            <Text style={[styles.text, this.state.pressStatus ? { color: '#ffffff' } : { color: "#000000" }]}>点击回调</Text>
          </View>
        </TouchableHighlight>

(1)手指按下快速弹起

          手指按下  ->  onPressIn,  手指弹起  ->  onPress onPressOut

 (2)手指长按弹起

          手指按下  ->  onPressIn,  手指长按  ->  onLongPress,   手指弹起  ->  onPress onPressOut

 (3)手指按下不弹起滑出view  -> onPressIn onPressOut

    (4) 手指长按不弹起滑出view -> onPressIn onLongPress onPressOut

    (5) 回调的顺序 onShowUnderlay > onPressIn > onPress|onLongPress > onHideUnderlay > onPressOut

  (二)事件处理流程

  事件分发机制是从根View依次向子view传递,根据捕获、分发等判断是哪个view获得响应事件

 1)在创建ReactRootView的时候,内部会创建一个JSTouchDispatcher实例,JSTouchDispatcher是事件分发的管理类,用来把事件处理传递给JS的方法处理,也就是当UI界面产生事件,就会执行JS的代码处理。

2)当有触摸事件的时候会执行ReactRootView的onInterceptTouchEvent、onTouchEvent方法

@Override
  public boolean onInterceptTouchEvent(MotionEvent ev) {
    dispatchJSTouchEvent(ev);
    return super.onInterceptTouchEvent(ev);
  }

  @Override
  public boolean onTouchEvent(MotionEvent ev) {
    dispatchJSTouchEvent(ev);
    super.onTouchEvent(ev);
    // In case when there is no children interested in handling touch event, we return true from
    // the root view in order to receive subsequent events related to that gesture
    return true;
  }
private void dispatchJSTouchEvent(MotionEvent event) {
    if (mReactInstanceManager == null
        || !mIsAttachedToInstance
        || mReactInstanceManager.getCurrentReactContext() == null) {
      FLog.w(
          ReactConstants.TAG,
          "Unable to dispatch touch to JS as the catalyst instance has not been attached");
      return;
    }
    if (mJSTouchDispatcher == null) {
      FLog.w(
          ReactConstants.TAG, "Unable to dispatch touch to JS before the dispatcher is available");
      return;
    }
    ReactContext reactContext = mReactInstanceManager.getCurrentReactContext();
    EventDispatcher eventDispatcher =
        reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher();
    mJSTouchDispatcher.handleTouchEvent(event, eventDispatcher);
  }

//EventDispatcher 负责将UI事件传到到JS的类。充当native代码生成事件和RN之间的中介

JSTouchDispatcher里的public void handleTouchEvent(MotionEvent ev, EventDispatcher eventDispatcher) 负责解析传入的MotionEvent事件,并把相应的内容包装成Event,然后调用EventDispatcher里的public void dispatchEvent(Event event)

3)EventDispatcher的public void dispatchEvent(Event event)

public void dispatchEvent(Event event) {
    Assertions.assertCondition(event.isInitialized(), "Dispatched event hasn't been initialized");

    for (EventDispatcherListener listener : mListeners) {
      listener.onEventDispatch(event);
    }

    synchronized (mEventsStagingLock) {
      mEventStaging.add(event);
      Systrace.startAsyncFlow(
          Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, event.getEventName(), event.getUniqueID());
    }
    maybePostFrameCallbackFromNonUI();
  }
private void maybePostFrameCallbackFromNonUI() {
    if (mReactEventEmitter != null) {
      // If the host activity is paused, the frame callback may not be currently
      // posted. Ensure that it is so that this event gets delivered promptly.
      mCurrentFrameCallback.maybePostFromNonUI();
    } else {
      // No JS application has started yet, or resumed. This can happen when a ReactRootView is
      // added to view hierarchy, but ReactContext creation has not completed yet. In this case, any
      // touch event dispatch will hit this codepath, and we simply queue them so that they
      // are dispatched once ReactContext creation completes and JS app is running.
    }
  }

依次跟代码,会调用到

EventDispatcher里的public void doFrame(long frameTimeNanos)

 @Override
    public void doFrame(long frameTimeNanos) {
      UiThreadUtil.assertOnUiThread();

      if (mShouldStop) {
        mIsPosted = false;
      } else {
        post();
      }

      Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "ScheduleDispatchFrameCallback");
      try {
        moveStagedEventsToDispatchQueue();

        if (!mHasDispatchScheduled) {
          mHasDispatchScheduled = true;
          Systrace.startAsyncFlow(
              Systrace.TRACE_TAG_REACT_JAVA_BRIDGE,
              "ScheduleDispatchFrameCallback",
              mHasDispatchScheduledCount.get());
          mReactContext.runOnJSQueueThread(mDispatchEventsRunnable);
        }
      } finally {
        Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE);
      }
    }

 重点是mReactContext.runOnJSQueueThread(mDispatchEventsRunnable) 做了一个线程切换,由UI线程切换到了JS线程,然后执行到了DispatchEventsRunnable的run函数里的event.dispatch(mReactEventEmitter);

mReactEventEmitter是通过mReactContext.getJSModule(RCTEventEmitter.class),mReactEventEmitter这个是一个JS module

通过mReactEventEmitter去分发,分发事件的本质:就是去执行JS的代码的相应事件

public interface RCTEventEmitter extends JavaScriptModule {
  void receiveEvent(int targetTag, String eventName, @Nullable WritableMap event);

  void receiveTouches(String eventName, WritableArray touches, WritableArray changedIndices);
}

4)这样就能完成把UI事件交给JS代码相应

上面是android的分发事件代码执行,下面来看一下js的
react-native/Libraries/EventEmitter /RCTEventEmitter.js会进行如下操作

const RCTEventEmitter = {
  register(eventEmitter: any) {
    BatchedBridge.registerCallableModule(
      'RCTEventEmitter',
      eventEmitter
    );
  }
};

react-native/Libraries/Renderer/implementation/ReactNativeRenderer-prod.js

/**
 * Register the event emitter with the native bridge
 */

ReactNativePrivateInterface.RCTEventEmitter.register({
  receiveEvent: receiveEvent,
  receiveTouches: receiveTouches
});

最终事件处理逻辑是在ReactNativeRenderer-prod.js这个文件中的receiveTouches函数


(三)单组件触摸事件分发RN

对于大部分交互来说,可以运用上述的四个touch组件进行实现,但是如果交互比较复杂,则需要引入React-Native的gesture responder system

RN中的组件默认是没有触摸事件的,如果一个组件想要处理触摸事件,需要“申请”成为触摸事件的响应者(Responder),完成事件处理以后,会释放响应者的角色。一个触摸事件处理周期,是从用户手指按下屏幕,到用户抬起手指抬起结束,这是用户的一次完整触摸操作。

 

1.当手指按下开始申请响应事件执行onStartShouldSetResponder 当返回true时

2.执行自身的onResponderGrant表示已经申请成功,组件成为了事件处理响应者,这时组件就开始接收后序的触摸事件输入。一般情况下,这时开始,组件进入了激活状态,并进行一些事件处理或者手势识别的初始化。

3.onResponderStart,表示手指按下时,成功申请为事件响应者的回调。

4.onResponderMove,表示触摸手指移动的事件,这个回调可能非常频繁,所以这个回调函数的内容需要尽量简单。

5.onResponderEnd,表示手指抬起组件结束事件响应的回调

6.onResponderRelease,表示触摸完成手指抬起的时候的回调,表示用户完成了本次的触摸交互,这里应该完成手势识别的处理,这以后,组件不再是事件响应者,组件取消激活

7.如果存在多个responder,已经激活了一个而且没释放时又想去激活另外一个,注意在一个ReactNative应用中只能存在一个responder。所以此时,就存在一个协商的过程。对于这种情况,React-Native提供了一个onResponderTerminationRequest方法。

1)已经激活的responder不愿意放弃主动权,此时onResponderTerminationRequest返回false,待激活的responder的onResponderReject方法会被调用,其保持不被激活的状态进行等待

2)已经激活的responder愿意放弃主动权,此时onResponderTerminationRequest返回true,待激活的responder的onResponderGrant方法会被调用变为激活状态,而之前激活的responder的onResponderTerminate方法会被调用,其被释放。

上述手势处理的回调都会回传一个参数GestureResponderEvent类型event参数,通过event可以获取到nativeEvent

nativeEvent可以获取到属性:
changedTouches - 在上一次事件之后,所有发生变化的触摸事件的数组集合(即上一次事件后,所有移动过的触摸点)
identifier - 触摸点的 ID
locationX - 触摸点相对于当前元素的横坐标
locationY - 触摸点相对于当前元素的纵坐标
pageX - 触摸点相对于根元素的横坐标
pageY - 触摸点相对于根元素的纵坐标
target - 触摸点所在的元素 ID
timestamp - 触摸事件的时间戳,可用于移动速度的计算
touches - 当前屏幕上的所有触摸点的集合

 

(四)多组件触摸事件分发

在实际开发过程中,肯定不仅仅处理单组件触摸事件行为,可能会涉及到多个组件间的交互,或者多层次的嵌套组件交互。同一时刻,只会存在一个响应者。多组件交互的场景很多,比如flatList上边有个view,这个时候手指滑动列表又不想让这个view影响到列表的滑动,这就会涉及到多个组件间共同申请成为响应者互斥的场景

PS:on*ShouldSetResponderCapture 表示 onStartShouldSetResponderCapture 和onMoveShouldSetResponderCapture

        onResponder*表示onResponderGrant,onResponderStart,onResponderMove,onResponderEnd,onResponderRelease

        on*ShouldSetResponder表示 onStartShouldSetResponder和onMoveShouldSetResponder

ViewA嵌套ViewB嵌套ViewC,分发从父View依次向子View传递

捕获期可通过onStartShouldSetResponderCapture 或 onMoveShouldSetResponderCapture回调决定是否阻止事件往下级组件传递。

冒泡期可通过onStartShouldSetResponder或onMoveShouldSetPanResponder回调决定是否成为响应者。若上级组件与下级组件都返回true,则下级组件成为当前触摸事件的响应者

 

(五)PanResponder

PanResponder是ReactNative提供的一套抽象方法,和gesture responder system比起来,其抽象程度更高,使用起来更加方便。可以将多点触摸操作协调成一个手势,它使得一个单点触摸可以接受更多的触摸操作,也可以用于识别简单的多点触摸手势,比如缩放view的时候使用这个很方便

它在基本的evt参数之外,还提供了另外一个参数gestureState。gestureState是一个对象,包含了以下信息:

stateID - 触摸状态的 ID。在屏幕上有至少一个触摸点的情况下,这个 ID 会一直有效。

moveX - 最近一次移动时的屏幕横坐标

moveY - 最近一次移动时的屏幕纵坐标

x0 - 当响应器产生时的屏幕坐标

y0 - 当响应器产生时的屏幕坐标

dx - 从触摸操作开始时的累计横向路程

dy - 从触摸操作开始时的累计纵向路程

vx - 当前的横向移动速度

vy - 当前的纵向移动速度

numberActiveTouches - 当前在屏幕上的有效触摸点的数量

 this.panResponder = PanResponder.create({
      // 要求成为响应者:
      onStartShouldSetPanResponder: (evt, gestureState) => true,
      onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
      onMoveShouldSetPanResponder: (evt, gestureState) => true,
      onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,

      onPanResponderGrant: (evt, gestureState) => {
        // 开始手势操作。给用户一些视觉反馈,让他们知道发生了什么事情!

        // gestureState.{x,y} 现在会被设置为0

      },
      onPanResponderStart: (evt, gestureState) => {
       
      },
      onPanResponderMove: (evt, gestureState) => {
        // 最近一次的移动距离为gestureState.move{X,Y}

        // 从成为响应者开始时的累计手势移动距离为gestureState.d{x,y}
        
      },
      onPanResponderTerminationRequest: (evt, gestureState) => true,
      onPanResponderRelease: (evt, gestureState) => {
        // 用户放开了所有的触摸点,且此时视图已经成为了响应者。
        // 一般来说这意味着一个手势操作已经成功完成。
      },
      onPanResponderTerminate: (evt, gestureState) => {
        // 另一个组件已经成为了新的响应者,所以当前手势将被取消。
      
      },
      onShouldBlockNativeResponder: (evt, gestureState) => {
        // 返回一个布尔值,决定当前组件是否应该阻止原生组件成为JS响应者
        // 默认返回true。目前暂时只支持android。
        
        return true;
      },
    })
 <View style={styles.panResponderView}
            {...this.panResponder.panHandlers}/>

PS:由于 RN 的异步通信和执行机制,前面描述的所有回调函数都是在 JS 线程中,而 Native 平台的 Touch 事件都是在 UI 线程中。所以在 JS 中通过 Touch 或者手势实现动画,可能会延迟的问题。

 

 

  • 5
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
React Native中的VirtualizedList组件是用于高效渲染大量数据的列表视图组件。虚拟化列表在滚动时只加载可见部分的数据,而非一次性渲染所有的列表项,从而提高了性能。 VirtualizedList组件的源码解析可以从以下几个方面进行: 1. 数据源:VirtualizedList接受一个名为data的props,用于表示要渲染的数据源。该数据源可以是一个数组或者是一个带有迭代方法的对象。在源码中,会使用this.props.data来获取传入的数据源。 2. 视图创建:在VirtualizedList组件的源码中,会通过ViewabilityHelper类型来管理可见项和滚动状态。ViewabilityHelper会根据滚动位置计算哪些列表项是可见的,并且会在必要时创建和删除列表项的视图。 3. 渲染性能优化:为了提高滚动的性能,VirtualizedList使用了windowSize属性,该属性定义了可见区域外额外渲染的列表项数量。源码中会根据滚动的位置,动态加载和卸载视图,以保证只渲染用户可见的列表项。 4. 列表项更新:当列表项的数据发生变化时,VirtualizedList会根据数据的变化更新相应的列表项。在源码中,会使用shouldItemUpdate()方法来判断列表项是否需要更新。 5. 交互处理:VirtualizedList会对滚动事件、滚动结束事件等进行处理,以便实现列表的滚动效果。在源码中,会使用onScroll()和onScrollEndDrag()等方法来处理相关交互事件。 总的来说,VirtualizedList作为React Native中高效渲染大量数据的列表视图组件,其源码实现了数据源管理、视图创建和销毁、渲染性能优化、列表项更新、交互处理等功能。通过深入源码的分析,可以更好地了解VirtualizedList组件的工作原理,并能更好地使用和定制该组件。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值