Android 事件分发机制的源码分析

作者 : 开心源码 本文共9724个字,预计阅读时间需要25分钟 发布时间: 2022-05-12 共216人阅读

问题

在进行正文之前,我们带着以下几个问题有目的的进行,而后最后再做问题的处理。

  • 问题 1:activity、 ViewGroup和 View 都不消费 ACTION_DOWN,那么 ACTION_MOVE 和 ACTION_UP 事件是怎样传递的?
  • 问题 2:在 ViewGroup 中的 onTouchEvent 中消费 ACTION_DOWN 事件(onInterceptTouch 默认设置),那么 ACTION_MOVE 和 ACTION_UP 事件是怎样传递的?
  • 在一个列表中,同时对父 View 和子 View 设置点击方法,优先响应哪个?为什么会这样?
  • 为什么子 View 不消费 ACTION_DOWN,之后的所有事件都不会向下传递了。

基础认识

事件分发的对象

首先我们要清楚,事件分发的对象是什么?其实就 MotionEvent,这个 MotionEvent 可以有 ACTION_DOWN,ACTION_UP,ACTION_MOVE,ACTION_CANCEL 等事件类型。

事件(MotionEvent)分发是在哪些对象中进行的?

Activity -> Window -> ViewGroup -> View

重点关注的方法

  • dispatchTouchEvent
  • onInterceptTouchEvent
  • onTouchEvent

ACTION_DOWN 事件在 Activity、ViewGroup、View 中根据不同函数不同的返回值的走向?

关于事件的走向,我觉的以下这张图可以很清晰的看出事件的最终走向,该图来自Kelin

注:流程图来之 Kelin

以下是使用文字对事件走向的形容,帮助对流程图的了解。

结论

  1. dispatchTouchEvent 和 onTouchEvent 中返回 true,ACTION_DOWN 事件就在此终止,不会再往上传也不会往下传了。
  2. dispatchTouchEvent 和 onTouchEvent 中返回 false,ACTION_DOWN 事件交给父控件的 onTouchEvent 进行解决
  3. onInterceptTouchEvent 一旦返回 true,那么 ViewGroup 之后不会再调用 onInterceptTouchEvent

事件分发源码分析

Activity

一旦事件产生,那么首先被调用的是 Activity 中的 dispatchTouchEvent 方法。我们来看看这个方法的实现。

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

首先,假如是 ACTION_DOWN,那么 onUserInteraction 方法就会被调用,onUserInteraction 是个空的方法,当事件产生时,那么这个方法就会被调用,假如在 activity 运行的时候,我们想要知道客户和设施的交互,那么我们即可以实现这个方法。

接着 window 中的 superDispatchTouchEvent 方法被调用,事件传递到 window 中。Window 是个笼统类,他的唯一实现是 PhoneWindow。在 Window.superDispatchTouchEvent 中调用了 DecorView 的 superDispatchTouchEvent,而这个 DecorView 就是我们在 activity 中通过调用 setContentView 设置的布局的顶层 View。

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

最后,假如 Activity 中的所有下级事件承载对象没有解决事件,最后 Activity 中的 onTouchEvent 就会被调用,当事件超出边界或者者事件为 ACTION_DOWN 时,mWindow.shouldCloseOnTouch(this, event) 为 true,onTouchEvent 默认是返回 false 的。

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

onTouchEvent 方法 当所有的 view 都没有消费事件的时候,activity 的 onTouchEvent() 就会被调用

我们这边看一下 getWindow() 里面的实现,其实也就是返回一个 Window 实例

    public Window getWindow() {        return mWindow;    }

Window(PhoneWindow)

Window 在事件分发的过程中就相似于一个中间的桥接一样,是没有做什么操作的,只是将事件传递给 DecorView 中。

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

ViewGroup

  • 要了解 ViewGroup 对于事件的解决,我们先来个简化的代码逻辑来帮助我们了解
void dispathTouchEvent(Event event){    boolean consume = false;    if(OnInterceptTouchEvent(event)){        consume = onTouchEvent(event);    }else{        consume = childView.dispatchTouchEvent(event);    }    return consume;}

对于一个 ViewGroup 来说,点击事件产生之后,dispatchTouchEvent 就会被调用,假如这个 ViewGroup 的 onInterceptTouchEvent 返回 true 就表示它要阻拦当前的事件,接着事件就会给这个 ViewGroup 解决,即它的 onTouchEvent 会被调用;假如 ViewGroup 的 onInterceptTouchEvent 返回 false,就表示它不阻拦事件,这时当前事件就会被传递给它的子 view,接着调用子元素的 dispatchTouchEvent 方法就会被调用,如此反复,直到事件被最终解决。

推荐阅读:阿里腾讯Android开发十年,到中年危机就只剩下这套移动架构体系了!

