加入收藏 | 设为首页 | 会员中心 | 我要投稿 李大同 (https://www.lidatong.com.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 百科 > 正文

React-Native系列Android——Touch事件原理及状态效果

发布时间:2020-12-15 03:34:59 所属栏目:百科 来源:网络整理
导读:Native 原生相比于 Hybrid 或 H5 最大优点是具有流畅和复杂的交互效果,触摸事件便是其中重要一项,包括点击( Click )、长按( LongClick )、手势( gesture )等。 以最简单常见的点击( Click )为例, Native 组件可以自定义 selector ,使得被点击的

Native原生相比于HybridH5最大优点是具有流畅和复杂的交互效果,触摸事件便是其中重要一项,包括点击(Click)、长按(LongClick)、手势(gesture)等。

以最简单常见的点击(Click)为例,Native组件可以自定义selector,使得被点击的组件具有动态效果,Android 5.0以上甚至可以有涟漪效果(Material Design)。而这些在HybridH5中很难实现,很多时候区分它们与原生最简单的方法就是检验点击交互效果。

React-Native的强大之处在于实现了较为全面的Touch事件机制,虽然仍略有缺陷,但相比于HybridH5的体验而言,已经足足提高了一大截,下面分析讲解一下其实现原理,和具体使用方式。


1、Touch事件机制

如果阅读过React-Native源码的话,应该了解React-Native页面的UI根视图是ReactRootView,包路径是:com.facebook.react.ReactRootView,它是FramLayout的一个子类。

首先,来看一下ReactActivity这个页面基类,ReactRootView是如何作为React-Native的根视图被初始化及添加的。

<code class="language-java hljs  has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro',monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-keyword" style="color: rgb(0,136); box-sizing: border-box;">public</span> <span class="hljs-keyword" style="color: rgb(0,136); box-sizing: border-box;">abstract</span> <span class="hljs-class" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0,136); box-sizing: border-box;">class</span> <span class="hljs-title" style="box-sizing: border-box; color: rgb(102,102);">ReactActivity</span> <span class="hljs-keyword" style="color: rgb(0,136); box-sizing: border-box;">extends</span> <span class="hljs-title" style="box-sizing: border-box; color: rgb(102,102);">Activity</span> <span class="hljs-keyword" style="color: rgb(0,136); box-sizing: border-box;">implements</span> <span class="hljs-title" style="box-sizing: border-box; color: rgb(102,102);">DefaultHardwareBackBtnHandler</span> {</span>

...

  <span class="hljs-keyword" style="color: rgb(0,136); box-sizing: border-box;">protected</span> ReactRootView <span class="hljs-title" style="box-sizing: border-box;">createRootView</span>() {
    <span class="hljs-keyword" style="color: rgb(0,136); box-sizing: border-box;">return</span> <span class="hljs-keyword" style="color: rgb(0,136); box-sizing: border-box;">new</span> ReactRootView(<span class="hljs-keyword" style="color: rgb(0,136); box-sizing: border-box;">this</span>);
  }

  <span class="hljs-annotation" style="color: rgb(155,133,157); box-sizing: border-box;">@Override</span>
  <span class="hljs-keyword" style="color: rgb(0,136); box-sizing: border-box;">protected</span> <span class="hljs-keyword" style="color: rgb(0,136); box-sizing: border-box;">void</span> <span class="hljs-title" style="box-sizing: border-box;">onCreate</span>(Bundle savedInstanceState) {
    <span class="hljs-keyword" style="color: rgb(0,136); box-sizing: border-box;">super</span>.onCreate(savedInstanceState);

    <span class="hljs-keyword" style="color: rgb(0,136); box-sizing: border-box;">if</span> (getUseDeveloperSupport() && Build.VERSION.SDK_INT >= <span class="hljs-number" style="color: rgb(0,102,102); box-sizing: border-box;">23</span>) {
      <span class="hljs-comment" style="color: rgb(136,0); box-sizing: border-box;">// Get permission to show redbox in dev builds.</span>
      <span class="hljs-keyword" style="color: rgb(0,136); box-sizing: border-box;">if</span> (!Settings.canDrawOverlays(<span class="hljs-keyword" style="color: rgb(0,136); box-sizing: border-box;">this</span>)) {
        Intent serviceIntent = <span class="hljs-keyword" style="color: rgb(0,136); box-sizing: border-box;">new</span> Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
        startActivity(serviceIntent);
        FLog.w(ReactConstants.TAG,REDBOX_PERMISSION_MESSAGE);
        Toast.makeText(<span class="hljs-keyword" style="color: rgb(0,136); box-sizing: border-box;">this</span>,REDBOX_PERMISSION_MESSAGE,Toast.LENGTH_LONG).show();
      }
    }

    mReactInstanceManager = createReactInstanceManager();
    ReactRootView mReactRootView = createRootView();
    mReactRootView.startReactApplication(mReactInstanceManager,getMainComponentName(),getLaunchOptions());
    setContentView(mReactRootView);
  }

