Android事件分发机制本质是树的深度遍历(图+源码)

什么是事件分发机制?


相关方法

事件分发机制相关的几个方法:
View

  • dispatchTouchEvent():处理事件。(:不分发)
  • onTouchEvent():触发 onClick() 等点击事件的回调。
    onTouchEvent() 在基类 View 的 dispatchTouchEvent() 中被调用。onTouchEvent()在 clickable 或 longclickable 或 contextclickable 时默认返回true,即消耗事件。

ViewGroup

  • dispatchTouchEvent():分发事件

    1. 判断当前事件是否需要调用 onInterceptTouchEvent() 对事件进行拦截。
    2. 若 onInterceptTouchEvent() 返回 true 则拦截,并处理事件。否则,则将事件分发给子View。
    3. 若没有子View 或事件不被下层 View 消耗(即所有子View的 dispatchTouchEvent() 返回false)则调用基类 View 的dispatchTouchEvent() 对事件进行处理。
  • onInterceptTouchEvent():决定是否拦截事件(即是否分发给子View),返回true 表示拦截,否则不拦截。
    onInterceptTouchEvent()(View没有这个方法)在 ViewGroup 的dispatchTouchEvent() 中被调用。onInterceptTouchEvent()默认返回false,即不拦截。

  • onTouchEvent():继承自 View,不重写。

概念

概念:事件分发机制就是事件(MotionEvent)如何在view tree 中分发、处理的一种规则。

  1. 事件分发
    概念:一个事件产生后,会先从Activity分发给Window,Window再分发给它里面的顶级View(是view tree 的根)。顶级View接受到事件后会调用自身的dispatchTouchEvent(),在该方法中会迭代调用子View的 dispatchTouchEvent() 直到事件被拦截或消耗若事件被拦截或消耗则结束分发/遍历),这个过程实质就是对view tree的深度遍历

    事件分发只发生在 Activity 和 ViewGroup 中,View 不分发。 Activity 和 ViewGroup 对事件的分发都是通过调用 dispatchTouchEvent() 。

    • 事件分发何时结束?

      1. 事件被拦截时。
      2. 事件被消耗时。
      3. 遍历完整个 view tree 时。

    事件拦截:即 onInterceptTouchEvent() 返回值是 true。只有ViewGroup才能拦截事件。
    事件处理结果:即 dispatchTouchEvent() 返回值,true 表示消耗,false表示不消耗。
    事件消耗:即 dispatchTouchEvent() 返回值是 true。

  2. 事件处理
    事件可以在 Activity 或 ViewGroup 或 View 中被处理,而且可被多次处理直到被消耗才停止处理。

    Activity:对事件的处理是通过调用自身的 onTouchEvent()。
    View:View 接收到事件后会调用 dispatchTouchEvent() 直接对事件进行处理。在该方法里会触发onTouch() 或 onTouchEvent() 对事件进行处理。
    ViewGroup:若没有重写dispatchTouchEvent(),都是通过调用基类 View 的 dispatchTouchEvent() 并将其返回值作为该 ViewGroup 的 dispatchTouchEvent() 的返回值。

    • ViewGroup 何时对事件进行处理?
      1、拦截事件或没有子View 时。
      2、下层的所有View 都未消耗事件时。

    • View 何时对事件进行处理?
      View 接受到事件后,(不会进行分发,因为没有子View)会直接处理事件,并将事件处理结果返回给它的ViewGroup。

注:事件处理不等于事件消耗。

事件分发


事件在view tree中分发的流程(图解)

“分发树”:当触碰事件产生时,ACTION_DOWN 产生时所触摸到的所有 View 按照父子关系可以组成一个 view tree 即“分发树”,“分发树”的根节点是顶级View,“分发树”是整个Window 中完整的 view tree 的一部分,ACTION_DOWN 就是在这个“分发树”中分发的。

