事件分发机制

1、触摸事件分发流程

在《Activity、Window、DecorView以及ViewRoot层级关系全解析》一节有下图: alt

我们知道View的结构是树形结构,View可以放在ViewGroup中,ViewGroup也可以放在另一个ViewGroup中,如此就形成了层层嵌套的关系。当我们触摸到屏幕后,就会生成一个Touch事件,常见的Touch事件有:

  • MotionEvent.ACTION_DOWN:按下
  • MotionEvent.ACTION_MOVE:滑动
  • MotionEvent.ACTION_CANCEL:非人为原因结束本次事件
  • MotionEvent.ACTION_UP:抬起 一般来说,一个事件会经过:按下 –》 滑动 –》抬起,这三个阶段,并在这个过程中会有非人为原因结束本次触摸流程。这些事件会在代码里会封装成一个MotionEvent。那么,当MotionEvent产生后,系统就会将其传递给View树,MotionEvent在View的层级传递,并最终得到处理的过程,就是触摸事件分发流程。一个流程的传递顺序是:

Activity/Window –> ViewGroup –> View

其中,View就是各种控件,如Button、TextView等,而ViewGroup是View的子类,因此本质上也是一个View,只不过ViewGroup可以包含多个子View和定义布局参数。

2、触摸事件分发的3个重要方法

有以下3个重要方法是必须掌握的:

  • dispatchTouchEvent(MotionEvent ev):进行事件的分发,在View和ViewGroup类都有该方法,下文会对该方法的源码进行分析,需要区分清楚;
  • onInterceptTouchEvent(MotionEvent ev):进行事件拦截,在dispatchTouchEvent()中调用,在分发的过程中判断是否需要进行拦截,需要注意的是只有ViewGroup有该方法,View是没有提供该方法的。如果返回true代表拦截,返回false代表不拦截;
  • onTouchEvent(MotionEvent ev):触摸事件处理,同样在dispatchTouchEvent()方法中进行调用,如果返回true代表已处理事件,返回false代表不处理事件,事件继续传递。

为了更好的了解三者的关系,我们从源码出发,首先看看ViewGroup的dispatchTouchEvent(),源码是Android9.0.0=》/frameworks/base/core/java/android/view/ViewGroup.java:


public boolean dispatchTouchEvent(MotionEvent ev) {
    ...
    if (!disallowIntercept) {
        intercepted = onInterceptTouchEvent(ev);  //1
        ev.setAction(action); // restore action in case it was changed
    } else {
        intercepted = false;
    }
    ...
    if (actionMasked == MotionEvent.ACTION_DOWN
            || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
            || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
        ...
        if (newTouchTarget == null && childrenCount != 0) {
            ...
            for (int i = childrenCount - 1; i >= 0; i--) { //2:遍历ViewGroup的子View
                final int childIndex = getAndVerifyPreorderedIndex(
                        childrenCount, i, customOrder);
                final View child = getAndVerifyPreorderedView(
                        preorderedList, children, childIndex);

                ...
                resetCancelNextUpFlag(child);
                if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { //3
                    ...
                    break;
                }

                ...
            }
        }
    }
}

在[注释1]调用了onInterceptTouchEvent()方法来判断是否要拦截当前的事件。

  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;
    }

可以看出onInterceptTouchEvent()默认返回false,代表不拦截。接着在[注释2]遍历ViewGroup的子View,如果子View可以接收到触摸事件,则会执行[注释3]dispatchTransformedTouchEvent():

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
                                              View child, int desiredPointerIdBits) {
    ...
    if (child == null) {
        handled = super.dispatchTouchEvent(transformedEvent); //4
    } else {
        final float offsetX = mScrollX - child.mLeft;
        final float offsetY = mScrollY - child.mTop;
        transformedEvent.offsetLocation(offsetX, offsetY);
        if (! child.hasIdentityMatrix()) {
            transformedEvent.transform(child.getInverseMatrix());
        }

        handled = child.dispatchTouchEvent(transformedEvent); //5
    }
    ...
    return handled;
}

在[注释4],如果当前的ViewGroup没有View则调用父类的dispatchTouchEvent(),在[注释5]如果有子View,则调用子View的dispatchTouchEvent()。因为ViewGroup类也是继承View类的,因此[注释4]super.dispatchTouchEvent(transformedEvent)对应的源码在:/frameworks/base/core/java/android/view/View.java

public boolean dispatchTouchEvent(MotionEvent event) {
    ...
    if (onFilterTouchEventForSecurity(event)) {
        if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
            result = true;
        }
        //noinspection SimplifiableIfStatement
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null ///6
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) { //7
            result = true;
        }

        if (!result && onTouchEvent(event)) { //8
            result = true;
        }
    }
    ...
    return result;
}

这个函数要稍微捋一捋,如果[注释6]mOnTouchListener不为空且[注释7]mOnTouchListener.onTouch()返回true,则result设置为true,因此[注释8]的onTouchEvent(event)就不执行,反之则执行。