...

}</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221,221,221); list-style: none; text-align: right; background-color: rgb(238,238,238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li><li style="box-sizing: border-box; padding: 0px 5px;">13</li><li style="box-sizing: border-box; padding: 0px 5px;">14</li><li style="box-sizing: border-box; padding: 0px 5px;">15</li><li style="box-sizing: border-box; padding: 0px 5px;">16</li><li style="box-sizing: border-box; padding: 0px 5px;">17</li><li style="box-sizing: border-box; padding: 0px 5px;">18</li><li style="box-sizing: border-box; padding: 0px 5px;">19</li><li style="box-sizing: border-box; padding: 0px 5px;">20</li><li style="box-sizing: border-box; padding: 0px 5px;">21</li><li style="box-sizing: border-box; padding: 0px 5px;">22</li><li style="box-sizing: border-box; padding: 0px 5px;">23</li><li style="box-sizing: border-box; padding: 0px 5px;">24</li><li style="box-sizing: border-box; padding: 0px 5px;">25</li><li style="box-sizing: border-box; padding: 0px 5px;">26</li><li style="box-sizing: border-box; padding: 0px 5px;">27</li><li style="box-sizing: border-box; padding: 0px 5px;">28</li><li style="box-sizing: border-box; padding: 0px 5px;">29</li><li style="box-sizing: border-box; padding: 0px 5px;">30</li><li style="box-sizing: border-box; padding: 0px 5px;">31</li></ul>

ReactActivityonCreate这个生命周期里,直接实列化,然后作为当前WindowContentView,也就可以认为其是所有React-Native组件的根视图。

熟悉Android触摸事件机制的,应该知道视图树中,触摸事件是逐级传递的,每个视图(View)中有两个接收和处理Touch事件的方法,分别是onInterceptTouchEventonTouchEvent,这两个方法的区别为:

onInterceptTouchEvent的传递层级是由父视图向子视图,顾名思义,通常用作事件拦截。
onTouchEvent的传递层级是由子视图向父视图,通常用作事件处理。

我们来看一下ReactRootView的事件接收和处理。

 

很明显,这里onInterceptTouchEventonTouchEvent的处理都是全部交给handleTouchEvent方法统一处理的。

我们再继续看一下handleTouchEvent方法。

 

代码不是很多,也很好理解。先来看一下注释,意思是ReactRootView负责收集和发送事件给JS,当原生触摸事件响应时通过EventDispatcher类发送,并且在Down事件时通过TouchTargetManagerHelper查找具体被触摸的子View。这里一语道破了触摸事件的核心原理:

所有React组件的触摸事件都是由ReactRootView统一处理,将具体被触摸组件和具体触摸事件发送给Javascript。其中隐藏的一层意思是:React组件自身不用处理触摸事件。

这个很关键,而具体被处理的触摸事件有以下6种,分别是ACTION_DOWN、ACTION_UP、ACTION_MOVE、ACTION_POINTER_DOWN、ACTION_POINTER_UP、ACTION_CANCEL,已经包含了几乎所有的手势动作。


2、Touch事件接收者

接下来,看一下ACTION_DOWN事件时,是如何定位消费Touch事件React组件的呢?以下图为例:

如果黄色的点表示被触摸的事件点,由于Touch事件是由ReactRootView根节点开始拦截,所以从ReactRootView开始遍历视图树,遍历顺序如下:

1、ReactViewGroup,判断黄点坐标位于ReactViewGroup区域,再判断ReactViewGroup自身或其子视图是否消费Touch事件(通过PointerEvents枚举类,后面详解)。如果自身消费Touch事件,遍历中断,直接返回ReactGroupView;如果子视图消费Touch事件,继续遍历其子视图树;如果不消费Touch事件,返回null

2、child 1,如果ReactViewGroup的子视图消费Touch事件,则遍历至child 1,首先判断黄点坐标是否位于child 1区域,再判断自身或其子视图是否消费Touch事件。如果child 1ReactViewGroup类型,同上方1过程处理;如果child 1是非ReactViewGroup类型,即ImageViewTextView等非复合型视图,判断其自身是否消费Touch事件,一般除具有Span属性的TextView外,基本都是消费Touch事件的。如果消费Touch事件,返回child 1,如果不消费Touch事件,返回null

3、child 2,如果2child 1不消费Touch事件,继续遍历到child 2,由于触摸点黄点坐标不位于child 2区域内,遍历终止,返回null

关于视图是否消费Touch事件,通过一个枚举类来说明,代码位于com.facebook.react.uimanager.PointerEvents
一共有4种枚举类型:

NONE:视图自身或其子视图不消费Touch事件。
BOX_NONE:视图自身不消费Touch事件,但其子视图消费。
BOX_ONLY:视图自身消费Touch事件,而其子视图不消费。
AUTO:视图自身或其子视图消费Touch事件,但不确定是哪一个。

关于具体如何查找事件消费者的代码主要在com.facebook.react.uimanager.TouchTargetHelper中。

 

具体有三层逻辑:findTouchTargetViewfindClosestReactAncestorgetTouchTargetForView,最终是要返回目标ViewID,代码我们一一来看。

1、findTouchTargetView

 

循环遍历ReactRootView的视图树,通过isTransformedTouchPointInView方法判断触摸点坐标是否位于当前遍历子视图的区域内。有一点需要特别注意,就是坐标的处理。默认的触摸点坐标是基于ReactRootView的坐标系参照,如果遍历到子视图,需要将触摸点坐标转换成以子视图为坐标系参照的坐标。主要是通过上方代码中的childPoint变量保存和处理的。
触摸点坐标是否位于当前遍历子视图的区域内,通过findTouchTargetViewWithPointerEvents方法,判断当前遍历子视图是否消费Touch事件。

 

findTouchTargetViewWithPointerEvents方法对PointerEvents的四种枚举做了相应处理,NONE返回nullBOX_ONLY返回当前视图,BOX_NONEAUTO继续遍历,递归调用了findTouchTargetView

2、findClosestReactAncestor

 

由于查找最终是要返回目标视图的ID,如果目标视图的ID非法小于0,则返回其父视图作为替代。此处作用不是很理解,忘解答,感激不尽。

3、getTouchTargetForView

 

这个方法是针对ReactTextView做特殊处理的,由于ReactTextView中可能存在消费Touch事件的Span,如果有则返回其Spantag值(具体请阅读ReactTextViewReactTagSpan)。


3、Touch事件发送

代码位于com.facebook.react.uimanager.events.EventDispatcher中,先来看一下EventDispatcher对象的初始化。

 

EventDispatcher实现了LifecycleEventListener接口,在ReactActivity的各个生命周期执行时回调给EventDispatcher

onHostResume方法对应ActivityonResume生命周期,主要通过ReactChoreographer单例来post了一个ScheduleDispatchFrameCallback。而ReactChoreographer是对Choreographer的一层封装,这里可以直接看成是ChoreographerChoreographer是一个消息处理器,具体作用不多讲,请参考http://www.52php.cn/article/p-evfvxvhb-qx.html。

ScheduleDispatchFrameCallbackEventDispatcher的一个内部Choreographer.FrameCallback实现类。接下来看看ScheduleDispatchFrameCallback这个回调类里面处理了哪些东西。

 

doFrame的回调方法里面一共做了两件事情:
1、将mDispatchEventsRunnable塞进Javascript线程处理队列中,此线程队列内部有一个Looper消息队列并持有当前线程的Handler消息句柄,最终mDispatchEventsRunnable是通过此Handler塞进消息队列完成处理的。
2、递归调用,再次post了当前ScheduleDispatchFrameCallback,达到一个循环的目的。

接下里,我们看看mDispatchEventsRunnable是如何发送Touch事件的。

 

DispatchEventsRunnable这个对象的作用,只有一个:循环遍历mEventsToDispatch数组,然后调用eventdispatch方法发送给Javascript。这里涉及到RCTEventEmitter的一个JS组件类,里面有一个receiveTouches(String eventName,WritableArray touches,WritableArray changedIndices)的方法用来与JS交互的,这里不做深入分析,下一篇博客会以此为例详解,敬请关注!

梳理一下,也就是说所有的Touch事件都会预先存入mEventsToDispatch数组里,然后在每次ScheduleDispatchFrameCallback回调后,使用DispatchEventsRunnable最终将Touch事件传递给JS。

Touch事件如何预先存入mEventsToDispatch数组中,则是通过onInterceptTouchEvent->handleTouchEvent->dispatchEvent->moveStagedEventsToDispatchQueue->addEventToEventsToDispatch的流程运行的,里面还会有一个mEventStaging暂存的过程,比较简单,不再讲解。


4、Touch事件在React-Native中的使用

既然Javascript能够接收到原生native端的几乎所有Touch事件,那么就可以做出很多复杂的交互效果了,以点击(click)事件为例,演示下React-Native的几种交互效果。

4-1 普通触摸效果

点击文本,出现‘Awesome,Clicking!’的点击Toast提示,这是最简单和常用的点击功能,直接使用onPress属性实现。

<code class="hljs php has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro',monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-function" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0,136); box-sizing: border-box;">function</span> <span class="hljs-title" style="box-sizing: border-box;">onClick</span><span class="hljs-params" style="color: rgb(102,102); box-sizing: border-box;">()</span>{</span>
   <span class="hljs-keyword" style="color: rgb(0,136); box-sizing: border-box;">var</span> ToastAndroid = <span class="hljs-keyword" style="color: rgb(0,136); box-sizing: border-box;">require</span>(<span class="hljs-string" style="color: rgb(0,0); box-sizing: border-box;">'ToastAndroid'</span>)
   ToastAndroid.show(<span class="hljs-string" style="color: rgb(0,0); box-sizing: border-box;">'Awesome,Clicking!'</span>,ToastAndroid.SHORT);
}