事件在view tree中分发的流程:

  1. ACTION_DOWN 事件
    ACTION_DOWN 在具体应用程序app区域下分发的几种常见情形:
    view tree的深度遍历

    不了解Android 手机界面组成参考:Android手机界面组成
    总之,ACTION_DOWN 会深度遍历“分发树”并确定“消耗树”。注:这里的“消耗树”指的就是上图中的“消耗路径”。

  2. 后续同一事件序列的事件(ACTION_MOVE 或 ACTION_UP)
    后续同一序列事件都是沿着这一“消耗树”分发(深度遍历,但通常都是线性结构)的,且可被中途拦截但“消耗树”不变。

若ACTION_DOWN找不到“消耗路径”(即不被任何View消耗)也不被Activity消耗,那么后续事件就会消失,不会被处理。

“消耗路径”:从顶级View到消耗事件的View的最短路径就是“消耗路径”(线性结构的),该“消耗路径”会与已有的“消耗树”合并(若存在“消耗树”的话。通常情况下是没有的,因为ACTION_DOWN时会清空)。
“消耗树”:一条或多条“消耗路径”合并形成的。“消耗路径”也是“消耗树”的一种,只不过它只有一条“消耗路径”。
同一个事件序列:指从手指接触屏幕(触发ACTION_DOWN事件)到手指离开屏幕(触发ACTION_UP)以及期间产生的一系列事件。
注意:同一事件序列只有一条“消耗路径”,因此,觉得混乱的话可以直接把“消耗树”当做“消耗路径”。

下面是将“分发树”简化为线性结构后事件分发的几种情形:

红色:代表ACTION_DOWN事件的分发路径,不是“消耗路径”。
蓝色:代表ACTION_MOVE 和 ACTION_UP 事件分发路径,也是“消耗路径”。

1、我们重写ViewGroup1 的dispatchTouchEvent 方法,直接返回true消费这次事件
ACTION_DOWN 事件从(Activity的dispatchTouchEvent)——–> (ViewGroup1 的dispatchTouchEvent) 后结束传递,事件被消费(如下图红色的箭头代码ACTION_DOWN 事件的流向)。
1
2、我们在View 的onTouchEvent 返回true消费这次事件
4
3、我们在ViewGroup 2 的onTouchEvent 返回true消费这次事件
5
4、我们在Activity 的onTouchEvent 返回true消费这次事件
7
5、我们在View的dispatchTouchEvent 返回false(即重写并直接返回false)并且Activity 的onTouchEvent 返回true消费这次事件
8
上面例子来源:图解 Android 事件分发机制

注:下面的源码都来自Android-23版本。

事件在各结点中分发的流程(图+源码)

Activity

  • 流程图
    Activity的 dispatchTouchEvent() 的流程:
    Activity的dispatchTouchEvent方法的流程

  • 源码

public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}

Activity对事件的分发是通过Window来分发的,并不拦截。
注意:Window只负责帮Activity分发事件,不处理,也就是没有onTouchEvent方法,可以把它的分发功能当成Activity的一部分,然后忽略它。

ViewGroup

  • View和ViewGroup中的方法:
    View:dispatchTouchEvent();onTouchEvent()
    ViewGroup:重写dispatchTouchEvent();onInterceptTouchEvent();
    注意:ViewGroup及其子类并未重写onTouchEvent()。

  • 流程图
    ViewGroup的dispatchTouchEvent方法的流程:
    ViewGroup的dispatchTouchEvent方法的流程

