<
Touch事件分发时序
>
上一篇

自定义View
下一篇

Class对象初始化过程

学习Android Touch事件分发时序

Android Touch事件分发时序

关于事件分发主要有3个方向可以展开深入分析:

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

其中与上层开发相关的就是第3条,也是本次总结学习的重点内容,基于Android-28源码阅读学习

一、思路梳理,基本概念

首先需要弄清楚2个概念

1.ViewGroup

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

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

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

2.View

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

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

二、事件分发核心dispatchTouchEvent

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

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

1

如注释所言,dispatch主要分为3大步骤:

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

接下来具体看下各个步骤细节

1.判断当前ViewGroup是否需要拦截此touch事件

2

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

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

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

2.将事件分发给子View

3

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

3.根据mFirstTouchTarget再次分发事件

4

步骤3有2个分支判断:

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

三、事件分发流程代码演示

定义如下布局文件

5

DownInterceptedGroup和CaptureTouchView是两个自定义View,源码如下:

6

手指触摸CaptureTouchView并滑动一段距离后抬起,最终打印log如下:

7

上图中在Down事件中,DownInterceptedGroup的onInterceptTouchEvent被触发一次;然后在子View CaptureTouchView的dispatchTouchEvent中返回true,代表它捕获消费了这个Down事件。这种情况下CaptureTouchView会被添加到父视图(DownInterceptedGroup)中的mFirstTouchTarget中。因此后续的Move和Up事件都会经过DownInterceptedGroup的onInterceptTouchEvent进行拦截判断。

四、为什么DOWN事件特殊

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

在步骤2(将事件分发给子View)中,只有DOWN事件会传递给子View进行捕获判断,一旦子View捕获成功,后续的MOVE和UP事件是通过遍历mFirstTouchTarget链表,查找之前接受ACTION_DOWN的子View,并将触摸事件分配给这些子View。

也就是说后续的MOVE,UP等事件的分发交给谁,取决于他们的起始事件Down是由谁捕获的。

五、mFirstTouchTarget有什么作用

mFirstTouchTarget部分源码:

8

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

为什么是链表结构呢?因为Android设备是支持多指操作的,每一个手指的Down事件都可以当作一个TouchTarget保存起来。在步骤3(根据mFirstTouchTarget再次分发事件)中判断如果mFirstTouchTarget不为null,则再次将事件分发给相应的TouchTarget

六、容易被遗漏的CANCEL事件

在上面的步骤3(根据mFirstTouchTarget再次分发事件)中,继续向子View分发事件的代码中,有一段比较有趣的逻辑:

9

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

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

10

因为之前传入参数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焦点。

比如以下代码:

11

CaptureTouchView是一个自定义的View,源码如下:

12

CaptureTouchView的onTouchEvent返回true,表示它会将接收到的touch事件进行捕获消费

上述代码执行后,当手指点击屏幕时DOWN事件会被传递给CaptureTouchView,手指滚动屏幕将Scrollview上下滑动,刚开始MOVE事件还是由CaptureTouchView来消费处理,但是当Scrollview开始滚动时,CaptureTouchView会接收一个CANCEL事件,并不再接受后续的touch事件

13

因此,平时自定义View时,尤其是有可能被Scrollview或ViewPager嵌套使用的控件,不要遗漏对CANCEL事件的处理,否则可能引起UI显示异常!!!

总结

主要分析了dispatchTouchEvent的事件的流程机制,这一过程分3部分:

  1. 判断是否需要拦截->主要是根据onInterceptTouchEvent方法的返回值来决定是否拦截
  2. 在Down事件中将touch事件分发给子View->这一过程如果有子View捕获消费了touch事件,会对mFirstTouchTarget进行赋值
  3. 最后一步,DOWN,MOVE,UP事件都会根据mFirstTouchTarget是否为null,决定是自己处理touch事件,还是再次分发给子View

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

Top
Foot