Android 事件分发机制源码详解-最新 API

大体内容如下

  • 概念
  • Activity 对事件的分发
  • ViewGroup 的事件分发过程
  • View 的事件分发过程
  • View 的点击事件处理
  • 总结
    分析了源码可以得到的结论
    事件的传递过程文字描述
    事件传递机制流程图
    伪代码展示事件分发拦截和消费三者的关系

本篇文章是基于最新 Android 源码 ( API27 ),进行分析总结的,可以直接翻到文章末尾查看「源码总结」和「事件传递流程图」,带着大体流程和结论去看源码,效率更高。

概念

事件:就是用户手指从触摸屏幕的那一刻起,到手指离开屏幕的那一刻为止,中间产生的一系列动作,(DOWN MOVE UP等)都是事件,都被封装到了 MotionEvent 中。

所谓事件分发:就是当一个 MotionEvent 产生以后,系统需要把它传递给某一个具体的 View,而这个传递的过程就是分发过程。

《Android 事件分发机制源码详解-最新 API》 image

事件的大体流向:

《Android 事件分发机制源码详解-最新 API》 image

事件一级一级的往下传递,如果没有任何一个 View 消耗掉事件,那么最终还是会传递给 Activity 的,于是就有了网上的说法,事件传递是由外向内,事件消耗是由内向外传递的

分发过程中几个重要的方法:

  • dispatchTouchEvent(MotionEvent)

  • onInterceptTouchEvent(MotionEvent)

  • onTouchEvent(MotionEvent)

  • requestDisallowInterceptTouchEvent(boolean)

下面开始一步一步详细分析源码:

Activity 对事件的分发

事件分发的第一个回调方法就是 dispatchTouchEvent,每次都会调用

/**
 * Activity#dispatchTouchEvent
 * You can override this to intercept all touch screen events before they are dispatched to the window.           
 * @return boolean Return true if this event was consumed.
 */
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}

说明:
onUserInteraction() 是一个空实现的方法,官方示意为:实现这个方法,就会告诉你用户与设备已经开始交互了;与之对应的还有一个 onUserLeaveHint(),这两个方法可以配合起来,来决定状态栏显示通知和取消的时机。

第二个 if 是重点,如果 Window.superDispatchTouchEvent(ev) 返回 true,那么事件被消费,到此就结束了。如果返回的 false,即事件一级一级向下传递,直至到最后一个 View#OnTouchEvent() 全部返回了 false,那么最终会回调到 Activity#onTouchEvent() 方法。

getWindow() 返回的就是一个 Window,是一个抽象类,它的唯一实现类是 PhoneWindow,Window#superDispatchTouchEvent(MotionEvent)也是一个抽象方法,所以事件是传递到了PhoneWindow#superDispatchTouchEvent(MotionEvent)

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

我们看到事件进一步通过 mDecor 进行分发了,DecorView 就是我们在 Activity 里边通过 setContentView(xxx) 设置的布局挂载到的一个顶级父 View,换句话说,我们在 Activity 中通过 setContentView(xxx) 设置的 View,其实就是 DecorView的一个子 View。

DecorView 继承自 FrameLayout,是 ViewGroup 类型。

简单贴下 DecorView 的代码,下一篇会分析 Activity 的 setContentView(xxx) 到底是怎么加载我们的布局的

Activity#setContentView(int layoutResID) -> 会回调到 PhoneWindow#setContentView(int)


// DecorView的实例化过程
public class PhoneWindow extends Window implements MenuBuilder.Callback {
    // This is the top-level view of the window, containing the window decor.
    private DecorView mDecor;
    ......
    
    // PhoneWindow#setContentView(int)
    @Override
    public void setContentView(int layoutResID) {
         // installDecor() new 出来一个对象
        if (mContentParent == null) {
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
    }
    ......
}
说明:
installDecor() -> 回调到 generateDecor(int),在该方法中最终通过 new DecorView(Context, int, PhoneWindow,WindowManager.LayoutParams) 出来的,留到下一篇分析
    ......
}