干货很长,文末有彩蛋

  • 大致流程图

  • 我们再看看 ViewGroup.dispatchTouchEvent() 复原度比较高的源码
            // 省略部分代码            // 假如是 ACTION_DOWN 事件,重置标志位 mGroupFlags 为 非 FLAG_DISALLOW_INTERCEPT,这个标志位关系到 ViewGroup 的 onInterceptTouch 能否有效。            if (actionMasked == MotionEvent.ACTION_DOWN) {                cancelAndClearTouchTargets(ev);                resetTouchState();            }            // 检查能否阻拦            final boolean intercepted;            if (actionMasked == MotionEvent.ACTION_DOWN                    || mFirstTouchTarget != null) {                    // 解决条件为:                    // 1\. 事件为 ACTION_DOWN                    // 2\. 有下级的 View 解决事件                    // 判断阻拦能否失效?mGroupFlags = FLAG_DISALLOW_INTERCEPT 时,onInterceptTouchEvent 是不会被调用的                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 {                // 非 ACTION_DOWN 事件且没有其余的下级解决该事件的时候,不会再调用 onInterceptTouchEvent                intercepted = true;            }            // 正常事件分发            // 假如 ViewGroup 决定阻拦或者者已经有 子 view 解决事件,那么就开始正常的事件分发流程            if (intercepted || mFirstTouchTarget != null) {                ev.setTargetAccessibilityFocus(false);            }            // 不阻拦事件            if (!canceled && !intercepted) {                View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()                        ? findChildWithAccessibilityFocus() : null;                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);                            // 找到接收该事件的子 View 上,假如找到,则直接跳出循环                            newTouchTarget = getTouchTarget(child);                            if (newTouchTarget != null) {                                newTouchTarget.pointerIdBits |= idBitsToAssign;                                break;                            }                            // 判断事件能否落在子 view 上,假如是,则跳出循环                            resetCancelNextUpFlag(child);                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {                                // Child wants to receive touch within its bounds.                                mLastTouchDownTime = ev.getDownTime();                                if (preorderedList != null) {                                    // childIndex points into presorted list, find original index                                    for (int j = 0; j < childrenCount; j++) {                                        if (children[childIndex] == mChildren[j]) {                                            mLastTouchDownIndex = j;                                            break;                                        }                                    }                                } else {                                    mLastTouchDownIndex = childIndex;                                }                                mLastTouchDownX = ev.getX();                                mLastTouchDownY = ev.getY();                                newTouchTarget = addTouchTarget(child, idBitsToAssign);                                alreadyDispatchedToNewTouchTarget = true;                                break;                            }                            // The accessibility focus didn't handle the event, so clear                            // the flag and do a normal dispatch to all children.                            ev.setTargetAccessibilityFocus(false);                        }                        if (preorderedList != null) preorderedList.clear();                    }                }            }

从以上的代码中可以看出,当事件传递到 ViewGroup 中的 dispatchTouchEvent 的时候,这次经历了以下的几个重要步骤:

  • step 1: 重置标志位 mGroupFlags,将 mGroupFlags 标志位设置为非 FLAG_DISALLOW_INTERCEPT,假如 mGroupFlags = FLAG_DISALLOW_INTERCEPT,那么 ViewGroup 将不再调用 onInterceptTouch(),默认 ViewGroup 不阻拦任何事件。
  • step 2: 通过判断事件能否为 ACTION_DOWN 或者 mFirstTouchTarget != null 来决定能否向下分发 ACTION_DOWN 之外的事件;
    • 若是子 View 消费 ACTION_DOWN,那么 mFirstTouchTarget 会被赋值,mFirstTouchTarget != null 不成立
    • 若是子 View 不消费 ACTION_DOWN,那么 mFirstTouchTarget 则为 null,ViewGroup 默认阻拦 ACTION_DOWN 之后的所有事件,不向下传递。
  • step 3:通过 mGroupFlags 标志位判断阻拦能否有效,若是 mGroupFlags = FLAG_DISALLOW_INTERCEPT,则 ViewGroup 默认不阻拦任何事件。
  • step 4: 循环所有的子 view
    • (1)判断能否有子 view 已经解决改事件了,假如有则跳出循环,直接向下级子 view 分发事件。
    • (2)判断点击能否落在某个子 view;
  • step 5: 假如子 view 消费了事件,那么将 mFirstTouchTarget 进行赋值,mFirstTouchTarget(链表)。

View

View 对于事件的解决要略微简单一点,注意这里的 View 并不包含 ViewGroup。我们先看看 dispatchTouchEvent 方法。

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

View 对于点击事件的解决就比较简单了,由于 View 是个单独的元素,它没有子元素,因而无法向下传递事件,所以它只能自己解决。