TouchTarget链表:每个ViewGroup都持有一个mFirstTouchTarget变量,该变量指向一个TouchTarget链表(不带头结点)的首结点,该链表的结点用于存放当前Viewgroup的子结点,该子结点必须是“消耗树”上的结点。TouchTarget链表会在ACTION_DOWN被清空即“消耗树”消亡。总之,ViewGroup 的 TouchTarget链表存放该 ViewGroup 在“消耗树”上的所有子结点,mFirstTouchTarget指向它的首结点。

  • 事件在 ViewGroup 中分发的流程(即 ViewGroup 的 dispatchTouchEvent() 的流程):

    若为 ACTION_DOWN 事件:

    1. 是否拦截
      当前 ViewGroup 结点若拦截事件则结束分发,否则会将事件分发给它的子View。

    2. 只分发给“分发树”上的子结点
      遍历 Window 中完整的 view tree,若当前结点不在触碰区域内则进入下一结点,不对其分发事件。其实就是只在“分发树”上分发。
      注:在事件是 pointer down(包括ACTION_DOWN、ACTION_POINTER_DOWN、ACTION_HOVER_MOVE等)时会形成一个“分发树”。

    3. 更新 TouchTarget 列表
      在遍历它在“分发树”上的子结点时,若遍历到的子结点在当前 ViewGroup 的 TouchTarget 链表中则将事件分发给该子结点(即调用该子结点的dispatchTouchEvent())并结束对当前 ViewGroup 的子结点的遍历,否则,遍历每个子结点,调用每个子结点的 dispatchTouchEvent()直到被消耗即返回 true 才结束遍历并将该子结点插入到 TouchTarget 链表的头部(其实就是合并到已有的“消耗树”上,如果有“消耗树”的话)。总之,遍历子结点时,若子结点在已有的“消耗树”上则分发给它并结束更新,否则调用子结点的 dispatchTouchEvent(),若返回true则将该子结点插入链表头部结束更新。

    4. 分发。
      遍历 TouchTarget列表,调用每个元素(即该 ViewGroup 在“分发树”上的每个子View)的 dispatchTouchEvent() 。

    若是后续同一事件序列的事件(ACTION_MOVE 或 ACTION_UP):

    1. 是否拦截
      “消耗树”上的 ViewGroup 结点若拦截事件则结束分发,否则会将事件分发给它的子View。

    2. 遍历TouchTarget列表,调用每个元素(即该 ViewGroup 在“消耗树”上的每个子View)的 dispatchTouchEvent() 。

    是否拦截:当 ViewGroup 的 disallowIntercept 为 false 也就是允许拦截时,若事件为ACTION_DOWN 或mFirstTouchTarget != null 必定触发当前ViewGroup的onInterceptTouchEvent()。滑动冲突的处理就是在 ACTION_MOVE 事件在“消耗树”上分发时在中途将它拦截,参考:Android 滑动冲突的处理

  • 源码

