深入浅出Android事件分发机制:最全面最易懂:高级篇(三)

欢迎关注程序引力

安卓开发者必须了解的事件分发机制。本文将从源码角度,以最全面、最易懂的形式来讲解Android事件分发机制。

若有错漏,烦请斧正。转载请注明出处。

0. 前言

鉴于安卓分发机制较为复杂,故分为多个层次进行讲解,分别为基础篇、实践篇与高级篇。

1. 内容简介

本文内容为(三)高级篇,将从源码角度分析事件分发机制。该部分源码来自于目前最新的Android Pie (即Android 9.0),API 28的源码。

建议读者在阅读了本系列的基础篇与实践篇的文章后,再阅读本篇内容,会更容易理解。

2. Activity核心分发方法

对于Activity,负责参与分发的方法有:

  • dispatchTouchEvent()
  • onTouchEvent()

这两个方法不是并列同级的关系,实际上前者是包含后者的。

Activity的dispatchTouchEvent

public boolean dispatchTouchEvent(MotionEvent ev) {
    // 关键点2.1
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {    
        onUserInteraction();
    }
    // 关键点2.2
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;    
    }
    // 关键点2.3
    return onTouchEvent(ev);
}

从上面的源码可以看到,对于关键点2.1,当Activity的dispatchTouchEvent方法接收到按下Down事件后,首先调用onUserInteraction方法。该方法在事件分发给Activity时会被调用,且该方法一般为空。如果开发者希望知道用户与设备的交互情况,可以覆写该方法。

实际上,onUserInteraction方法主要是用于管理状态栏通知,以及在恰当的时候取消通知。与该方法相关的还有另一个方法,onUserLeaveHint。该方法作为Activity生命周期回调的一部分,会在用户将Activity放到后台时调用(如用户点击Home键),该方法会在onPause方法之前调用。

对于关键点2.2,getWindow()返回的是Window类对象,而Window类实际是一个抽象类,它有一个唯一的实现类PhoneWindow。实际上,getWindow()返回的是PhoneWindow类对象,后面调用方法也实际上是PhoneWindow对象的方法,该方法实现如下:

@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
}

在该方法中,调用了mDecor的同名方法,mDecor对象属于DecorView,该类是PhoneWindow类的内部类。其源码实现如下:

public boolean superDispatchTouchEvent(MotionEvent event) {
    return super.dispatchTouchEvent(event);
}

该类继承自FrameLayout,即是ViewGroup的子类。调用super父类的分发方法,即是调用了ViewGroup的分发方法(详细介绍见下文)。通过该方法,可以将事件分发给此ViewGroup内部的子View处理。至此,Activity的分发流程介绍完毕。

总的说来,getWindow().superDispatchTouchEvent()这一语句将事件从Activity传递到了其ViewGroup,再通过该ViewGroup传递给其内部的子View或ViewGroup。如果getWindow()的分发方法返回TRUE,即表示已经消费了该事件,那么会通过return返回TRUE,告知调用者已经消费事件。否则在源码中的关键点2.3处,调用onTouchEvent()方法进行处理。

Activity的onTouchEvent方法

Activity会将事件不断向下分发给其内部的ViewGroup或View,若内部的ViewGroup以及View都不消费该事件,则事件会层层传递回来,到达Activity的onTouchEvent方法。

这一事件传递流程的介绍,请参考本系列文章的《基础篇》与《实践篇》。本篇内容更为偏向源码层面的分析与解读。

onTouchEvent方法的源码如下:

public boolean onTouchEvent(MotionEvent event) {
    if (mWindow.shouldCloseOnTouch(this, event)) {
        finish();
        return true;
    }
    return false;
}

在该方法中,首先调用mWindow成员的shouldCloseOnTouch方法,该方法不是PhoneWindows对象的方法,而是抽象类Window自身的方法,其表示是否要在该touch事件后关闭窗口。如果返回TRUE,则调用Activity的finish方法,并返回TRUE表示消费事件。否则返回FALSE。

在Window类的shouldCloseOnTouch方法中,其源码如下:

public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
    final boolean isOutside = event.getAction() == MotionEvent.ACTION_DOWN && isOutOfBounds(context, event)
        || event.getAction() == MotionEvent.ACTION_OUTSIDE;

    if (mCloseOnTouchOutside && peekDecorView() != null && isOutside) {
        return true;
    }
    return false;
}

该方法主要是判断点击事件是否为Down按下事件,并且在边界之外,并且还判断了一些标志位。若按下事件在边界外,且标志位为TRUE,否则返回FALSE。返回TRUE的话,正如上文所说,会在onTouchEvent中调用finish方法,并返回TRUE,否则也直接返回FALSE.