<span class="hljs-class" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0,102);">MyProject</span> <span class="hljs-keyword" style="color: rgb(0,102);">Component</span> {</span>
  render() {
    <span class="hljs-keyword" style="color: rgb(0,136); box-sizing: border-box;">return</span> (
      <View style={styles.container}>
        <Text onPress={onClick}>Click Me!</Text>
      </View>
    );
  }
}</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221,238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li><li style="box-sizing: border-box; padding: 0px 5px;">13</li><li style="box-sizing: border-box; padding: 0px 5px;">14</li></ul>

4-2 变色触摸效果

使用TouchableHighlight组件实现,点击瞬间或者长按时,可以设定一个颜色视觉差。TouchableHighlight标签必须包裹被点击组件,使用underlayColor属性定义点击时的背景色,同时还有onShowUnderlayonHideUnderlay两个属性可以监听,背景色显示和隐藏瞬间的事件。

需要注意的一点是onPress属性,必须设置给TouchableHighlight

 

4-3 透明触摸效果

使用TouchableOpacity组件实现,点击瞬间或者长按时,可以设定一个透明度视觉差,一般用于点击图片时使用。使用方式同TouchableHighlight。设定透明度的属性是activeOpacity,如果不设置,默认值为0.2

 

4-4 原生触摸效果

使用TouchableNativeFeedback组件实现,点击瞬间或者长按时,呈现原生系统的点击效果。使用方式有点特殊,必须且只能包含一个节点,如果是Text这种多节点组件,必须在外面包一层View节点。而且这个功能目前并不完善,快速点击时并不会出现原生点击效果,只有较长时间按住时才正常。

 

4-5 无反馈触摸效果

使用TouchableWithoutFeedback组件实现,表示触摸时无任何反馈效果(同4-1),使用方式同TouchableHighlight。facebook官方并不推荐使用这个组件,除非你有特殊的原因。

                        

(编辑:李大同)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    推荐文章
      热点阅读