Android进阶之事件分发机制深度剖析

前言

Android 事件分发机制不仅是日常面试高频考点,同时也是日常开发中滑动冲突、自定义控件、事件拦截、多指触控问题的根源。绝大多数开发者只看重结论:dispatchTouchEvent、onInterceptTouchEvent 、onTouchEvent ,但遇到复杂场景(嵌套 ScrollView、RecyclerView 横向滑动、自定义手势、多指触控冲突)依旧会踩坑。本文将基于 Android 原生 ViewGroup 核心源码,深度剖析事件分发机制。

更多精彩文章,可关注公众号全栈码小峰,随时查看!

一、核心方法

ViewGroup 继承自 View,并实现 View 中的dispatchTouchEventonTouchEvent 方法 同时还增加一个onInterceptTouchEvent 方法

  • dispatchTouchEvent(): 事件分发入口。返回 true:事件被当前控件消费;false:不消费,向上回传给父容器。
  • onTouchEvent(): 处理触摸事件;true 代表消费事件,false 代表不处理,事件向上传递。
  • onInterceptTouchEvent(): 是否拦截本次事件;true代表拦截,不再传给子 View;false 代码不拦截,将事件继续分发给子View;仅在 dispatchTouchEvent 内部调用。

ViewGroup 中事件分发过程(伪代码)如下:

ini 复制代码
        public boolean dispatchTouchEvent(MotionEvent ev) {
            boolean consume = false;
            if (onInterceptTouchEvent(ev)) {
                consume = onTouchEvent(ev);
            } else {
                consume = child.dispatchTouchEvent(ev);
            }

            return consume;
        }

ViewGroup 的 dispatchTouchEvent()方法是整个事件分发机制的核心方法,事件分发过程从 ViewGroup 的 dispatchTouchEvent()开始, 先调用 ViewGroup 的onInterceptTouchEvent()方法判断 ViewGroup 是否自己拦截该事件,如果不拦截再分发给子View 的dispatchTouchEvent()。

二、事件分发

由于ViewGroup 的 dispatchTouchEvent 方法源码太多,这里根据事件分发流程简化涉及主干源码。

ini 复制代码
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    boolean handled = false;

    // ① ACTION_DOWN:当新一轮手势开始,清空上一轮触摸目标、重置拦截状态
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        cancelAndClearTouchTargets(ev);
        resetTouchState();
    }

    // ② 判断是否拦截事件
    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 {
            
                intercepted = true;
            }
    if (mFirstTouchTarget == null || !onInterceptTouchEvent(ev)) {
        intercepted = false;
    } else {
        intercepted = true;
    }

    // ③ 不拦截:遍历子View,寻找能够消费事件的子控件
    if (!intercepted) {
        final int childrenCount = mChildrenCount;
        if (childrenCount != 0) {
            // 反序遍历:后add的子View(上层View)优先接收触摸事件
            for (int i = childrenCount - 1; i >= 0; i--) {
                final View child = mChildren[i];
                // 判断触摸点是否落在child范围内、View可见
                if (!canViewReceivePointerEvents(child)
                        || !isTransformedTouchPointInView(...)) {
                    continue;
                }
                // 递归分发事件给子View
                if (child.dispatchTouchEvent(ev)) {
                    // 子View消费事件,创建TouchTarget链表,记录目标View
                    newTouchTarget = addTouchTarget(child);
                    break;
                }
            }
        }
    }

    // ④ 分支判断:根据有无子View 进行触摸事件分发
    if (mFirstTouchTarget == null) {
        // 情况1:拦截事件 / 没有子View愿意消费
        // 调用super.dispatchTouchEvent(),执行ViewGroup自身onTouchEvent
        handled = super.dispatchTouchEvent(ev);
    } else {
        // 情况2:找到目标子View,事件下发给子View
        handled = dispatchToTouchTarget(ev);
    }

    return handled;
}
  1. ACTION_DOWN:当新一轮手势开始,清空上一轮触摸目标、重置拦截状态