Activity分发方法小节

总的说来,Activity的事件分发逻辑还是比较简单的。在Activity的分发方法dispatchTouchEvent方法中,首先会在按下事件调用onUserInteraction方法,该方法一般为空。之后会调用PhoneWindow对象的分发方法,最后通过ViewGroup的分发方法将事件分发给Activity内部的ViewGroup或View。若它们不能处理,则会将事件传回来,调用Activity的onTouchEvent方法。在该方法中,会判断是否应该关闭Activity,否则则返回默认的FALSE。

3.ViewGroup核心分发方法

对于ViewGroup,负责分发事件的方法有:

  • dispatchTouchEvent()
  • onInterceptTouchEvent()
  • onTouchEvent()

上面这些方法并不是并列同级的关系,事实上,dispatch方法包含了后面两个方法。更为准确地说,ViewGroup中并不包含onTouchEvent方法,只是由于ViewGroup是View的子类,而后者拥有onTouchEvent方法,故也认为ViewGroup也存在该方法。下面对这三个核心方法分别做介绍。

ViewGroup的dispatchTouchEvent方法

由于该方法是事件分发机制中最长的一个,也是众多读者最难以理解的一个。网上的相关教程和总结也仅仅是讲源码的细节,并未对方法进行总结,难以让读者有一个整体的认识。为此,在介绍该方法之前,先对方法中的一些关键变量作介绍,然后再列出方法中的核心步骤与行为,在此基础之上再阅读源码,则会事半功倍。

关键变量

  • ev : MotionEvent类型,即需要分发的事件对象。
  • action : int型,表示事件类型
  • actionIndex : int型,表示action位运算后的值
  • handled : 布尔型,表示该方法最后的处理结果,TRUE表示已处理消费,FALSE表示未消费。
  • intercepted : 布尔型,表示是否拦截事件
  • canceled :布尔型,表示是否取消事件
  • mFirstTouchTarget :TouchTarget类型,用于描述被点击的View。实际上,该对象是一个链表的头指针。

核心步骤

  • Step1: 初始化、过滤、重置等工作。
  • Step2: 判断是否应该拦截,调用拦截方法,给是否已经拦截的标志位赋值。
  • Step3: 判断是否取消事件。
  • Step4: 开始主要对按下事件处理。(若事件被拦截、取消或者事件不是按下事件,则不执行Step4到Step6)
  • Step5: 循环对ViewGroup的子View进行处理。
  • Step6: 尝试获得新的touchTarget,并添加到mFirstTouchTarget指向的链表。若无新的target,则以链表末尾节点为target。
  • Step7: 将事件分发给mFirstTouchTarget处理。
  • Step8: 收尾工作。

读者看完这些核心步骤没有完全理解也没有关系,此处只是帮助读者对ViewGroup中的事件分发机制有一个整体全面的认识。待读完后续的详细介绍后,再对这些步骤进行比对,会有更深刻的认识。

源码分析

下面开始对ViewGroup的分发方法的源码进行分析。由于Android 9.0中的该方法源码非常长,故只分析其核心逻辑。同时,为了让读者更易于阅读,不用前后查阅,这些源码会以分片的形式展示。并且在源码附近会添加注释,用于更直观地解释源码。

 //方法的入参为ev,即点击事件,所有的分发都是围绕它来进行的。该变量属于上文介绍的关键变量。
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    // 输入一致性校验
    // 若该类的isInstrumentationEnabled方法为TRUE,则会在View类中初始化该对象。
    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onTouchEvent(ev, 1);  
    }

    // 通过点击事件判断是否应该被有焦点的View处理事件,如果同时存在拥有焦点的View,则设置为FALSE。
    if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
        ev.setTargetAccessibilityFocus(false);
    }

上面是dispatchTouchEvent方法入口会执行的逻辑,不是特别重要,可以重点关注后面的源码。

