一点见解: Android事件分发机制(二)

一点见解: Android事件分发机制(一) - 基本概念解释
一点见解: Android事件分发机制(二) - 分析ViewGroup
一点见解: Android事件分发机制(三) - 分析View

本文主要分析事件分发机制的传递路径和传递规则, 着重分析ViewGroup.

对于源码的分析假设大家总是能够找到具体的源码, 所以只贴出关键的部分进行分析.

分发的源头逻辑分析

从头开始最清晰.

事件最开始会由系统分发给Activity#dispatchTouchEvent(MotionEvent ev)

注意这时候还没进入控件间的事件分发逻辑, 因为Activity不是一个View.那么Activity又是怎样把事件传给第一个View的, 又是传给了谁, 看源码.

// Activity#dispatchTouchEventpublic 
boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) { 
        onUserInteraction(); 
    } 
    if (getWindow().superDispatchTouchEvent(ev)) {// 分发了这个事件 
        return true;
    } 
    return onTouchEvent(ev);
}

因为Activity只是一个中转站, 所以代码不多, 关键代码就是getWindow().superDispatchTouchEvent(ev).
getWindow()返回的是一个Window抽象类, 在Android中, 唯一继承了这个抽象类的类是PhoneWindow, 所以这里实际调用的就是PhoneWindow#superDispatchTouchEvent(MotionEvent ev), 同样PhoneWindow也不是View, 所以还要再看PhoneWindow源码

// PhoneWindow.javaprivate DecorView mDecor;
@Overridepublic boolean superDispatchTouchEvent(MotionEvent event) { 
    return mDecor.superDispatchTouchEvent(event);
}
// PhoneWindow.DecorView 内部类
private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {
    public boolean superDispatchTouchEvent(MotionEvent event) { 
        return super.dispatchTouchEvent(event); 
    }
}

从摘录的源码可以得到结论

系统分发事件给Activity, 然后传递给PhoneWindow, 接着传递给PhoneWindow实例中的DecorView, 而DecorView是一个View(继承了FrameLayout), 之后, 事件就进入了控件间传递逻辑了.

源码Bonus

  1. 因为Activity#dispatchTouchEvent(MotionEvent ev)是事件分发的起点站, 所以只要重写这个方法不调用PhoneWindow#superDispatchTouchEvent(MotionEvent ev)就可以使得整个Activity内的控件接收不到任何事件. 实际上还有几个类似的dispatchXXXEvent()方法, 可以拦截键盘点击事件等.
  2. Activity#dispatchTouchEvent(MotionEvent ev)里面出现了onUserInteraction(), 这个方法可以看作一个回调, 任何用户操作开始之前, 包括键盘操作都会调用这个方法, 所以可以重写这个方法来监听用户操作的开始节点. 还有一个对应方法onUserLeaveHint()
  3. Activity#dispatchTouchEvent(MotionEvent ev)中如果PhoneWindow#superDispatchTouchEvent(MotionEvent ev)没有消费掉这个事件, 会调用Activity#onTouchEvent(MotionEvent event)来尝试消费事件.

从ViewGroup开始

从上面分析可以知道, 第一个接收到事件的View方法是DecorView#superDispatchTouchEvent(MotionEvent event), 里面直接调用了super.dispatchTouchEvent(), DecorView直接继承FrameLayout, 一路跟踪过去就可以得到结论

控件间事件传递的起始方法是ViewGroup#dispatchTouchEvent()

值得指出的是, View也有dispatchTouchEvent(), 后面再说.

从方法命名就可以看出, 这个方法的作用是分发事件, 所以事件分发机制的实现逻辑就在这个方法里面, 这部分的代码有200多行, 其中很多代码都是保持控件状态一致或者处理多点触控的问题, 本文不关心这部分的实现, 所以在分析前需要明确分析的关键点

  1. 它是如何把事件传递给下一个控件的, 包括之前提到的拦截等
  2. 返回值标识事件是否被消费, 所以它是如何确定返回值的为了让代码更清晰, 逐段分析源码, 以下源码都是来自ViewGroup#dispatchTouchEvent

传递规则

因为要把事件分发给子控件, 所以在这个方法内必定会遍历子控件的, 所以我们首先找到这部分遍历代码, 如下

for (int i = childrenCount - 1; i >= 0; i--) {
    // 允许修改默认的child获取规则, 但是一般情况下会获取
    children[childIndex] final View child = (preorderedList == null) 
                                ? children[childIndex] : preorderedList.get(childIndex); 
    // 省略通常不会影响流程的代码 
    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {// 关键方法 
        // ... 
        newTouchTarget = addTouchTarget(child, idBitsToAssign);// 关键方法 
        // ... break; 
    }
}

上面在遍历的过程有个关键的判断中执行了ViewGroup#dispatchTransformedTouchEvent方法, 代码就不贴出来了, 虽然有一系列的判断, 但是归根到底就是判断child参数是否为空, 为空就执行super.dispatchTouchEvent()不为空就执行child.dispatchTouchEvent(), 这里child必定不为空, 所以就是在这里把事件传递给了子控件.

传递给子控件后, 返回true证明子控件接收了这个事件, 注意, 这里有另一个关键方法ViewGroup#addTouchTarget, 这个方法把当前这个接收事件的子控件转换成了TouchTarget对象并赋值给了mFirstTouchTarget, 为什么要这样做?