最后,看看三大重要方法最后一个的onTouchEvent(),同样在View.java里:

  public boolean onTouchEvent(MotionEvent event) {
        ...
        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE //9
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) //10
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
        ...
        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
        switch (action) {
            case MotionEvent.ACTION_UP: // 11
                ...
                if (mPerformClick == null) {
                    mPerformClick = new PerformClick(); //12
                }
                if (!post(mPerformClick)) {
                    performClickInternal();
                }
            ...
            break;
            case MotionEvent.ACTION_DOWN:
            ...
            break;
            case MotionEvent.ACTION_CANCEL:
            ...
            break;
            case MotionEvent.ACTION_MOVE:
            ...
            break;
        }
        return false;
    }

从上面代码知道,[注释9]的CLICKABLE(点击)和[注释10]的LONG_CLICKABLE(长按点击)有一个为true,则会进入switch循环处理这个事件。最后在ACTION_UP[注释11]事件会调用performClick()[注释12]方法:

public boolean performClick() {
    final boolean result;
    final ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnClickListener != null) {
        playSoundEffect(SoundEffectConstants.CLICK);
        li.mOnClickListener.onClick(this);
        result = true;
    } else {
        result = false;
    }

    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
    return result;
}

如果View设置了点击事件OnClickListener,那么它的onClick()方法就会被执行。

3、onTouch()、onTouchEvent()、onClick()傻傻分不清?

这三个方法,相信很多读者都经常见过,到底谁的优先级更高呢?结合[注释7][注释8][注释12],可以看出,如果执行了onTouch()并返回false才会执行onTouchEvent(),在执行onTouchEvent()时如果调用了setOnClickListener()注册了点击事件回调,则还会执行OnClick(),因此三者的优先级为:

onTouch() > onTouchEvent() > OnClick()

4、事件分发总结

4.1 表格总结

上述源码分析如果觉得还有点懵,我们做个总结:

alt

上面表格里的“事件列”是指一个事件序列有:ACTION_DOWN、ACTION_MOVE、ACTION_UP等。大部分情况由上面的表格就可以概括了。

4.2 伪代码表示

如果用伪代码可以如下表示:

// 父View调用dispatchTouchEvent()开始分发事件
public boolean dispatchTouchEvent(MotionEvent event){
    boolean consume = false;
    // 父View决定是否拦截事件
    if(onInterceptTouchEvent(event)){
        // 父View调用onTouchEvent(event)消费事件,如果该方法返回true,表示
        // 该View消费了该事件,后续该事件序列的事件(Down、Move、Up)将不会在传递
        // 该其他View。
        consume = onTouchEvent(event);
    }else{
        // 调用子View的dispatchTouchEvent(event)方法继续分发事件
        consume = child.dispatchTouchEvent(event);
    }
    return consume;
}

以下总结的非常好,因此直接抄录至刘偶像的博客,最后升华下本文的理解:参考文献

4.3 事件由上到下的传递规则

当点击事件产生后会由Activity来处理再传递给Window再传递给顶层的ViewGroup,一般在事件传递中只考虑ViewGroup的onInterceptTouchEvent()方法,因为一般情况我们不会去重写dispatchTouchEvent()方法。

对于根ViewGroup,点击事件首先传递给它的dispatchTouchEvent()方法,如果该ViewGroup的onInterceptTouchEvent()方法返回true,则表示它要拦截这个事件,这个事件就会交给它的onTouchEvent()方法处理,如果onInterceptTouchEvent()方法返回false,则表示它不拦截这个事件,则交给它的子元素的dispatchTouchEvent()来处理,如此的反复下去。如果传递给最底层的View,View是没有子View的,就会调用View的dispatchTouchEvent()方法,一般情况下最终会调用View的onTouchEvent()方法。

举个现实的例子,就是我们的应用产生了重大的bug,这个bug首先会汇报给技术总监那:

技术总监(顶层ViewGroup)→技术经理(中层ViewGroup)→工程师(底层View)
技术总监不拦截,把bug分给了技术经理,技术经理不拦截把bug分给了工程师,工程师没有下属只有自己处理了。
事件由上而下传递返回值规则为:true,拦截,不继续向下传递;false,不拦截,继续向下传递。

4.4 事件由下而上的传递规则

点击事件传给最底层的View,如果他的onTouchEvent()方法返回true,则事件由最底层的View消耗并处理了,如果返回false则表示该View不做处理,则传递给父View的onTouchEvent()处理,如果父View的onTouchEvent()仍旧返回返回false,则继续传递给改父View的父View处理,如此的反复下去。

再返回我们现实的例子,工程师发现这个bug太难搞不定(onTouchEvent()返回false),他只能交给上级技术经理处理,如果技术经理也搞不定(onTouchEvent()返回false),那就把bug传给技术总监,技术总监一看bug很简单就解决了(onTouchEvent()返回true)。

事件由下而上传递返回值规则为:true,处理了,不继续向上传递;false,不处理,继续向上传递。