// 重要的局部变量,该变量值表示是否处理了该事件。该变量也是方法最后的返回值。该值属于上文介绍的关键变量之一。
boolean handled = false;
// 出于安全原因,会过滤点击事件。在该方法中会对event的一些标志位进行处理。
// 返回FALSE表示丢弃事件,返回TRUE表示继续处理,即执行流程会进入if语句内部。
if (onFilterTouchEventForSecurity(ev)) {
    final int action = ev.getAction();    // action变量表示事件类型
    // 在安卓源码中有大量的位操作,通过进行位操作限定标志位范围,再对其做判断
    // 此处位操作的作用是保留action的末尾8bit位,其余置0,作为actionMasked
    final int actionMasked = action & MotionEvent.ACTION_MASK;

    // 因为事件流是以按下事件开始的,为此当按下事件到来时,会做一些初始化工作。
    if (actionMasked == MotionEvent.ACTION_DOWN) {
        // 初始化工作主要就是清空target以及重置状态
        cancelAndClearTouchTargets(ev);
        resetTouchState();
    }

上面这部分操作即属于步骤Step1。在过滤以及初始化工作结束后,下面进入Step2 拦截事件的部分,其源码片段为:

// 该标志位表示是否拦截事件,此处声明该变量
final boolean intercepted;
// 若该事件是按下事件,或者事件已经被某个组件(target)处理过,则进入if子句。
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {

    // 该变量表示是否禁止拦截,这个标志位由子View控制
    // 子View通过调用requestDisallowInterceptTouchEvent来禁用父View拦截
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    // 默认情况下,都是允许拦截的,此时会调用拦截方法
    if (!disallowIntercept) {
        // 调用拦截方法,并将是否拦截的结果赋值给变量
        intercepted = onInterceptTouchEvent(ev);
        // 将ev事件中的action值恢复,因为前面进行了位操作
        ev.setAction(action); // restore action in case it was changed
    } else {
        // 如果子View禁用拦截,会使得父View不会调用拦截方法。即令下面变量为false,后面会判断该变量。
        intercepted = false;
    }
} else {
    // 如果没有组件处理过事件,同时当前事件已经不是按下事件,则拦截事件
    // 也就是说,按下事件已经分发了,但是没有任何组件处理它,所以剩余的事件都拦截
    intercepted = true;
}

// 若拦截该事件,或者事件已经有目标组件进行处理,则进行正常的事件分发
// 此处的focus具体含义待查
if (intercepted || mFirstTouchTarget != null) {
    ev.setTargetAccessibilityFocus(false);
}

下面是Step3,获取是否取消事件的标志位,并且声明变量。

// 获取是否取消事件的标志位
final boolean canceled = resetCancelNextUpFlag(this)
        || actionMasked == MotionEvent.ACTION_CANCEL;

// 获取是否将事件分发给多个子View的标志位
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;

// 声明后续会使用到的变量
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;

查看后续源码:

// 此部分首先判断是否取消事件或者拦截事件,若都是否的话,则进入该if子句
// 需要注意的是,该if语句没有else分支。
// 即对于被拦截或取消的事件,不执行该if子句中的所有方法。不执行上文总结的Step4到6
if (!canceled && !intercepted) {
    // If the event is targeting accessibility focus we give it to the
    // view that has accessibility focus and if it does not handle it
    // we clear the flag and dispatch the event to all children as usual.
    // We are looking up the accessibility focused host to avoid keeping
    // state since these events are very rare.
    View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
            ? findChildWithAccessibilityFocus() : null;

    // 该if语句没有else分支
    // 此处对事件的类型等进行判断,主要是处理按下事件
    if (actionMasked == MotionEvent.ACTION_DOWN
            || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
            || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {

        // 此处获得事件的actonIndex与事件id.
        final int actionIndex = ev.getActionIndex(); // always 0 for down
        final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                : TouchTarget.ALL_POINTER_IDS;

        // Clean up earlier touch targets for this pointer id in case they
        // have become out of sync.
        removePointersFromTouchTargets(idBitsToAssign

查看后续源码,属于上文总结的Step5 循环对ViewGroup的子View进行处理。可以看到下面这部分源码在对一些变量进行初始化后,主要就是一个循环体:

// 获取子View数量,如果为0,或者已经有处理事件的target组件,则不会进入if子句
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
    final float x = ev.getX(actionIndex);
    final float y = ev.getY(actionIndex);

    // 获取子View的前序遍历的列表
    final ArrayList<View> preorderedList = buildTouchDispatchChildList();
    final boolean customOrder = preorderedList == null
            && isChildrenDrawingOrderEnabled();
    final View[] children = mChildren;

    // 以‘从尾到头’的方式遍历这个前序遍历的列表
    for (int i = childrenCount - 1; i >= 0; i--) {
        final int childIndex = getAndVerifyPreorderedIndex(
                childrenCount, i, customOrder);
        final View child = getAndVerifyPreorderedView(
                preorderedList, children, childIndex);

        // If there is a view that has accessibility focus we want it
        // to get the event first and if not handled we will perform a
        // normal dispatch. We may do a double iteration but this is
        // safer given the timeframe.
        // 这里会进行判断,如果有一个View具有焦点,并且该View就是当前的子View,会对该View做第二次遍历
        if (childWithAccessibilityFocus != null) {
            if (childWithAccessibilityFocus != child) {
                continue;
            }
            childWithAccessibilityFocus = null;
            // 序号减1,之后会再做一次遍历处理。
            i = childrenCount - 1;
        }

        // pointerEvent,待查
        if (!canViewReceivePointerEvents(child)
                || !isTransformedTouchPointInView(x, y, child, null)) {
            ev.setTargetAccessibilityFocus(false);
            continue;
        }

下面是上文总结的Step6,即对touchTarget进行处理。

        // 在getTouchTarget方法中,其逻辑是如果mFirstTouchTarget表示的链表中某一个节点就是child,则返回它作为新的target。若找不到则返回空。
        newTouchTarget = getTouchTarget(child);
        if (newTouchTarget != null) {
            // newTouchTarget不为空则表示该child子View已经在其范围内接收到了事件
            // 为此需要给它一个新的pointer ID
            // Give it the new pointer in addition to the ones it is handling.
            newTouchTarget.pointerIdBits |= idBitsToAssign;
            //注意,此处是break,结束循环。
            break; 
        }

下面的代码为newTouchTarget为空时才能执行得到,

        resetCancelNextUpFlag(child);
        // 下面这个为方法为一个重要的方法,即将事件与child传递进行,会在其中对child的分发方法进行递归调用。也就是说,这一部分遍历子View的逻辑是在一个循环中,对View进行递归处理。
        // 如果该方法返回TRUE,表示子View已经消费了该事件爱你,则执行if子句的方法。核心是将child添加到链表中,获得newTouchTarget.
        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();
            // 将child添加到mFirstTouchTarget为头结点的链表中,并且返回这个节点。
            newTouchTarget = addTouchTarget(child, idBitsToAssign);
            // 将已经分发的标志设置为TRUE,并且break结束循环。
            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);

下面是for循环之后的代码:

        // 该if子句进入的条件是,该ViewGroup中没有一个子View能够处理该事件,并且已经有View能够处理该事件了。则需要将链表的最后一个节点作为newTouchTarget,并重新分配point ID.
        if (newTouchTarget == null && mFirstTouchTarget != null) {
            newTouchTarget = mFirstTouchTarget;
            while (newTouchTarget.next != null) {
                newTouchTarget = newTouchTarget.next;
            }
            newTouchTarget.pointerIdBits |= idBitsToAssign;
        }

结合前面对子View遍历的循环中的break,分析一下可能的几种情况:

  • 情况一:在for循环中遍历时,能够处理事件的View已经在链表中,则跳出循环
  • 情况二:在循环遍历时,该View的子View能够处理事件,为此将该View添加到链表,也跳出循环

从上面的分析中,可以知道,这些代码对View进行遍历也好,递归也好,就是要找出一个View能够处理该事件,并且将该View作为newTouchTarget,并且添加到mFirstTouchTarget的链表中。如果实在找不到这样一个View,也会将链表中最后一个节点作为newTouchTarget。总之,经过这些处理后,就是得到一个mFirstTouchTarget链表。

下面是上文总结的步骤 Step7: 将事件分发给mFirstTouchTarget处理。

    // 若链表为空,则对事件进行再次分发。
    if (mFirstTouchTarget == null) {
        // No touch targets so treat this as an ordinary view.
        handled = dispatchTransformedTouchEvent(ev, canceled, null,
                TouchTarget.ALL_POINTER_IDS);
    } else {
        // 若链表不为空,则遍历这个链表,将事件分发给除了newTouchTarget之外的节点。
        TouchTarget predecessor = null;
        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;
                }
                if (cancelChild) {
                    if (predecessor == null) {
                        mFirstTouchTarget = next;
                    } else {
                        predecessor.next = next;
                    }
                    target.recycle();
                    target = next;
                    continue;
                }
            }
            predecessor = target;
            target = next;
        }
    }

总结Step 7,可能有几种情况:

  • 情况一:链表不为空,给链表中不为newTouchTarget的View分发事件
  • 情况二:链表为空,以null遍历子View

在分发方法的最后,是Step8的额外收尾工作:

        // 若需要的话,更新链表
        // Update list of touch targets for pointer up or cancel, if needed.
        if (canceled
                || actionMasked == MotionEvent.ACTION_UP
                || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
            resetTouchState();
        } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
            final int actionIndex = ev.getActionIndex();
            final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
            removePointersFromTouchTargets(idBitsToRemove);
        }
    }

    // 若没有消费事件,且某对象不为空,调用该回调方法。
    if (!handled && mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
    }
    // 返回最终结果
    return handled;
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容