// 在 API 27,DecorView 单独是一个类,是 ViewGroup 类型
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
    ......
    // DecorView#superDispatchTouchEvent(MotionEvent)
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event); // 实际调用的就是 ViewGrup 的方法
    }
    ......
}

看到这里,我们已经知道了,其实啥也没干,事件就是简单的从 Activity 传到了 ViewGroup 的dispatchTouchEvent()。

ViewGroup 的事件分发过程

ViewGroup 对事件的分发,就是通过 ViewGroup#dispatchTouchEvent(MotionEvent) 来进行事件传递的过程

    // ViewGroup#dispatchTouchEvent(MotionEvent)
    // 这个方法每次都会调用
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
    ......
        boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)) {
            // 该 if 条件默认返回 true;除非当前的 Window 被另一个可见的 Window 部分或者全部遮挡掉了,就会丢弃掉该事件
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;

            // 代码片段一 
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                // 这个方法很关键,只要是 DOWN 事件传递到这里,会清除一些状态
                // 注意 执行完 cancelAndClearTouchTargets(ev) 方法后 mFirstTouchTarget == null,
                // 这个具体是什么等下细说
                // 先记住一个结论:事件如果能够正常传递给子 View,并且被子 View 消费掉,
                // 那么mFirstTouchTarget 就会被赋值(即 mFirstTouchTarget != null)
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }


            // 代码片段二  这个地方检测 ViewGroup 是否拦截事件
            // DOWN 事件时,这个 mFirstTouchTarget == null,会判断ViewGroup 是否拦截事件
            // 情况一:ViewGroup 拦截事件,即onInterceptTouchEvent(ev) 方法返回 true,
            // 会直接调用 ViewGroup.onTouchEvent(MotionEvent)方法自己处理事件,这个时候 mFirstTouchTarget == null;
            // 当 MOVE/UP事件到来时,该 if 条件不满足,ViewGroup.onInterceptTouchEvent(ev) 不会被调用,此时的 MOVE/UP事件直接传递到了 ViewGroup.onTouchEvent(MotionEvent)中

            // 情况二:如果 ViewGroup 不拦截事件,事件顺利传递给子 View ,并且事件被子 view 消费掉的话,
            // mFirstTouchTarget 会被赋值并指向子元素,mFirstTouchTarget != null 条件才成立;后续的 MOVE/UP 事件会走「代码片段四」进行传递
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                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;
            }
        ......

            // A. 往下走 DOWN 事件,会有两种情况
            // 情况A1:onInterceptTouchEvent默认返回 false,不拦截;不取消,即默认的 if 条件满足,
            // for 循环遍历所有的子 view,事件继续往下传递;注意:子 View 是否消费 DOWN 事件,
            // 会影响到后续的 MOVE UP等事件的传递情况(即会影响到情况 B)
            // 情况A2:如果我们重写 ViewGroup#onInterceptTouchEvent(MotionEvent)并返回 true,
            // 那么这个 if 不满足,就会走下面 ViewGroup 自己的 OnTouchEvent()方法 (即代码片段三)

            // B. 后续的MOVE/UP等事件 走到这里时,if条件不满足,又分两种情况:
            // 情况B1:如果子 View 消费了 DOWN 事件,那么会直接走到 「代码片段四 」,进行事件的传递
            // 情况B2:如果子 View 不消费 DOWN 事件,那么事件就会交给父View 处理,会走「代码片段三 」
            TouchTarget newTouchTarget = null;  // TouchTarget在这里声明

            // 先看不拦截事件的情况
            if (!canceled && !intercepted) {
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                    // 除了 DOWN 事件,后续的 MOVE 和 UP 事件进不来
                    final int childrenCount = mChildrenCount;

                    // DOWN事件走到这里,如果同时 ViewGroup 有子 View,就继续往下走了
                    if (newTouchTarget == null && childrenCount != 0) {
                        final View[] children = mChildren;
                        // 遍历所有的子 View,
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);

                        ......
                            // 剔除中间几行不重要的代码,删掉的代码直白的说下:如果当前的某个 view 处于获取到焦点状态,
                            // 那么优先把这个事件传递给它,如果该 View 不消费,不处理的话,事件就继续正常分发下去


                            // 重点来了:这个地方分为2种情况,取决于dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)返回值
                            // 情况1:返回 true,表示子 View 消费了事件(先别管是怎么消费的)
                            // 情况2:返回 false,表示子 View 不消费事件,if 条件不满足,这个时候mFirstTouchTarget == null,就不会被赋值
                            // 这里先分析 假设消费 true 的情况,那么 if 满足,正常进入,这时候 child!=null
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {  //调用一  该 if 条件具体源码及分析 在下面
                            ......
                                // 下面这句代码执行完 mFirstTouchTarget != null
                                // 同时 newTouchTarget != null,跳出循环
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }
                        }

                    }

                ......
                }
            }


            // Dispatch to touch targets.
            if (mFirstTouchTarget == null) {
                // 代码片段三
                // 这里有3种情况,都会走到这里
                // 1. ViewGroup 主动拦截事件
                // 2. ViewGroup 没有子 View
                // 3. ViewGroup 有子 View,但是都不消费事件 dispatchTransformedTouchEvent() 返回了 false

                // 注意这个时候,参数三 child传的为 null -> 会调用 ViewGroup.onTouchEvent(MotionEvent)
                // 调用二
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                // intercepted = false 的情况会 即 ViewGroup#onInterceptTouchEvent(ev) 返回了 false,不拦截事件,同时事件正常的传递到了目标子 View
                // 代码片段四 
                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;
                        // 子 View 消费了 DOWN 事件,那么后续的 MOVE/UP 等事件,就是在这里传递给对应的子 View 的 
                        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;
    }

