简介
本篇主要对事件分发中的基本概念做一下简单介绍,同时也介绍一下参与事件分发的主要方法。从这些方法的核心逻辑中结合源码,总结事件分发的原理。
注意:不同版本间的代码会有区别,本文是基于Android-28的源码进行分析
思路分析
touch
事件是如何从驱动层传递给Framework
层的InputManagerService
;WMS
是如何通过ViewRootImpl
将事件传递到目标窗口;touch
事件到达DecorView
后, 又是如何一步步传递到内部的子View中的。
接下来针对上述思路进行知识整理。
分发对象
首先了解被分发的对象到底是哪些?这些被分发的对象就是用户触摸屏幕而产生的的点击事件,事件主要包括:按下、滑动、抬起、取消。这些事件被封装成MotionEvent
对象。该对象的主要事件如下表所示:
事件 | 触发场景 | 单次事件流中触发的次数 |
---|---|---|
MotionEvent.ACTION_DOWN | 在屏幕按下时 | 1次 |
MotionEvent.ACTION_MOVE | 在屏幕滑动时 | 0次或多次 |
MotionEvent.ACTION_UP | 在屏幕抬起时 | 0次或1次 |
MotionEvent.ACTION_CANCEL | 滑动超出控件边界时 | 0次或1次 |
按下、滑动、抬起、取消这几种事件组成了一个事件流。事件流以按下为开始,中间可能有若干次滑动,以抬起或者取消作为结束。
在Android对事件分发的处理过程中,主要是对按下事件做分发,进而找到能够处理按下事件的控件。对于事件流中后续的事件(如滑动、抬起等),则直接分发给能够处理按下事件的组件。
分发事件的组件
分发事件的组件,也称为分发事件者,这些包括Activity、View、ViewGroup,三者的一般结构为:
从图上可看出,Activity包括了ViewGroup
,ViewGroup
又可以包含多个View
组件 | 特点 | 示例 |
---|---|---|
Activity | Android视图类 | 如MainActivity |
ViewGroup | View的容器,可以包含若干个View | 各种布局类 |
View | UI类组件的基类 | 如按钮、文本框 |
ViewGroup
ViewGroup 是一组View的组合,在其内部有可能包含多个子View,当手指触摸屏幕时,手指所在区域既能在ViewGroup显示范围内,也可能在其内部View控件上。
因此该组件内部的事件分发是处理当前 Group 和子 View 之间的逻辑关系:
1. 当前 Group 是否需要拦截 touch 事件
2. 是否需要将 touch 事件继续分发给子View
3. 如何将 touch 事件 分发给 子View
View
View 是页面上能呈现的最小组件单元,不可再细分,内部也不会存在子 View,所以该组件的事件分发重点在于当前 View 如何去处理 touch 事件,并根据相应的手势逻辑进行一系列的效果展示(比如滑动、放大、点击、长按等)。
所以这里面需要处理一些逻辑关系是:
1. 是否存在 TouchListener;
2. 是否自己接收处理 touch 事件(主要逻辑在 onTouchEvent 方法中)。
分发的核心方法
负责对事件进行分发的方法主要有三个,分别是:
|
它们并不是全都存在于所负责分发的组件中,具体情况分析如下表中:
组件 | dispatchTouchEvent | onTouchEvent | onInterceptTouchEvent |
---|---|---|---|
Activity | 存在 | 存在 | 不存在 |
ViewGroup | 存在 | 存在 | 存在 |
View | 存在 | 存在 | 不存在 |
从表中可看出,dispatchTouchEvent
和 onTouchEvent
方法存在于上述三个组件中,而 onInterceptTouchEvent
单独为ViewGroup
所拥有。
ViewGroup
类中,实际上是没有 onTouchEvent
方法,但是由于其继承自View
,而后者是拥有onTouchEvent
方法,因此ViewGroup
也是可以调用onTouchEvent
方法的。
事件分发过程
这一章节是本文的核心,具体流程介绍如下。
分发方法:dispatchTouchEvent
从方法名可看出该方法的主功能就是负责分发,这是Android事件分发过程中的核心。事件是如何传递的,主要是看该方法,理解此方法,也就吃透了Android事件分发机制。
在了解该方法的核心机制之前,需要知道这样一个结论:
如果某个组件的该方法返回
TRUE
,则表示该组件已对该事件进行了处理,即不会继续调用其他组件的事件分发方法,也就停止了分发。如果某个组件的该方法返回
FALSE
,则表示该组件不能对该事件进行处理,需要按照规则继续分发事件,在不覆写该方法的情况下,除了一些特殊组件(系统已做过处理组件),其余组件默认都是返回false
。
结合源码分析核心逻辑
// Activity 的 dispatchTouchEvent 方法 |
这里 Activity
并没有通过自身来判断是否有子View消费分发事件,而是交给它依附的Window对象来处理,但是在Window对象里,该方法是一个抽象方法,实际上它交给了它的具体对象 PhoneWindow
处理的;
PhoneWindow核心代码:
|
DecorView核心代码
public boolean superDispatchTouchEvent(MotionEvent event) { |
这也很好理解,DecorView
就是一个ViewGroup
,因此,整个流程串通起来即可看出,当事件传递给Activity
后,它先将事件分发给子View处理。
如果经过子View层层传递或者处理后,该事件被消费了(即返回
TRUE
),则Activity的分发方法也返回TRUE
,同样也表示该事件已经被消费了。如果经过子View层层传递或者处理后,该事件没有被消费(即返回
FALSE
),则Activity的分发方法就会返回FALSE
,接着调用自身的onTouchEvent
去处理。如果
onTouchEvent
消费了该事件,那依然能返回TRUE
(表示已消费该事件),这个TRUE
作为dispatchTouchEvent
的返回值,让调用它的对象知道该Activity
已经消费了事件。如果
onTouchEvent
没有消费该事件,则会返回FALSE
(表示未消费该事件),这个FALSE
作为dispatchTouchEvent
的返回值,让调用它的对象知道该Activity
并没有消费事件,需要继续处理。
接下来看ViewGroup 关于 dispatchTouchEvent
方法的核心逻辑代码(去掉非重点代码)
|
dispatch主要分为3大步骤:
步骤1:判断当前 ViewGroup 是否需要拦截此 touch 事件,如果拦截则此次 touch 事件不再传递给子 View(或者以 CANCEL 的方式通知子 View)。
步骤2:如果没有拦截,则将事件分发给子 View 继续处理,如果子 View 将此次事件捕获,则将
mFirstTouchTarget
赋值给捕获 touch 事件的 View。步骤3:根据
mFirstTouchTarget
重新分发事件。
如果步骤1中,当前 ViewGroup 并没有对事件进行拦截,则执行步骤2
事件分发流程demo演示
上图 DOWN 事件中,DownInterceptGroup
的 onInterceptTouchEvent
被触发一次;
然后在子 View CaptureTouchView
的 dispatchTouchEvent
中返回 true,代表它消费了这个 DOWN 事件。
这样 CaptureTouchView
会被添加到父视图(DownInterceptGroup
)中的 mFirstTouchTarget
中。
因此后续的 MOVE 和 UP 事件都会经过 DownInterceptGroup
的 onInterceptTouchEvent
进行拦截判断
为什么 DOWN 事件特殊
所有 touch 事件都是从 DOWN 事件开始的,这是 DOWN 事件比较特殊的原因之一。另一个原因是 DOWN 事件的处理结果会直接影响后续 MOVE、UP 事件的逻辑。
在步骤2中,只有 DOWN 事件会传递给子 View 进行捕获判断,一旦子 View 捕获成功,后续的 MOVE 和 UP 事件是通过遍历 mFirstTouchTarget
链表,查找之前接受 ACTION_DOWN 的子 View,并将触摸事件分配给这些子 View。也就是说后续的 MOVE、UP 等事件的分发交给谁,取决于他们的起始事件 DOWN 是由谁捕获的。
mFirstTouchTarget的作用
mFirstTouchTarget
的部分源码如下:
private static final class TouchTarget { |
可以看出 mFirstTouchTarget
是一个 链表 结构 TouchTarget
类型的对象。这个对象的作用就是用来记录捕获了 DOWN 事件的 View,具体是保存在它的成员变量 child 变量中。
至于为什么是链表类型结构,是因为Android设备支持多指操作,每一个手指的 DOWN 事件都可以当做一个 TouchTarget
保存起来。在步骤3中判断 mFirstTouchTarget
不为null,则再次将事件分发给响应的 TouchTarget
。
小结
本文主要分析 dispatchTouchEvent
的事件流程机制,这一过程主要分3部分:
判断是否需要拦截 ——> 主要根据
onInterceptTouchEvent
方法的返回值来决定是否拦截;在 DOWN 事件中将 touch 事件分发给子 View ——> 这一过程如果有子 View 捕获消费了 touch 事件,则会对
mFirstTouchTarget
进行赋值最后,DOWN、MOVE、UP 事件都会根据
mFirstTouchTarget
是否为null,决定是自己处理 touch 事件,还是再次分发给子 View。
还有整个事件分发中一些特殊的注意点:
DOWN 事件的特殊:事件的起点;决定后续事件由谁来消费处理;
mFirstTouchTarget
作用:记录捕获消费 touch 事件的 View,是一个链表结构;CANCEL 事件的触发场景:当父视图先不拦截,然后再 MOVE 事件中重新拦截,此时子 View 会接收到一个 CANCEL 事件。