Android---touch 事件分发

Android touch 事件分发,是 android 工程师必备技能之一。关于事件分发主要有几个方向可以展开深入分析:

\bullet touch 事件是如何从驱动层传递给 Framework 层的 InputManagerService;

\bullet WMS 是如何通过 ViewRoolmple 将事件传递到目标窗口;

\bullet touch 事件到达 DecorView 后,是如何一步步传递到内部子 View 中的。

与上层软件开发息息相关的就是最后一条。

主义:本文是基于 Android-28 的源码上进行分析。

ViewGroup

ViewGroup 是一组 View 的组合,在其内部可能包含多个子 View。当手指触摸屏幕时,手指所在的区域既能在 ViewGroup 显示范围内,也可能在其内部 View 控件上。因此,它内部事件分发的重心是处理当前 Group 和子 View 之间的关系:

1. 当前 Group 是否需要拦截 touch 事件;

2. 是否需要将 touch 事件继续分发给子 View;

3. 如何将 touch 事件分发给子 view。

View

View 是一个单独的控件,它的事件分发重点在于当前 View 如何去处理 touch 事件,并根据相应的手势逻辑进行一些列的效果展示(比如滑动、放大、点击和长按等)

1. 是否存在 TouchListener;

2. 是否自己接收处理 touch 事件(主要逻辑在 onTouchEvent 方法中)。

事件分发核心 dispatchTouchEvent

整个 View 之间的事件分发,实质上是一个大的递归函数,这个递归函数就是 dispatchTouchEvent方法。在这个递归的过程中会适时调用 onInterceptTouchEvent 来拦截事件,或者调用 onTouchEvent 方法来处理事件。

从宏观角度纵览一下 diapatchTouchEvent 方法源码,主要分为3个步骤

Android---touch 事件分发_第1张图片

步骤1:判断当前 ViewGroup 是否需要拦截事件。如果拦截,此次 touch 事件不会再传递给子 view。

步骤2:没有拦截,则将事件分发给子 view,继续处理。如果子 view 将此次事件捕获,则将 mFirstTouchTarget 复制给捕获 touch 事件的 view。

步骤3:根据 mFirstTouchTarget 重新分发事件。

接下来,详细的看一下每个步骤

步骤1的具体代码如下:

Android---touch 事件分发_第2张图片

图中,红框部分标出了是否需要拦截的判断条件。如果事件为 DOWN 事件,则调用 onInterceptTouchEvent 来进行拦截。或者 mFirstTouchTarget != null,代表已经有了子 view 捕获了这个事件。子 view 的 dispatchTouchEvent 返回 true,就代表捕获了 touch 事件。

如果步骤1中并没有对当前事件拦截,则进入步骤2

步骤2的具体代码如下:

Android---touch 事件分发_第3张图片

图中 1 处表明事件主动分发的前提是事件为 DOWN 事件;图中 2 处遍历所有子 View;图中 3 处判断事件坐标是否在子 View 坐标范围内,并且子 View 并没有处在动画状态;图中 4 处调用 dispatcTransformedTouchEvent 方法将事件分发给子 View,如果子 View 捕获事件成功,则将 mFirstTouchTarget 复制给 View。

步骤3的具体代码如下:

Android---touch 事件分发_第4张图片

图中 1 处,如果此时 mFirstTouchTarget == null,说明在上述的事件分发中,并没有子 View 对事件进行捕获操作。这种情况下直接调用 dispatchTransformedTouchEvent() 方法,并传入 child 为 null,最终会调用 super.dispatchTouchEvent 方法。实际上最终会调用自己的 onTouchEnent 方法进行处理 touch 事件。即,如果没有子 View 捕获处理 touch 事件,ViewGroup 会通过自身的 onTouchEvent 方法进行处理。图中 2 处,mFirstTouchTarget != null,说明上面步骤2中有子 view 对 touch 事件进行了捕获,则将当前以及后续的事件之间交给 mFirstTouchTarget 指向的 view 直接处理。

为什么 Down 事件特殊

1)所有 Touch 事件都是从 Down 事件开始的。这时 DOWN 事件比较特殊的原因之一;

2)DOWN 事件的处理结果会直接影响后续 MOVE、UP 事件的逻辑。

在步骤 2 中,只有 DOWN 事件会传递给子 View 进行捕获判断。一但子 View 捕获成功,后续的 MOVE 和 UP 事件是通过遍历 mFirstTouchTarget 链表,查找之前接受 ACTION_DOWN 的子 View,并将触摸事件分配给这些子 View。后续的 MOVE、UP 等事件的分发交给谁,取决于它们的起始事件 Down 是由谁捕获的

mFirstTouchTarget 有什么作用

mFirstTouchTarget 部分源码如下

Android---touch 事件分发_第5张图片

可以看出,mFirstTouchTarget 是一个 TouchTarget 链表结构。而这个 TouchTarget 的作用就是用来记录捕获 Down 事件的 View。具体保存在上图中的 child 变量中。

为什么是链表类型的结构呢?

因为 android 设备是支持多指操作的,每个手指的 Down 事件都可以当作一个 TouchTarget 保存起来,在步骤3中判断,如果 mFirstTouchTarget != null,则再次将事件分发给相应的 TouchTarget。

容易被遗漏的 CANCEL 事件

在上面的步骤3中,继续向子 View 分发事件的代码,有一段比较有趣的逻辑,如下

Android---touch 事件分发_第6张图片

 上图红框中表明已经有了子 View 捕获了 touch 事件,但是蓝色框中的 intercepted 变量又是 true。这种情况下,事件主导权重新回到父视图,ViewGroup 中。并在传递给子 View 的分发事件中,传递了一个 cancelChild == true

再看一下 dispatchTransformedTouchEvent 的部分源码,如下

Android---touch 事件分发_第7张图片

因为之前传入的参数 cancel 为 true,并且 child != null。最终这个事件会被包装为一个 ACTION_CANCEL 事件传递给 child。

什么情况下会触发这段逻辑呢?

当父视图的 onInterceptTouchEvent 先返回 false,

然后在子 View 的 dispatchTouchEvent 中返回 true(表示子 View 捕获事件)。

关键步骤就是在接下来的 MOVE 过程中

父视图的 onInterceptTouchEvent 又返回 true,intercepted 被重新置为 true,

此时上述逻辑就会触发,子控件就会收到 ACTION_CANCEL 的 touch 事件。

总结

重点分析了 dispatchTouchEvent 事件的流程机制:

\bullet 判断是否需要拦截 --> 主要是根据 onInterceptTouchEvent 方法的返回值来决定是否拦截。

\bullet 在 DOWN 事件中将 touch 事件分发给子 View --> 这一过程如果有子 View 捕获消费了 touch 事件,会对 mFirstTouchTarget 进行赋值;

\bullet DOWN、MOVE、UP事件都会根据 mFirstTouchTarget 是否为 null 决定是自己处理 touch 事件,还是再次分发给子 View。

介绍了整个事件分发中的几个特殊的点:

\bullet DOWN 事件的特殊之处:事件的起点;决定后续事件由谁来消费处理;

\bullet mFirstTouchTarget 的作用:记录捕获消费 touch 事件的 View,是一个链表结构;

\bullet CANCEL 事件的触发场景:当父视图先不拦截,然后在 MOVE 事件中重新拦截,此时子 View 会接收到一个 CANCEL 事件。

你可能感兴趣的:(#,Android进阶,android)