默认第一次,就是DOWN事件传递到代码片段二的时候,if 满足条件,同时mFirstTouchTarget == null;是否拦截事件,这时候取决于 FLAG_DISALLOW_INTERCEPT,默认disallowIntercept = false,直接会走 ViewGroup#onInterceptTouchEvent(MotionEvent),这个值是子 View通过调用 requestDisallowInterceptTouchEvent(true) 来改变的,子 View 一旦调用设置后,ViewGroup将无法拦截除了 DOWN 以外的其它事件。

之所以说除了 DOWN 以外的其它事件,是因为每次当 DOWN 事件传递到 ViewGroup 的 dispatchTouchEvent()时候,会调用「代码片段一」 resetTouchState();标记位 FLAG_DISALLOW_INTERCEPT会被重置,这将导致子 View 中设置的这个标记无效;

所以,当 DOWN 事件传递到 ViewGroup 时,ViewGroup 总是会调用自己的 onInterceptTouchEvent() 来询问是否要拦截事件;

    /** 常量的声明 转为二进制位:1000 0000 0000 0000
     * When set, this ViewGroup should not intercept touch events.
     * {@hide}
     */
    protected static final int FLAG_DISALLOW_INTERCEPT = 0x80000;

    // 子 View 调用,从而影响到父 View 是否能拦截事件
    @Override
    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
        ......
        if (disallowIntercept) {
            mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
        } else {
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }

        // Pass it up to our parent
        if (mParent != null) {
            mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
        }
    }

《Android 事件分发机制源码详解-最新 API》 image

    # ViewGroup#onInterceptTouchEvent(MotionEvent)
    /**
     * 可以实现该方法拦截所有的触摸事件,
     * 返回 true,就会调用自己的onTouchEvent()
     * 返回 false,有可能会调用子 View 的 onTouchEvent()
     */