public boolean dispatchTouchEvent(MotionEvent ev) {   
        //关键步骤:
        //1、是否拦截事件。
        //2、更新TouchTarget 列表。
        //3、分发。

        ......
        boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)) {
            ......

            // Handle an initial down.
            if (actionMasked == MotionEvent.ACTION_DOWN) {             
                //每次事件序列开始(即发生ACTION_DOWN事件时)都会
                //重置mFirstTouchTarget、mGroupFlags的值为初始值(分别为null 和 允许拦截)。
                resetTouchState();
            }

            //1、是否拦截事件
            // Check for interception.
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
            //mFirstTouchTarget指向一个TouchTarget列表的首结点。
            //TouchTarget列表是Linked List,若事件分发给某个子View后可被“下层”消耗,
            //则添加该子View到列表头部(采用的是头插法)。
            //“下层”包括当前ViewGroup里面的所有在当前事件触摸范围内的View,不仅仅指它的子View。
            //简单的说就是当前事件之前的事件被下层消耗则不为null。
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                //disallowIntercept是否不被允许拦截事件。
                //可通过子元素调用requestDisallowInterceptTouchEvent(boolean b)来
                //设置mGroupFlags的值,从而改变disallowIntercept。
                //但是这一条件判断对ACTION_DOWN无效(总为true),因为会重置mGroupFlags(见上面代码)。
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                intercepted = true;
            }


            //下面的代码都是讲事件是如何往下层分发的,分两步:2、3

            //2、更新TouchTarget 列表,事件为pointer down时才需要。
            //pointer down包括ACTION_DOWN、ACTION_POINTER_DOWN、ACTION_HOVER_MOVE等,参考下面判断条件。
            //注意:添加的元素必须是事件范围内也就是触摸到的区域内的子View,同时,沿着这个子View往下分发能消耗事件,
            //或者说,调用该子View的dispatchTouchEvent方法返回结果是true。
            // Update list of touch targets for pointer down, if needed.
            final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
            TouchTarget newTouchTarget = null;
            boolean alreadyDispatchedToNewTouchTarget = false;
            //非canceld && 不拦截 时才向下层分发事件              
            if (!canceled && !intercepted) {
                View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                        ? findChildWithAccessibilityFocus() : null;

                //注意:以下几种情况才需要更新TouchTarget 列表
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {     

                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null && childrenCount != 0) {
                        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不被触碰到或不能相应事件则剔除,其实就是只允许对“分发树”遍历
                            if (childWithAccessibilityFocus != null) {
                                if (childWithAccessibilityFocus != child) {
                                    continue;
                                }
                                childWithAccessibilityFocus = null;
                                i = childrenCount - 1;
                            }
                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }

                            newTouchTarget = getTouchTarget(child);
                            if (newTouchTarget != null) {//子元素已经在TouchTarget链表中,结束遍历。
                                // Child is already receiving touch within its bounds.
                                // Give it the new pointer in addition to the ones it is handling.
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }

                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                            //dispatchTransformedTouchEvent()会调用子元素的dispatchTouchEvent()。
                                // Child wants to receive touch within its bounds.
                                ......

                                //采用头插法将子元素添加到TouchTarget列表中
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                //已找到消耗此事件的路径且已分发给下层的目标View
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }
                        }
                    }

                    //并未找到消耗事件的路径 && mFirstTouchTarget 不为空时,将TouchTarget链表中
                    //最近最少被添加的target赋给它,即将链表的最后一个结点的引用赋给newTouchTarget。
                    if (newTouchTarget == null && mFirstTouchTarget != null) {                    
                        // Did not find a child to receive the event.
                        // Assign the pointer to the least recently added target.
                        newTouchTarget = mFirstTouchTarget;
                        while (newTouchTarget.next != null) {
                            newTouchTarget = newTouchTarget.next;
                        }
                        newTouchTarget.pointerIdBits |= idBitsToAssign;
                    }
                }
            }

            //3、分发:往下层分发事件。
            // Dispatch to touch targets.
            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                //当做普通View对待,而不是ViewGroup。
                //会调用super.dispatchTouchEvent()方法,最终调用自身的onTouchEvent方法.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                // Dispatch to touch targets, excluding the new touch target if we already
                // dispatched to it.  Cancel touch targets if necessary.
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    //在上面更新TouchTaget列表时已分发完毕。
                        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;
                }
            }

        return handled;
    }

关键步骤:
1、是否拦截事件。

  • 当ViewGroup的disallowIntercept为false也就是允许拦截时,若事件为ACTION_DOWN 或mFirstTouchTarget != null 一定会触发当前ViewGroup的onInterceptTouchEvent()。
  • 子元素能够通过调用requestDisallowIntercept(boolean b)来控制父容器能否调用onInterceptTouchEvent()。

2、更新TouchTarget 列表。

  • 本质是寻找该事件的“消耗路径”的下一个结点。若TouchTarget列表中有元素处于当前事件的触摸区域内,则结束更新,否则会遍历调用触摸区域内的子View的dispatchTouchEvent方法,直到返回true并添加到列表。。
  • 事件属于pointer down(包括ACTION_DOWN、ACTION_POINTER_DOWN、ACTION_HOVER_MOVE等),则要更新列表。
  • 更新列表规则:添加的元素必须是事件范围内也就是触摸到的区域内的子View,同时,沿着这个子View往下分发能消耗事件,或者说,调用该子View的dispatchTouchEvent方法返回结果是true。
  • 更新列表的步骤:遍历判断 1、子View是在触摸区域内,不是则continue,进入下一个循环。2、子View是否已在列表中,是则break,结束遍历。3、调用子View的dispatchTouchEvent方法,返回结果是true则用头插法加入列表(在这一步中已进行分发,后面的“分发”代码不会再分发一次)。