scss 复制代码
 if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        cancelAndClearTouchTargets(ev);
        resetTouchState();
    }

当新的触摸事件来时,调用cancelAndClearTouchTargets 和 resetTouchState 将上一轮处理手势的mFirstTouchTarget 链表中的处理触摸事件的对象清除,同时将一些标记位恢复初始值,如mGroupFlags,表示是否允许父控件拦截触摸事件。

  1. 判断是否拦截事件
ini 复制代码
 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 {
             intercepted = true;
            }
    if (mFirstTouchTarget == null || !onInterceptTouchEvent(ev)) {
        intercepted = false;
    } else {
        intercepted = true;
    }

在事件分发过程中,对于 ViewGroup 是否拦截事件有两个条件,第一个是mFirstTouchTarget 是否为 null,表示 ViewGroup 中是否有子View,如果没有子 View 或者onInterceptTouchEvent()返回 false 满足其中任何一个条件都表示不用拦截,这一点很好理解,如果 ViewGroup 中没有其他 View,不管拦不拦截,该事件最终都会交给 ViewGroup 处理。而 onInterceptTouchEvent()默认是不拦截事件的。

  1. 不拦截:遍历子View,寻找能够消费事件的子控件
ini 复制代码
if (!intercepted) {
        final int childrenCount = mChildrenCount;
        if (childrenCount != 0) {
            // 反序遍历:后add的子View(上层View)优先接收触摸事件
            for (int i = childrenCount - 1; i >= 0; i--) {
                final View child = mChildren[i];
                // 判断触摸点是否落在child范围内、View可见
                if (!canViewReceivePointerEvents(child)
                        || !isTransformedTouchPointInView(...)) {
                    continue;
                }
                // 递归分发事件给子View
                if (child.dispatchTouchEvent(ev)) {
                    // 子View消费事件,创建TouchTarget链表,记录目标View
                    newTouchTarget = addTouchTarget(child);
                    break;
                }
            }
        }
    }

如果当前触摸事件没有被 ViewGroup 拦截,首先遍历查找 ViewGroup 中的子 View,然后递归调用子 View 的dispatchTouchEvent(),将触摸事件分发给子 View,同时将子 View 添加到链表中。

  1. 根据有无子 View 进行触摸事件分发
ini 复制代码
 if (mFirstTouchTarget == null) {
        // 情况1:拦截事件 / 没有子View愿意消费
        // 调用super.dispatchTouchEvent(),执行ViewGroup自身onTouchEvent
        handled = super.dispatchTouchEvent(ev);
    } else {
        // 情况2:找到目标子View,事件下发给子View
        handled = dispatchToTouchTarget(ev);
    }

如果 ViewGroup 中没有子 View愿意消费事件,则调用super.dispatchTouchEvent(),执行ViewGroup自身onTouchEvent,由于 ViewGroup 继承自 View,所以这里调用了 View 中的dispatchTouchEvent。如果有,则找到目标子View,将触摸事件下发给子View。

三、事件处理

当 ViewGroup 中没有子 View 时,则 ViewGroup 调用super.dispatchTouchEvent(ev)进行事件消费。这里调用的是 View 的dispatchTouchEvent(ev)。

ini 复制代码
    public boolean dispatchTouchEvent(MotionEvent event) {
       
        boolean result = false;


        final int actionMasked = event.getActionMasked();
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            //1. 停止滑动事件
            stopNestedScroll();
        }

        if (onFilterTouchEventForSecurity(event)) {
           
            //2.是否实现onTouchListener的onTouch方法
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }
            //3.是否实现了onTouchEvent方法 
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }
        if (actionMasked == MotionEvent.ACTION_UP ||
                actionMasked == MotionEvent.ACTION_CANCEL ||
                (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
            stopNestedScroll();
        }

        return result;
    }

在 View 的dispatchTouchEvent()方法中,可以看出如果实现了 onTouchListener 监听,并且在 onTouch 方法返回了 True,则后续的 onTouchEvent 不会执行。否则执行 onTouchEvent 方法。