public boolean onInterceptTouchEvent(MotionEvent ev) {
    if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
            && ev.getAction() == MotionEvent.ACTION_DOWN
            && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
            && isOnScrollbarThumb(ev.getX(), ev.getY())) {
        return true;
    }
    return false;
}
    /**
     * View 的事件分发:把MotionEvent 传递给相关的 childview,如果传递过来的 child == null,那么该 ViewGroup 的onTouchEvent(MotionEvent)方法就会被调用
     */
    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;

        // DOWN 事件传递过来,if 不成立
        final int oldAction = event.getAction();
        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
            event.setAction(MotionEvent.ACTION_CANCEL);
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;
        }

       ......
        
        // 重点  这里有2种情况
        // 情况1:当前 ViewGroup 是有子 View 的情况,此时传过来的 child != null,由此事件就传递到了具体的 childView 了,事件消耗与否取决于子 View
        // 情况2:ViewGroup 拦截了事件,此时传递过来的 child == null
        if (child == null) {
            // 调用 super 的方法,最终会调用到 ViewGroup#onTouchEvent(MotionEvent)里
            handled = super.dispatchTouchEvent(transformedEvent);
        } else {
            ......
            // 事件继续向下传递,由此事件已经从父 View 传递给了下一层 View,接下来的传递过程与顶级父 View 的传递过程是一致的,如此循环,完成整个事件的分发
            handled = child.dispatchTouchEvent(transformedEvent);
        }
        
        return handled;
    }

接下来继续往下分析 View.dispatchTouchEvent(MotionEvent)

View 的事件分发过程


    /**
     * View#dispatchTouchEvent(MotionEvent)
     * Pass the touch screen motion event down to the target view, or this
     * view if it is the target.
     * @return True 表示当前 view 消费掉此事件
     */
    public boolean dispatchTouchEvent(MotionEvent event) {
        ......
        boolean result = false;
        final int actionMasked = event.getActionMasked();
       
        if (onFilterTouchEventForSecurity(event)) {
            // 默认情况下都是会进来的;除非当前的 Window 被另一个可见的 Window 部分或者全部遮挡掉了,就会丢弃掉该事件
            
            // 重中之重的地方来了;首先 ListenerInfo 是 View.java 里边一个静态类,里边封装了各种监听事件,比如 焦点变化的监听,滑动状态改变的监听 点击、长按事件、触摸事件的监听等等
            //这里我们如果设置了 触摸事件,那么就会回调到触摸事件的 onTouch()方法中,根据返回值决定了 result 的值
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

            
            // 情况一:如果设置了触摸事件,并且li.mOnTouchListener.onTouch(this, event)返回 true,表示事件被消费掉了,不再往下传递;那么 View.onTouchEvent(MotionEvent)方法就不会执行
            // 情况二:如果设置了触摸事件,但是返回了 false,或者用户就没有设置触摸事件,那么最终就会回调到 onTouchEvent(event)方法
            // 由此我们可以看到设置的触摸事件的优先级会高于OnTouchEvent(),给用户提供一个外部处理触摸事件的回调,可以提前做一些事情
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }
        ......
        return result;
    }

说明:

如果是从上边 「调用二」super.dispatchTouchEvent(transformedEvent) 调用的,那么就会调用 ViewGroup.onTouchEvent(MotionEvent),事件就交给 ViewGroup 自己处理;

如果是具体的 View 调用的「调用一」,那么事件就相当于传递到末端了,是否消费事件取决于 onTouch(View, MotionEvent) 和 onTouchEvent(MotionEvent)这两个方法的返回值;

如果最终的 result = true,那么就表示事件被子 View 消费掉了,事件不再往上传递(子 View 都不处理,那么事件就会一层一层的传递给父 View ,父 View 的 OnTouchEvent(MotionEvent) 就会被调用)

View 的点击事件处理

我们给 View 设置的点击事件到目前为止还没有看到,实际上,点击事件的回调时机是在 View#onTouchEvent(MotionEvent) 的 case MotionEvent.ACTION_UP: 手指抬起中进行回调的,简单的贴下代码

