Android源码系列六:事件分发机制

基础

在分析事件分发之前,我们先来了解三个相关的重要知识点:

  1. 事件分发的对象是什么?
  2. 谁在分发?
  3. 依赖于什么分发?

我们先把这三个问题弄清楚了再来看具体的原理:

  1. 事件分发的对象就是我们的触摸屏幕产生的 Touch 事件。最常见的事件类型为:down、up、move和cancel。

    • MotionEvent.ACTION_DOWN:触摸到屏幕所立即产生的事件类型
    • MotionEvent.ACTION_UP:离开屏幕所产生的事件类型
    • MotionEvent.ACTION_MOVE:在屏幕上移动所产生的事件类型
    • MotionEvent.ACTION_CANCEL:事件被取消,非人为的取消,比如:关机、锁屏
  2. 事件分发是在 ActivityViewGroupView 三者之间传递的过程,事件先传递到 Activity,接着传递到 ViewGroup,最后传给了 View。如下图 ① -> ② -> ③:

  3. 事件分发主要就是通过三个方法在上述三者之间传递:

    • dispatchTouchEvent(ev: MotionEvent?): Boolean:分发、派发事件的方法
    • onInterceptTouchEvent(ev: MotionEvent?): Boolean:拦截事件的方法
    • onTouchEvent(event: MotionEvent?): Boolean:消费事件的方法

    通过下方的表格来认识一下三个方法和三者之间的拥有关系:

    dispatchTouchEvent() onInterceptTouchEvent() onTouchEvent()
    Activity 可以分发 无拦截方法 可消费
    ViewGroup 可以分发 有拦截方法 可消费
    View 可以分发 无拦截方法 可消费

    理解了上面三个问题之后,我们就可以放心大胆的去通过源码去了解事件分发的原理了。

入口 Activity

首先看看 ActivitydispatchTouchEvent() 方法:

/**
 * 事件分发的入口,可以override
 */
public boolean dispatchTouchEvent(MotionEvent ev) {
    // 如果事件类型为down,那么先执行一遍onUserInteraction()
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    // getWindow()是获取Window对象,但是Window是抽象类,唯一实现类为PhoneWindow
    // 可直接查看PhoneWindow.superDispatchTouchEvent(ev)方法
    // 顺藤摸瓜下去就可以看到实际上调用的是ViewGroup.dispatchTouchEvent(ev)方法
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}

/**
 * 用于和用户交互
 * 此方法为空方法,可override
 */
public void onUserInteraction() {
}

/**
 * 当前Activity下的任何一个View都没有处理点击事件,就会执行此方法
 * 此方法用于处理Window边界外的点击事件
 */
public boolean onTouchEvent(MotionEvent event) {
    if (mWindow.shouldCloseOnTouch(this, event)) {
        finish();
        return true;
    }

    return false;
}

Activity 的事件分发代码还是比较简洁的,我们大致理一下流程:

  • 接收到 down 事件,执行 onUserInteraction()
  • 如果 ViewGroup.dispatchTouchEvent(ev) 返回为 true,那么就直接返回 true,结束;
  • 如果 ViewGroup.dispatchTouchEvent(ev) 返回为 false ,调用自身的 onTouchEvent(ev) 方法,结束。

结合下方的流程图更容易理解:


中间人ViewGroup

理解了 Activity 对事件的处理之后,我们趁热打铁来分析一下 ViewGroup 对事件的分发、拦截和消费,因为它涉及到了三个方法,可想而知难度会提升一点。

来看看 ViewGroup 是如何分发、拦截和消费事件的:

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    // 标志是否分发了此点击事件
    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)) {
        // 判断是否拦截此点击事件
        final boolean intercepted;
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {
            // disallowIntercept = 是否禁用事件拦截的功能(默认是false),可通过调用requestDisallowInterceptTouchEvent()修改
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            // 可以拦截
            if (!disallowIntercept) {
                // 调用拦截事件方法,默认返回false
                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;
        }
        // 如果没有被取消而且没有被自身拦截,那就向下(子View)分发
        if (!cancel && !intercepted) {
            // 循环每个子View,调用子View的dispatchTouchEvent
            for (int i = childrenCount - 1; i >= 0; i--) {
                final View child = getAndVerifyPreorderedView(
                        preorderedList, children, childIndex);
                // 实际就是调用子View的dispatchTouchEvent(ev)
                if(dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)){
                    // 如果子View分发成功,那么将child赋值到mFirstTouchTarget对象的next指针中
                    // mFirstTouchTarget是TouchTarget对象的引用,TouchTarget类似链表结构
                    addTouchTarget(child,idBitsToAssign);
                    alreadyDispatchedToNewTouchTarget = true;
                }
            }
        }
        // 如果mFirstTouchTarget为空,说明没有执行上面的addTouchTarget(child,idBitsToAssign)方法
        // 那就代表事件要么被取消了,要么被拦截了,这时候dispatchTransformedTouchEvent()第三个参数传的是null,会在下面介绍
        if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
        }else{
            // alreadyDispatchedToNewTouchTarget在上面循环的时候,如果分发成功,它就为true,handled就为true
            if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    }
        }
        return handled;
    }
}

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) {
    final boolean handled;

    final int oldAction = event.getAction();
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
        event.setAction(MotionEvent.ACTION_CANCEL);
        if (child == null) {
            // 如果子View为空,那么就调用super.dispatchTouchEvent(ev) == View.dispatchTouchEvent(ev)
            // ViewGroup是View的子类,所以在super中的事件将被ViewGroup代替
            handled = super.dispatchTouchEvent(event);
        } else {
            // 子View不为空,调用View.dispatchTouchEvent(ev)
            handled = child.dispatchTouchEvent(event);
        }
        event.setAction(oldAction);
        return handled;
    }
}

