彻底掌握Android touch事件分发顺序

彻底掌握Android touch事件分发顺序

Android touch事件的分发主要由几个方向可以展开深入分析:

  1. touch事件是如何从驱动层传递给Framework层的InputManagerService;
  2. WMS是如何通过ViewRootImpl将事件传递到目标窗口;
  3. touch事件到达DecorView后,是如何一步步传递到内部的子View中。

其中与上层软件开发息息相关的就是第三条。

思路梳理

在深入分析事件分发源码之前,需要先弄清楚2个概念。

ViewGroup

ViewGroup是一组View的组合,在其内部有可能包含多个子View,当手指触摸屏幕上时,手指所在的区域能在ViewGroup的显示范围内,也可能在其内部的View控件上。

因此它内部的事件分发的重心是处理当前Group和子View之间的逻辑关系。

  1. 当前Group是否需要拦截touch事件
  2. 是否需要将touch事件继续分发给子View;
  3. 如何将touch事件分发给子View。

View

View是一个单纯的控件,不能被再细分,内部也不会存在子Veiw,所以它的事件分发的重点在于当前View如何去处理touch事件,并根据相应的手势逻辑进行一系列的效果展示(比如滑动,放大,点击,长按)。

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

事件分发核心 dispatchTouchEvent

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

先从宏观角度,纵览整个dispatch的源码如下:

public boolean dispatchTouchEvent(){
    /**
     * 步骤1:检查当前ViewGroup是否需要拦截事件
     */
     ...
    /**
     *	步骤2:将事件分发给子View
     */
     ...
    /*
     * 步骤3:根据mFirstTouchTarget,再次分发事件
     */
     ...
}

如代码中的注释,dispatch主要分为3大步骤:

  • 步骤1:判断当前ViewGroup是否需要拦截此touch事件,如果拦截则此次touch事件不再会传递给子View(或者以CANCEL的方式通知子View
  • 步骤2:如果没有拦截,则将事件分发给子View继续处理,如果子View将此次事件捕获,则将mFirstTouchTarget赋值给捕获touch事件的VIew。
  • 步骤3:根据mFirstTouchTarget重新分发事件。

步骤1的具体代码如下:

在这里插入图片描述

图中红框标出了是否需要拦截的条件:

  • 如果事件为DOWN事件,则调用onInterceptTouchEvent进行拦截判断
  • 或者 mFirstTouchTarget 不为 null,代表已经有子 View 捕获了这个事件,子 View 的 dispatchTouchEvent 返回 true 就是代表捕获 touch 事件。

如果在上面步骤 1 中,当前 ViewGroup 并没有对事件进行拦截,则执行步骤 2。

步骤 2 具体代码如下:

在这里插入图片描述

仔细看上述代码可以看出:

  • 图中①处表名事件主动分发的前提是事件为DOWN事件;
  • 图中②处遍历所有子View;
  • 图中③处判断事件坐标是否在子View的坐标范围内,并且子View并没有处在动画状态;
  • 图中④处调用dispatchTransformedTouchEvent方法将事件分发给子View,如果子View捕获事件成功,则将mFirstTouchTarget赋值给子View。

步骤3具体代码如下:

在这里插入图片描述

步骤3有2个分支判断。

  • 分支1:如果此时mFirstTouchTarget为null,说明在上述的事件分发中并没有子View对事件进行了捕获操作。这种情况下,直接调用dispatchTransformedTouchEvent方法,并传入child为null,最终会调用super.dispatchTransformedTouchEvent方法。实际上最终会调用自身的onTouchEvent方法,进行处理touch事件。也就是说:如果没有子View捕获处理touch事件,ViewGroup会通过自身的onTouchEvent方法进行处理

为什么DOWN事件特殊?

所有的touch事件都是从DOWN事件开始的,这是DOWN事件比较特殊的原因之一。另一个原因就是DOWN事件的处理结果会直接影响到后续MOVE,UP事件的逻辑。

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

mFirstTouchTarget有什么作用?

mFirstTouchTarget的部分源码如下:

在这里插入图片描述

可以看出其实mFirstTouchTarget是一个TouchTarget类型的链表结构。而这个TouchTarget的作用就是用来记录捕获了DOWN事件的View,具体保存在上图中的child变量。可是为什么是链表类型的结构呢?因为Android设备是支持多指操作的,每一个手指的DOWN事件都可以当作一个TouchTarget保存起来。在步骤3中判断如果mFirstTarget不为null,则再次将事件分发给相应的TouchTarget。

容易被遗漏的CANCEL事件

在上面步骤3中,继续向子View分发事件的代码中,有一段比较有趣的逻辑:
在这里插入图片描述

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

看一下dispatchTransformedTouchEvent方法的部分源码如下:

在这里插入图片描述

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

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

总结一下就是:当父视图的onInterceptTouchEvent先返回false,然后在子View的dispatchTouchEvent中返回true(表示子View捕获事件),关键步骤就是在接下来的MOVE的过程中,父视图的onInterceptTouchEvent又返回true,intercepted被重新置为true,此时上述逻辑就会被触发,子控件就会收到ACTION_CANCEL的touch事件。

实际上有个很经典的例子可以用来演示这种情况:

当在ScrollView中添加自定义View时,ScrollView默认在DOWN事件中并不会进行拦截,事件会被传递给ScrollView内的子控件。只有当手指进行滑动并达到一定的距离之后,onInterceptTouchEvent方法返回true,并触发ScrollView的滚动效果。当ScrollView进行滚动的瞬间,内部的子View会接收到一个CANCEL事件,并丢失touch焦点。

猜你喜欢

转载自blog.csdn.net/qq_43621019/article/details/106027711
今日推荐