3、 分发。

  • 遍历TouchTarget列表,调用每个元素的dispatchTouchEvent方法。

总结:ACTION_DOWN若能被消耗则会确定“消耗树”,后续同一事件序列的事件(ACTION_MOVE 或 ACTION_UP)都是沿着这一“消耗树”分发的,且可被中途拦截。

ViewGroup的分发过程可用如下伪代码表示:

dispatchTouchEvent(MotionEvent ev){
    boolean consume = false;//事件是否被消耗,若被消耗则该事件的分发结束

    if (onInterceptTouchEvent(ev)) {//拦截事件
        consume = super.dispatchTouchEvent(ev);//即View.dispatchTouchEvent(ev)
    }else {
        //遍历子元素,将事件分发给子元素,直到事件被消耗。
        //其实,实际代码只需遍历TouchTarget列表中的元素,不需要遍历所有子View。
        View child = null;
        for (int index = 0; index < childNum; index++) {
            child = getChild(index);
            if (null != child) consume = child.dispatchTouchEvent(ev);
            if (consume) break;
        }

        //遍历结束但事件没有被消耗,对事件进行处理。
        if (!consume) consume = super.dispatchTouchEvent(ev);
    }

    return consume;
}

View

View 的 dispatchTouchEvent() 直接处理事件,不分发。

事件处理


Activity

Activity 对事件的处理都是通过调用自身的 onTouchEvent() 。
源码:

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

    return false;
}

View

无论View 还是ViewGroup,若没有重写dispatchTouchEvent(),对事件的处理都是通过调用基类View的 dispatchTouchEvent(),最终调用 onTouch() 或 onTouchEvent() 。

基类 View 的 dispatchTouchEvent() 流程:
若View设置了 OnTouchListener 且 onTouch() 返回 true 则dispatchTouchEvent() 返回 true,不会调用onTouchEvent()。否则,会调用 onTouchEvent() 且 onTouchEvent() 的返回值就是dispatchTouchEvent() 的返回值。

在View的onTouchEvent()中若View是clickable或longclickable的则会调用onClick()(若有设置OnClickListener)。clickable或longclickable或contextclickable时默认返回true,否则返回false。

  • 流程图
    View的dispatchTouchEvent方法的流程:
    View的dispatchTouchEvent方法的流程

  • 源码

public boolean dispatchTouchEvent(MotionEvent event) {
        ......

        boolean result = false;//是否消耗事件
        if (onFilterTouchEventForSecurity(event)) {
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            //若设置了OnTouchListener,则先调用onTouch()。
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

            //若onTouch()没有消耗事件则调用onTouchEvent()
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }

        ......
        return result;
    }

若View是enabled的,且设置了OnTouchListener,则先调用onTouch(),若onTouch()返回true则分发结束,否则,接着调用onTouchEvent()。

ViewGroup

ViewGroup对事件处理是通过调用基类View的dispatchTouchEvent() ,最终调用 onTouch() 或 onTouchEvent() 。
ViewGroup的onTouchEvent()继承自View,本身不重写。ViewGroup的实现类也不重写该方法。

总结


事件分发机制概要流程:
Android事件分发机制概要流程
上面图片来源:Android事件分发机制 详解攻略,您值得拥有

事件分发机制详细流程:
Android事件分发机制详细流程

总之,ACTION_DOWN 会深度遍历“分发树”并确定“消耗树”,后续同一事件序列的事件(ACTION_MOVE 或 ACTION_UP)都是沿着这一“消耗树”分发(深度遍历,但通常都是线性结构)的,且可被中途拦截但“消耗树”不变。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值