/**
 * 可覆写,返回true表示拦截,false表示不拦截
 */
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;
}

上面的注释一定要仔细看,看完如果还有点不理解,没关系,我们接着理一下 ViewGroup 的整体思路:

  • dispatchTouchEvent() 方法中,首先执行 onInterceptTouchEvent(ev) 方法,看是否被拦截;
  • 如果被拦截,那么就不分发事件到子 ViewdispatchTouchEvent(ev),而是分发到 super.dispatchTouchEvent() ,由于 ViewGroup 的父类就是 View,所以还是会执行 View.dispatchTouchEvent(ev) 方法,这里先不急着弄懂这步,只要知道流程就行,将会在下节介绍;
  • 如果没有被拦截,直接循环子 View ,调用子 ViewdispathcTouchEvent(ev) 方法。

到这里大致的流程就清晰了,再结合下方的流程图加深下理解:


最后接收人View

    public boolean dispatchTouchEvent(MotionEvent event) {
        boolean result = false;
        ListenerInfo li = mListenerInfo;
        // 监听信息不为空
        // OnTouchListener不为空,也就是setOnTouchListener()
        // 当前view的enable为true
        // OnTouchListener接口中的boolean onTouch(View v, MotionEvent event)方法需要覆写
        // 缺一不可
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }

        // 如果上面不满足,那么看onTouchEvent方法是否返回true,如果返回true,那么result也为true,否则为false
        // 其实这个if()也就等于:return onTouchEvent(event),仔细体会下
        if (!result && onTouchEvent(event)) {
            result = true;
        }
        return result;
    }

    public boolean onTouchEvent(MotionEvent event) {
        // 只要设置了单击和长按事件,clickable就为true
        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            // 调用点击事件
            if (mPerformClick == null) {
                mPerformClick = new PerformClick();
            }
            if (!post(mPerformClick)) {
                performClickInternal();
            }
            return true;
        }
        return false;
    }

View 的事件分发相当于 ViewGroup 就简单多了,流程在源码中也体现的很清楚,我们来理一下整个流程:

  • 执行到 dispatchTouchEvent(ev) 的时候,先去调用 onTouch(view,ev) 方法;
  • 如果 onTouch() 返回 false,才去调用下面的 onTouchEvent(ev) 方法;
  • onTouchEvent(ev) 方法内部,可以看见执行了 performClickInternal() 方法,这个方法就是我们常见的 onClick()

以上三点都是需要满足种种条件才可执行的,条件都在源码注释中详细解释了,大家一定要把这些条件看清楚了。

到这里我们需要解决在 ViewGroup 中留下的一个问题,那么就是当 ViewGroup 没有子 View 的时候,调用了一个 super.dispatchTouchEvent(ev) 方法,其实就是把 ViewGrouponTouch()onTouchEvent()onClick() 三个方法按照条件来执行,千万不要以为调用父类的方法还是执行子 View 的事件分发!

View 的事件分发主要流程参考下方的流程图:


事件分发的流程说难吧其实理清了也不难,说不难吧还是挺绕的,最后总结下几点:

  • 事件分发的顺序是:Activity -> ViewGroup -> View

  • ActivityView 都没有拦截事件,Activity 如果存在拦截事件,那么整个页面都响应不了点击事件了,View 因为是最后一层,拦不拦截都没必要了;

  • ViewGroup 在无子 View 接收分发事件或子 View 分发事件返回 false 的时候,会调用自身的 onTouchonTouchEvent()onClick() 方法。

大家一定要好好体会Android事件分发机制,无论是在日常开发中还是面试中,都是必不可缺的知识点!源码分析的文章还在不断的更新,如果本文章你发现有不正确或者不足之处,欢迎你在下方留言或者扫描下方的二维码留言也可!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章