ini 复制代码
    public boolean onTouchEvent(MotionEvent event) {
       

        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) {
                            // 移除长按事件
                            removeLongPressCallback();

                            if (!focusTaken) {
                                    if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    performClickInternal();
                                }
                            }
                    }
                    mIgnoreNextUpEvent = false;
                    break;
            return true;
        }

        return false;
    }

在 onTouchEvent()中,当手指抬起时,触发了performClickInternal()方法,在此方法中调用了performClick()。这里主要处理用户点击事件。

ini 复制代码
    public boolean performClick() {

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

        return result;
    }

所以在一个完整的事件分发过程中,onTouchListener 的 onTouch 方法先执行,然后根据返回值来决定是否执行 onTouchEvent(),最后在 onTouchEvent 中执行onClick 方法。

四、事件拦截

在 ViewGroup 的事件分发过程中,优先调用onInterceptTouchEvent()方法,根据返回值进行事件拦截的判断。

scss 复制代码
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        //鼠标事件
        if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
                && ev.getAction() == MotionEvent.ACTION_DOWN
                && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
                && isOnScrollbarThumb(ev.getXDispatchLocation(0), ev.getYDispatchLocation(0))) {
            return true;
        }
        return false;
    }

在onInterceptTouchEvent()方法中,除非是鼠标事件,否则默认事件分发是不拦截的。除此之外还有requestDisallowInterceptTouchEvent()方法能影响事件是否允许被拦截。在 ViewGroup 的dispatchTouchEvent() 中,disallowIntercept 决定事件是否不允许拦截。

ini 复制代码
  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 {
             
                intercepted = true;
            }

而mGroupFlags 的值在requestDisallowInterceptTouchEvent()方法中被赋值,意味着可以通过调用 ViewGroup 的requestDisallowInterceptTouchEvent() 方法来干预事件的拦截。

typescript 复制代码
 @Override
    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {

        if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
           
            return;
        }

        if (disallowIntercept) {
            mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
        } else {
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }
        // Pass it up to our parent
        if (mParent != null) {
            mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
        }
    }

五、总结

在整个触摸事件分发过程中,由 ViewGroup 的 dispatchTouchEvent 开始。onInterceptTouchEvent 是分发流程中的分支判断节点,onTouchEvent 是事件的终点。所有嵌套滑动冲突、手势抢占、点击失效问题,本质都是这三个方法返回值、调用时机控制不当。读懂 ViewGroup 的 dispatchTouchEvent 主干源码,理解事件分发机原理,解决问题也就事半功倍。

相关推荐
触底反弹1 小时前
🔥 AI 写代码总翻车?这套实战方法论救了我
人工智能·面试·程序员
Csvn1 小时前
🛡️ 前端水印方案全解析——从「能被 F12 删掉」到「删不掉」的反破解实战
前端
风栖柳白杨1 小时前
【面试】AI算法工程师_空白自测版本
人工智能·算法·面试
学网安的肆伍2 小时前
【046-WEB攻防篇】注入工具&SQLMAP&Tamper编写&指纹修改&高权限操作&目录架构
前端·架构
峥嵘life2 小时前
Android WiFi连接过程 wpa_supplicant 日志分析
android·开发语言·php
Highcharts.js3 小时前
如何下载使用Highcharts Grid 开发web可编辑表格
前端·人工智能·grid 表格·web 表格·在线编辑表格·highcharts表格安装·表格开发工具
用户62960593247054 小时前
从“浏览器根本区分不了”到真正实现:刷新不退出,关闭标签页自动退出,前端判断浏览器关闭和刷新
前端
qetfw4 小时前
MWU:Vue 3 + FastAPI 的 MaaFramework 跨平台 WebUI 源码
前端·vue.js·python·fastapi·开源项目·效率工具
mfxcyh4 小时前
Vue3+Ant Design Vue 实现手动异步文件上传(含大小校验、弹窗上传)
前端·javascript·vue.js