一点见解: 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

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

推荐阅读更多精彩内容