public boolean onTouchEvent(MotionEvent event) {
    final int action = event.getAction();
    // 值得一提的是,只要设置了点击事件或者长按事件,就会改变 viewFlags 的值,clickable = true
    final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
            || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
            || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
    ......
    if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
        switch (action) { 
            case MotionEvent.ACTION_UP:
                ......
                 // Only perform take click actions if we were in the pressed state
                 if (!focusTaken) {
                     // Use a Runnable and post this rather than calling
                     // performClick directly. This lets other visual state
                     // of the view update before click actions start.
                     if (mPerformClick == null) {
                         mPerformClick = new PerformClick();
                     }
                     // 点击事件
                     if (!post(mPerformClick)) {
                         performClick();
                     }
                  }
                ......

                break;
                ......
        }
        // 只要是能够进入 if 条件,默认都是返回 true 的,即消费掉这个事件
        return true;
    }
    

总结

分析了源码可以得到的结论

  1. ViewGroup 默认不拦截任何事件,ViewGroup#onInterceptTouchEvent(MotionEvent) 方法默认返回 false

  2. ViewGroup#onInterceptTouchEvent(MotionEvent) 是用来拦截某个事件的,如果当前 ViewGroup 拦截了某个事件(这个事件可能是 DOWN 或者其它事件),那么同一个事件序列中的事件再次传递过来时,该方法不会被再次调用,而是直接调用了 ViewGroup#onTouchEvent(MotionEvent)方法

  3. 事件传递到某个 View,如果它不消耗 DOWN 事件( onTouchEvent(MotionEvent) 返回了 false),那么后续的MOVE UP 等事件都不会再传递给它,并且事件将重新交由它的父 View 去处理,即父 View 的 onTouchEvent(MotionEvent)会被调用。(mFirstTouchTarget == null,走「代码片段三」的情况)

  4. View 没有onInterceptTouchEvent(MotionEvent) 方法,一旦事件传递给它,那么它的 onTouchEvent(MotionEvent) 就会被调用

  5. onTouchEvent(MotionEvent) 返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前 View 无法再次接收到其它事件

  6. 如果给 View 设置了setOnTouchListener() 和 onTouchEvent(),要想两者都可以执行,触摸事件 onTouch() 返回 false 即可

事件的传递过程文字描述

事件的传递顺序是 Activity -> Window -> ViewGroup -> View,当一个点击事件产生后,就会按如下顺序传递到 父 ViewGroup 的 dispatchTouchEvent(),如果 ViewGroup 拦截事件,那么事件会直接传递到 ViewGroup 的 onTouchEvent() 方法中;如果父 ViewGroup 不拦截事件,那么就会 for 循环遍历子 View,进行事件的下发,如果事件往下传递的过程中有一个子 View 的 onTouchEvent() 返回 true消费掉事件,那么事件到此为止;

考虑一种情况:所有子 View 都不处理事件,那么这个事件就会传递到父 View 的 onTouchEvent(),如果父 View 也不消费事件,那么这个事件就会一级一级往上一个父 View 传递,如果所有的 View 都不消费事件,那么这个事件最终会传递到 Activity 的 onTouchEvent(MotionEvent),最终 Activity 默认也是把该事件丢弃掉的,但是源码里边给了我们一个思路,就是说如果事件是在 Window 的边界外产生的,那么我们就可以重写 Activity.onTouchEvent(MotionEvent) 来处理

事件传递机制流程图

最后贴一张图 Android 事件分发的流程图:

《Android 事件分发机制源码详解-最新 API》 image

伪代码展示事件分发拦截和消费三者的关系

一段有意思的伪代码[伪代码来源于艺术探索一书]:

public boolean dispatchTouchEvent(MotionEvent event) {
    boolean consume = false;
    if (onInterceptTouchEvent(event)) {
        consume = onTouchEvent(event);
    } else {
        consume = childView.dispatchTouchEvent(event);
    }
    return consume;
}

推荐阅读:
手写酷狗侧滑菜单效果
Jenkins 自动化构建 Android 项目图文教程(一)
Jenkins 自动化构建 Android 项目图文教程(二)

《Android 事件分发机制源码详解-最新 API》 微信扫码关注,接收更多更全文章

    原文作者:拂晓是个小人物
    原文地址: https://www.jianshu.com/p/468c46dcc948
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