因为这个遍历代码是包含在一个3重条件判断里面的, 也就说有可能不被执行, 看看判断的条件

if (!canceled && !intercepted) { 
    // ...
    if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { 
        // ... 
        if (newTouchTarget == null && childrenCount != 0) { 
            // 遍历代码 
        } 
    }
}

第一重判断根据命名可以推测是ACTION_CANCEL事件和被当前控件拦截事件, 之后再讨论;
第三重判断只要存在子控件就会为true;
关键是第二重判断, 限制了事件必须是ACTION_DOWN, ACTION_POINTER_DOWN或者ACTION_HOVER_MOVE才有可能进入遍历代码(对分发机制我们只关注ACTION_DOWN事件, 所以后面省略另外两个), 也就是说当事件为ACTION_MOVE等中间事件时, 是不会直接执行遍历代码的, 也就不会把事件分发给子控件, 所以还会有地方执行分发工作, 也就是调用ViewGroup#dispatchTransformedTouchEvent, 查找其余部分代码找到

if (mFirstTouchTarget == null) { 
    handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);
} else { 
    // ... 
    TouchTarget target = mFirstTouchTarget; 
    while (target != null) { 
        // ... 
        final TouchTarget next = target.next; 
        if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { 
            handled = true; 
        } else { 
            final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted; 
            if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) { 
                handled = true; 
            } 
            // ... 
        }
        // ... 
        target = next;  
    }
}

这段代码总是会执行的, 关键的判断依据是mFirstTouchTarget是否为空, 为空时最后就会调用View#dispatchTouchEvent, 否则就会把事件传给mFirstTouchTarget对应的子控件, 结合上面的分析, 只有在子控件接收了ACTION_DOWN等事件的时候, 它才不为空, 也就是说

当子控件没有接收ACTION_DOWN(即是View#dispatchTouchEventACTION_DOWN没有返回true)的时候, 后续的事件就不会分发给这个子控件.

不为空的时候可以看到mFirstTouchTarget其实是一个链表, 会把事件分发给链表中的所有子控件, 这是针对多点触控的处理, 不是本文关注的问题, 不作分析, 只需要知道其他ACTION_DOWN事件的传递不会重新遍历所有子控件, ACTION_DOWN是整个操作(一系列事件)的起点, 在这时候就已经确定后续事件需要传递的子控件了.

分析到这里我们已经知道事件分发机制是怎样在控件间传递事件的了

父控件遍历子控件, 询问所有子控件是否接收ACTION_DOWN事件, 然后保存接收事件的子控件到链表, 确定后续事件的分发对象, 当其他事件传递给父控件时直接传递事件给链表中的子控件. 当没有子控件接收ACTION_DOWN时执行View#dispatchTouchEvent

拦截事件 onInterceptTouchEvent

ViewGroup的事件分发还有一个关键点, 就是上面提到的遍历的第一重判断中的intercepted变量, 如果这个变量为true那么即使是ACTION_DOWN事件也不会遍历询问子控件, 这时mFirstTouchTarget链表就必定为空, 后续的所有事件都会传递给View#dispatchTouchEvent而不会传给子控件., 也就是说此时父控件拦截了传递给它的事件

final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; 
    if (!disallowIntercept) { 
        intercepted = onInterceptTouchEvent(ev);
        // ...
    } else { 
        intercepted = false; 
    }
} else { 
    intercepted = true;
}

一般情况下intercepted的值由ViewGroup#onInterceptTouchEvent决定, 值得指出, View是没有这个方法的, 很容易理解这是因为View不会有其他子控件了, 没有拦截事件的需要.

接着看ViewGroup#onInterceptTouchEvent, 里面直接返回了false, 默认不拦截事件, 因此

可以通过重写ViewGroup#onInterceptTouchEvent来拦截特定的事件

但是拦截方法同样有条件判断

  1. 需要是ACTION_DOWN事件, 或者mFirstTouchTarget不为空, 而mFirstTouchTarget即是第一个消费事件的子控件, 所以如果有子控件消费了事件, 那么后续总会调用ViewGroup#onInterceptTouchEvent, 父控件仍有机会拦截事件, 而如果是父控件自身消费了ACTION_DOWN事件, 那么就不会再调用ViewGroup#onInterceptTouchEvent
  2. 还需要没有设置FLAG_DISALLOW_INTERCEPT标志位, 很容易找到相关的方法ViewGroup#requestDisallowInterceptTouchEvent, 也就是可以通过调用这个方法来禁用拦截机制.

至此ViewGroup分发机制涉及的方法大致分析完毕了.

源码Bonus

  1. 可以通过ViewGroup#requestDisallowInterceptTouchEvent禁用拦截机制.
  2. 可以通过对子控件设置AccessibilityFocused来在遍历子控件的时候优先询问该子控件是否接收ACTION_DOWN事件.
  3. 默认的遍历顺序是根据子控件在布局中的Z轴值来决定的, 但是可以重写ViewGroup#getChildDrawingOrder来修改默认的子控件遍历顺序.

通过本文可以知道, 无论有没有子控件接收事件, 事件都会传递给View#dispatchTouchEvent, 所以下一篇将分析View

推荐阅读更多精彩内容