Android事件分发机制(二)

Android事件分发机制理解与描述的第二篇,主要是先介绍事件分发的流程

Android事件分发机制(二)

本篇是个人对Android事件分发机制理解与描述的第二篇,主要是先介绍事件分发的流程

下面将通过Android源码来讲解事件分发流程

事件分发流程

Activity

事件最开始会由系统分发给Activity。来调用Activity.dispatchTouchEvent(MotionEvent e)

注意,这个时候是还没有进入控件间的事件分发。这是因为Activity不是一个View,也不是一个ViewGroup

1
2
3
4
5
6
7
8
9
10
// Activity.dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {// 分发了这个事件
return true;
}
return onTouchEvent(ev);
}

Activity.DispathcTouchEven()中,我们可以看见,Activity通过getWindow获取Window抽象类,也就是获取PhoneWindow,在调用PhoneWindow.superDispatchTouchEvent()的方法继续将事件分发下去,但是PhoneWindow仍然还不是个View或ViewGroup,所以我们还要继续深入下去

getWindow()返回的是一个Window抽象类,在Android中,唯一继承了Window的类就是PhoneWindow,所以在这里Activity就是在调用PhoneWindow.superDispatchTouchEvent(MotionEvent e)

PhoneWindow

PhoneWindowAndroid中最基本的窗口系统,每一个Activity都会创建PhoneWindow对象,是Activity和整个View系统交互的接口。但是在事件分发中,它就只是扮演一个中间角色,进行传递事件,所以这里我们不会太关心PhoneWindow

下面就先来看看PhoneWindow.superDispatchTouchEvent(MotionEvent e)这个方法吧

1
2
3
4
5
6
7
8
9
10
11
// PhoneWindow.java
private DecorView mDecor;
@Override public 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);
}
}

PhoneWindow.superDispatchTouchEvent(MotionEvent event)的内部,是调用了一个mDecor.superDispatchTouchEvent(event)。这个mDecor其实是Window下面的DecorView,也就是说PhoneWindow内部是调用了DecorView.superDispatchTouchEvent(MotionEvent event)

这里再梳理一遍,首先系统将事件分发给到了Activity,Activity再将事件分发给PhoneWindow,PhoneWindow再将事件分发给到了DecorView。而DecorView是一个ViewGroup(一个继承与FrameLayout的ViewGroup),所以接下来就是属于控件间的事件分发了

所以PhoneWindow继续将事件传递到了DecorView,终于传递到ViewGroup上了,接下来就是属于控件间的事件分发了

ViewGroup

从上面就可以知道,第一个接受到事件的就是DecorView,而DecorView是继承FrameLayout,而DecorView内部也是直接调用了super.dispatchTouchEvent(event)

所以控件间事件传递的起始方法就是ViewGroup.dispatchTouchEvent(MotionEvent event),下面将讲解它是如何把事件传递给下一个控件的,是如何判断返回值标识事件是否被消费

  • 传递规则

    因为想要将事件继续分发下去,所以再ViewGroup中肯定先要遍历一遍子View

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    for (int i = childrenCount - 1; i >= 0; i--) {
    // 允许修改默认的child获取规则, 但是一般情况下会获取
    // 先判断preorderedList中是否已经存有子View,有的话就直接在里面取出,没有的话就从childern[]中取
    children[childIndex] final View child = (preorderedList == null) ? children[childIndex] : preorderedList.get(childIndex);
    // 省略通常不会影响流程的代码
    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {// 关键方法
    // ...
    newTouchTarget = addTouchTarget(child, idBitsToAssign);// 关键方法
    // ... break;
    }
    }

    在上面代码遍历的过程中,有一个关键判断执行了ViewGroup.dispathTransformedTouchEvent()这个方法。这个方法的作用就是判断传入进来的child是否为空,如果为空的话,就执行super.dispatchTouchEvent(event),不为空的话就执行child.dispatchTouchEvent(event)。正常情况下都是不为空的,所以就是在这里将事件传递给了子View

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
    View child, int desiredPointerIdBits) {
    ...
    if (child == null) {
    handled = super.dispatchTouchEvent(event);
    } else {
    handled = child.dispatchTouchEvent(event);
    }
    ...
    }
    }

    传递给子View之后,返回True就表示子控件接收到了这个事件,注意在这个方法下面还要一个重要的方法,那就是newTouchTarget = addTouchTarget(child, idBitsToAssign)这个方法只会在返回为True,也就是子控件接受到了事件的时候才会执行。这个方法的主要作用就是把当前这个接收事件的子控件转换成了TouchTarget对象并赋值给了mFirstTouchTarget

    至于为什么要这样做,那是因为,在这个循环的外面有几层这样的判断:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    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事件和被当前控件拦截事件。第二层判断是限制了事件只能是ACTION_DOWN,只有事件是ACTION_DOWN的时候才会继续执行,也就是说ACTION_MOVE等中间事件,是不会执行遍历代码的。第三层判断是只有之前能接收到事件的控件才会继续接收事件,之前不能接收到事件的控件是不会分发事件的

    简单点来说就是ViewGroup第一次会遍历所有控件并分发事件,接收到事件的View就会被转成TouchTarget记录起来。之后的中间事件(ACTION_MOVE等…)都只会传递给TouchTarget

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

View

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

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

拦截

Activity处拦截

因为Activity.dispatchTouchEvent(MotionEvent e)是整个事件分发的起点站,所以主要重写这个方法,不调用PhoneWindow.superDispatchTouchEvent(MotionEvent ev)就可以使得整个Activity内的控件收不到任何事件

ViewGroup处拦截

可以通过ViewGroup.onInterceptTouchEvent来拦截特定的事件