从上面的源码中,我们可以看出 View 对点击事件的解决过程,首先会判断有没有设置 onTouchListener,假如 onTouchListener 中的 onTouch 返回了 true,那么 onTouchEvent 就不会再被调用,可见 onTouchListener 的优先级要高于 onTouchEvent,这样的解决是方便点击事件在外界进行解决。

  • View 中的 onTouchEvent 的实现
    public boolean onTouchEvent(MotionEvent event) {        // 不可用状态下,View 仍然会消耗点击事件        if ((viewFlags & ENABLED_MASK) == DISABLED) {            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {                setPressed(false);            }            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;            return clickable;        }        // 假如设置了代理商,那么就设置代理商的方法        if (mTouchDelegate != null) {            if (mTouchDelegate.onTouchEvent(event)) {                return true;            }        }        //         if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {            switch (action) {                case MotionEvent.ACTION_UP:                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {                            // This is a tap, so remove the longpress check                            removeLongPressCallback();                            if (!focusTaken) {                                if (mPerformClick == null) {                                    mPerformClick = new PerformClick();                                }                                if (!post(mPerformClick)) {                                    performClick();                                }                            }                        }                        ....                    }                    break;            }            return true;        }        return false;    }

从上面的代码中可以看出,只需 View 的 CLICKABLE 和 LONG_CLICK 有一个为 true,那么它就会消耗这个事件,即 onTouchEvent 方法返回 true,不论它是不是 DISABLE 状态。

而后当 ACTION_UP 事件发生的时候,会触发 performClick 方法,假如 View 设置了 onClickListener,那么 performClick 方法内部就会调用它的 onClick 方法。如下所示:

    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);        notifyEnterOrExitForAutoFillIfNeeded(true);        return result;    }

到这里,点击事件的源码分析就结束了。

问题解答

问题 1:activity、 ViewGroup和 View 都不消费 ACTION_DOWN,那么 ACTION_MOVE 和 ACTION_UP 事件是怎样传递的?

  • 首先,假如大家都不消费 ACTION_DOWN,那么 ACTION_DOWN 的事件传递流程是这样的:
-> Activity.dispatchTouchEvent() -> ViewGroup1.dispatchTouchEvent() -> ViewGroup1.onInterceptTouchEvent()-> view1.dispatchTouchEvent() -> view1.onTouchEvent() -> ViewGroup1.onTouchEvent() -> Activity.onTouchEvent();
  • 接着,因为大家都不消费 ACTION_DOWN,对于 ACTION_MOVE 和 ACTION_UP 的事件传递是这样的
-> Activity.dispatchTouchEvent()-> Activity.onTouchEvent();-> 消费
  • 完整的事件分发走向

问题 2:在 ViewGroup 中的 onTouchEvent 中消费 ACTION_DOWN 事件(onInterceptTouch 默认设置),那么 ACTION_MOVE 和 ACTION_UP 事件是怎样传递的?

  • 首先,我们先分析一下 ACTION_DOWN 的事件走向,因为 ViewGroup 中的 onInterceptTouch 是默认设置的,那么 ACTION_DOWN 的事件最终在 ViewGroup 中的 onTouchEvent 方法中中止了,事件走向是这样的:
-> Activity.dispatchTouchEvent() -> ViewGroup1.dispatchTouchEvent() -> ViewGroup1.onInterceptTouchEvent()-> view1.dispatchTouchEvent() -> view1.onTouchEvent() -> ViewGroup1.onTouchEvent() 
  • 接着 ACTION_MOVE 和 ACTION_UP 的事件分发流程,之后 onInterceptTouch 和 View 中的方法都不会被调用了,事件分发如下:
-> Activity.dispatchTouchEvent() -> ViewGroup1.dispatchTouchEvent() -> ViewGroup1.onTouchEvent() 
  • 完整的事件分发走向

在一个列表中,同时对父 View 和子 View 设置点击方法,优先响应哪个?为什么会这样?

答案是优先响应子 view,起因很简单,假如先响应父 view,那么子 view 将永远无法响应,父 view 要优先响应事件,必需先调用 onInterceptTouchEvent 对事件进行阻拦,那么事件不会再往下传递,直接交给父 view 的 onTouchEvent 解决。

为什么子 View 不消费 ACTION_DOWN,之后的所有事件都不会向下传递了。

答案是:mFirstTouchTarget。当子 view 对事件进行解决的时,那么 mFirstTouchTarget 就会被赋值,若是子 view 不对事件进行解决,那么 mFirstTouchTarget 就为 null,之后 VIewGroup 就会默认阻拦所有的事件。我们可以从 dispatchTouchEvent 中找到如下代码,可以看出来,若是子 View 不解决 ACTION_DOWN,那么之后的事件也不会给到它了。

// 检查能否阻拦            final boolean intercepted;            if (actionMasked == MotionEvent.ACTION_DOWN                    || mFirstTouchTarget != null) {                // 省略和问题无关代码            } else {                // 默认阻拦                intercepted = true;            }

BAT主流Android高级架构技术大纲+学习路线+资料分享

架构技术详解,学习路线与资料分享都在博客这篇文章里《年薪50w的BATAndroid架构师知识体系详解》
(包括自己设置控件、NDK、架构设计、混合式开发工程师(React native,Weex)、性能优化、完整商业项目开发等)

  • 阿里P8级Android架构师技术脑图

  • 全套体系化高级架构视频;七大主流技术板块,视频+源码+笔记

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » Android 事件分发机制的源码分析

发表回复