前言
一个 App 中用户的交互设计是必不可少的,通过用户的交互,可以触发多种多样的 App 功能。那么用户触摸到 View 后,Android 系统是如何处理当前的触摸事件,又是怎么传递给当前触摸的 View 的呢?带着以上的疑问,让我们来看一看事件的分发规则。
1. 事件分发的方法简介
当我们点击屏幕的时候,Android 会将我们的操作封装成一个点击事件 MotionEvent。系统会将这个事件传递给一个具体的 View,MotionEvent 在各个 View 的层级之间传递的过程,其实就是点击事件的分发过程。
在事件分发的过程中,有 3 个最重要的方法:
dispatchTouchEvent(MotionEvent ev):用于进行事件的分发。onInterceptTouchEvent(MotionEvent ev):用于进行事件的拦截。onTouchEvent((MotionEvent ev):用来处理点击事件。
现在你是不是觉得有点被这几个方法绕晕了呢?没关系,下面我们就详细的阐述一下,Android 是如何利用这 3 个方法进行事件分发的。
2. 事件分发流程解析
2.1 从 Activity 开始分发
当一个点击操作发生的时候,事件最先传递到 Activity,由 Activity 的 dispatchTouchEvent() 方法进行事件分发:
public boolean dispatchTouchEvent(MotionEvent ev) {
// 1. 通过 onUserInteraction 回调告知用户进行了交互操作
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
// 2. 由 Window 进行分发
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
// 3. 没人处理该点击事件,交给 Activity 处理吧
return onTouchEvent(ev);
}
先看一下注释 1 处,这里会通过 MotionEvent 来判断用户是否进行了交互操作,从而可以触发 onUserInteraction 回调来告知。
注释 2 处就涉及到了我们本篇内容所提到了事件分发啦。可以看到 Activity 会将事件通过 Window 进行分发,如果返回 true 代表整个事件结束了,返回 false 则说明该事件没人处理。
注释 3 处,当这个点击事件分发了一圈也没人处理的时候,只能调用 Activity 的 onTouchEvent 进行处理。
2.2 分发给 PhoneWindow
看懂了 Activity 的 dispatchTouchEvent() 方法做了什么后,我们可以明确主要的逻辑就是在 getWindow().superDispatchTouchEvent(ev) 中,因为此处会将 Activity 的事件进行分发。既然如此,我们进去瞅瞅:
public abstract boolean superDispatchTouchEvent(MotionEvent event);
好家伙,竟然是一个抽象方法,我们得找到它的实现类才行。讲到这相信已经有小伙伴想起来了,Activity 中的 getWindow 不就是 PhoneWindow 嘛。
其实在官方文档中也介绍过 Window 的唯一实现就是 PhoneWindow。
The only existing implementation of this abstract class is android. policy.PhoneWindow, which you should instantiate when needing a Window.
既然如此,去 PhoneWindow 中看看它是如何实现 superDispatchTouchEvent() 方法的:
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
2.3 分发给 DecorView
到这里逻辑就很清晰了,PhoneWindow 将事件直接传递给了 DecorView,DecorView 我们在前面的文章中也介绍过,感兴趣的小伙伴可以看看 View 系列 —— 小白也能看懂的 DecorView。
我们这里简单说一下,DecorView 继承自 FrameLayout,其父类是 ViewGroup,当然啦,ViewGroup 也是继承自 View 的。所以 PhoneWindow 把点击事件分发给了 DecorView,也就是传递给了 View,只不过这个 View 是 Activity 的顶层 View——DecorView。
2.4 分发给 ViewGroup
既然 DecorView 的父类是 ViewGroup,所以接下来我们看一下 ViewGroup 的 dispatchTouchEvent() 方法。由于该方法很长,为了便于理解,我们分块来分析代码。
(1)初始化 ACTION_DOMN 事件
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
// 如果当前ACTION_DOMN事件,需要进行初始化
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
cancelAndClearTouchTargets(ev);
resetTouchState();
}
...
}
首先是判断当前是否是 ACTION_DOMN 事件,如果是的话则进行初始化。为什么该事件才需要执行初始化操作呢?
这是因为一个完整的事件是由 ACTION_DOMN 开始,并在 ACTION_UP 结束的。如果此时收到了一个 ACTION_DOMN 事件,说明这是一个新的事件序列,之前所有的状态都应该被复位。
初始化的操作中有一个变量需要我们注意一下 mFirstTouchTarget,在 resetTouchState() 方法中,会将 mFirstTouchTarget 变量置为 null。
如果事件被当前 ViewGroup 拦截,那么 mFirstTouchTarget 就是 null,如果没拦截交给子 View 去处理,那么 mFirstTouchTarget 就不为 null。后面我们也会再次提到该变量,这里大家有个印象就好。
(2)检查是否需要拦截
初始化工作结束后,dispatchTouchEvent 方法会去检查是否需要拦截该事件:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
// 1.FLAG_DISALLOW_INTERCEPT标记位
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
// 2.判断是否需要拦截事件
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
intercepted = false;
}
} else {
intercepted = true;
}
...
}
可以看到,这里就会对我们前面提到的 mFirstTouchTarget 进行判断,还出现了一个新的标记位 FLAG_DISALLOW_INTERCEPT。
这个标记位一般是通过子 View 的 requestDisallowInterceptTouchEvent() 方法来设置的,一旦设置了该标记位,ViewGroup 将无法拦截除了 ACTION_DOWN 以外的点击事件。因为 ViewGroup 在分发的时候,如果是 ACTION_DOWN 事件,就会重置 FLAG_DISALLOW_INTERCEPT 标记位,从而导致子 View 中设置的标记位不生效。
那这段代码的逻辑就很清晰啦。如果是 ACTION_DOWN 事件,或者当前事件 ViewGroup 不拦截交给子 View 处理,也就是 mFirstTouchTarget != null,那么会进入判断逻辑中。
在 if 逻辑中会先判断一下子 View 是否设置了不允许 ViewGroup 拦截,如果子 View 允许拦截就会进入 ViewGroup 的 onInterceptTouchEvent() 方法,处理事件的拦截。
所以 onInterceptTouchEvent() 方法也并不是每一次事件都会被触发到。假设 ViewGroup 在 ACTION_DOWN 事件时,执行了 onInterceptTouchEvent() 来判断是否需要拦截,那么在下一次经历像 ACTION_MOVE 事件时,就不会走进上面代码第 6 行处的 if 判断中,直接进入 17 行 intercepted = true。因此当一个 View 决定拦截,那么这一个从 ACTION_DOWN 到 ACTION_UP 的事件序列都将由其来处理,不会再调用 onInterceptTouchEvent 方法询问是否需要拦截了。
(3)ViewGroup 不拦截事件时,将事件下发给子 View
接下来我们来看 dispatchTouchEvent 方法中剩下的代码,分析一下 ViewGroup 不拦截事件时,事件是如何下发给子 View的。
2.5 分发给 View
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
// Find a child that can receive the event.
// Scan children from front to back.
final View[] children = mChildren;
// 1. 遍历所有的子 View
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
...
// 2. 判断子View能否接收到点击事件
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
...
// 3. 向子View分发事件
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
// 4. 该方法中会对 mFirstTouchTarget 进行赋值
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
...
}
注释 1 处,遍历 ViewGroup 所有的子 View,然后在注释 2 处判断该子 View 能否接收到点击事件。该判断主要由两个因素来衡量,分别是没有播放动画和点击事件的坐标在 View 的区域内。如果判断不通过,就 continue 结束本次循环,继续遍历下一个 View,否则就将事件交给该子元素处理。
注释 3 处 dispatchTransformedTouchEvent() 就是将事件分发给子元素,我们来看看该方法做了什么:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
...
final boolean handled;
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
// 1. 没有子View了,执行基类View的dispatchTouchEvent
handled = super.dispatchTouchEvent(event);
} else {
// 2. 有子View就执行子View的dispatchTouchEvent方法
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
...
}
可以看注释 1 和注释 2 处,没有子 View 就执行执行基类的 dispatchTouchEvent,否则就执行的子 View 的。代码分析到这,相信你已经大概明白了点击事件是如何从顶层 View 分发到子 Veiw 的了吧。
最后再呼应一下前面提到的 mFirstTouchTarget 变量,前面说过如果 ViewGroup 没有拦截当前事件交由子 View 处理的话,mFirstTouchTarget != null,那么 mFirstTouchTarget 是在哪里赋值的呢?答案就在 addTouchTarget 方法中。
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
可以看到 mFirstTouchTarget 的结构其实是一个链表。mFirstTouchTarget 是否为空会影响到 ViewGroup 对事件的拦截策略,如果 mFirstTouchTarget 为空,那么 ViewGroup 会拦截该序列序列中所有的事件,这一点在前面已经做了分析。
3. View 对点击事件的处理
前面我们讲到,事件会从 Activity 开始分发,经过 PhoneWindow、DecorView、ViewGroup,如果一直没有拦截的话,事件最终会被分发到子 View 中,那就先看看 View 的 dispatchTouchEvent 方法中做了什么吧:
public boolean dispatchTouchEvent(MotionEvent event) {
...
boolean result = false;
...
if (onFilterTouchEventForSecurity(event)) {
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
// 1. 设置了 mOnTouchListener
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
...
return result;
}
View 拿到事件后,如果设置了 OnTouchListener,并且 OnTouchListener 的 onTouch 方法返回 true,则代表该事件已经被消费,就不会去调用 onTouchEvent。
还有个问题不知道大家发现没有,View 是没有onInterceptTouchEvent 方法的,一旦有点击事件传递给它,那么它的 onTouchEvent 方法就会被调用。
接着看看 onTouchEvent 方法的内部实现:
public boolean onTouchEvent(MotionEvent event) {
...
final int action = event.getAction();
// 1. View 是否可点击
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
// 2. 不可用的 View 也会消耗点击事件
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return clickable;
}
...
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
...
if (!clickable) {
...
break;
}
...
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
// 3. 触发 performClick 方法
performClickInternal();
}
}
...
break;
...
return true;
}
return false;
}
注释 1 处,首先判断该 View 是否是可点击的。
注释 2 处可以看到不可用的 View 如果可以点击,那么也是会消耗点击事件的。可用的 View 也是一样的,只要 clickable 为 true,那么 onTouchEvent 最后就会返回 true 消耗该事件。
注释 3 处,如果当前是 ACTION_UP 事件时,会触发 performClick 方法,我们来看看这个方法:
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
// 1. OnClickListener
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}
注释 1 处可以看到,如果 View 设置了 OnClickListener,那么会执行其 onClick 方法。可以看出来 OnClickListener 的优先级在事件传递的过程是最低的。
所以,如果我们重写了 View 的 onTouchEvent,记得也同时处理下 performClick,避免重写后的方法把 performClick 覆盖了,就不会触发点击事件了。
4. 总结
4.1 分发流程总结
到这里 View 的事件分发机制基本分析结束,最后我们来总结一下分发流程。
在分发过程中,最重要的 3 个方法它们之间的关系可用如下的伪代码来表示:
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;
if (onInterceptTouchEvent(ev)) {
consume = onTouchEvent(ev);
} else {
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
当一个事件产生后,会最先传递给 Activity,Activity 会将其分发给 PhoneWindow,由于 PhoneWindow 在 setContentView 的时候会创建顶级 View 也就是 DecorView,因此事件也自然会分发到 DecorView 中处理。我们都知道 DecorView 是 ViewGroup 的子类,所以事件产生后,兜兜转转实际上是走到 ViewGroup 的 dispatchTouchEvent 进行分发的。
如果 onInterceptTouchEvent 返回 true 代表 ViewGroup 要拦截该事件,事件会在 onTouchEvent 中被消费。如果不拦截,那么会分发到 子 View 中,子元素的 dispatchTouchEvent 就会被调用。如此反复,直到事件被处理。
举个例子,你的 leader 接到了一项任务,由于 leader 日理万机,所以他把这项任务交给了二把手,二把手忙着需求评估,所以把这项任务交给了还在公司加班的你。你要是能搞定那最好,要是搞不定的话,你就会反馈给二把手,告诉他你先来帮我一起看看这个任务怎么搞吧。二把手看了看这个任务直挠头,决定告诉 leader,这项任务必须由 leader 亲自来操刀才能解决。
所以事件由上向下传递的过程就类似领导一层层派下来的任务,而事件自下而上传递的过程就类似我们把难题一层层的往上抛的过程。
4.2 一些重要结论总结
(1)如果一个 View 设置了 OnTouchListener,在分发的时候会执行其 onTouch 方法,如果返回了 true,那么是不会执行 View 的 onTouchEvent 方法的。
(2)在 onTouchEvent 的 ACTION_UP 事件中会执行到 performClick,在该方法会判断是否设置了 OnClickListener 并会执行 onClick 方法。所以 OnTouchListener 的优先级是最高的,其次是 onTouchEvent,OnClickListener 的优先级最低。因此开发中要注意一下,避免有些事件被覆盖不会被执行到。
(3)ViewGroup 的 onInterceptTouchEvent 默认返回的是 false,也就是不会拦截任何事件。而 View 是没有 onInterceptTouchEvent 的,一旦有事件分发给它,满足条件的情况下,就会执行 onTouchEvent,该方法默认是返回 true 的,除非该 View 不可点击。
(4)一旦父容器拦截了 ACTION_DOWN 事件,那么该事件序列后面所有的事件都会交给其处理,不会再传递给子元素了。