十四 Android事件分发体系详解

概述

Android的事件体系从根源说起的话,有3个绕不开的步骤:

    1. 事件从驱动层传递给Framework层的InputManagerService
    1. WMS通过ViewRootImpl传递给目标窗口
    1. 事件到达DecorView之后,继续传递给内部的子view

对安卓工程师而言,第三个步骤是最重要的。

ViewGroup和View的概念

在类的继承关系上,ViewGroup是View的子类。但是在 事件的分发顺序上,ViewGroup是优先于View的。

一个ViewGroup是View的组合,它内部可能拥有多个ViewGroup或者View。

ViewGroup事件分发的核心是 处理当前ViewGroup和内部子View的逻辑关系,主要分为以下几点:

  • 当前ViewGroup是否需要拦截事件
  • 是否需要将事件分发给子view
  • 如何将touch事件分发给子view 关键字是:拦截 和 分发

View的事件分发核心是:当前View如何处理touch事件,并且根据手势逻辑进行UI处理。

  • 是否存在 TouchListener
  • 是否自己接收Touch事件 主要逻辑在 onTouchEvent 关键字是:处理

核心逻辑

整个View的事件分发,实际上就是一个大的递归函数 dispatchTouchEvent. 递归过程中,会适时调用 onInterceptTouchEvent 或者 onTouchEvent来处理事件。

在ViewGroup的源代码中,dispatchTouchEvent 方法中拥有三大步骤:

  1. 检查当前ViewGroup是否需要拦截事件 如果拦截,那么此次事件则不会传递给子view,或者给子view发送一个cancel事件。
  2. 不拦截的情况下,将事件传递给子view
  3. 根据 mFirstTouchTarget 重新分发事件

步骤1

红框内为 判断是否需要拦截的逻辑。

可以看到:

  • 如果是down事件,则一定会进入到 拦截判断的逻辑。
  • 或者 mFirstTouchTarget 非空,代表已经有了子View捕获了这个事件,子View的dispatchTouchEvent返回true,就代表捕获了touch事件。

步骤2

如果步骤1并没有进行拦截,那么就进入下面的步骤2的逻辑:

    1. 只有DOWN事件才才会进入到分发逻辑
    1. 对所有的子view进行遍历
    1. 判断事件的坐标是否在子view范围内,并且子view没有处在动画的状态下
    1. 将事件分发给子view,如果子view捕获事件成功,则将 mFirstTouchTarget赋值为子view

步骤3

    1. mFristTouchTarget 为空,则说明在步骤2中,并没有子view捕获事件,此时,会调用自身的 onTouchEvent方法进行处理
    1. mFristTouchTarget 非空,则说明步骤2中,有子view捕获了事件,那么就将down事件以及后续事件全部交给 mFristTouchTarget指向的子view进行处理。

事件分发流程演示

假如有以下一个自定义ViewGroup和一个自定义View:

布局文件如下图:

由 DownInterceptGroup 包裹住 CaptureTouchView。那么运行起来之后,进行如下动作:手指在CaptureTouchView内部按下,并move一段距离后抬起。

日志打印如下:

日志分为3段,down事件,move事件,以及up事件。

down事件中,外层的 分发事件dispatchTouchEvent和拦截事件 onInterceptTouchEvent 被触发,内层的分发事件 dispatchTouchEvent 返回值为true (这是因为 内层的onTouchEvent return true.) 代表这个down事件被 内层捕获消费了。 这时候,内层的 CaptureTouchView 被添加到 父DownInterceptGroup的mFirstTouchTarget 中。

mFirstTouchTarget 的源代码如下,它是一个链表结构。

mFirstTouchTarget 是用来记录捕获down事件的view。

为什么是链表类型的结构呢?因为安卓支持多点触控,在上面的步骤3中,如果 mFirstTouchTarget 非空,则遍历这个链表,将事件逐个分发到每一个捕获事件的子类中。

容易遗漏的cancel事件

ViewGroup源代码中有这么一段: 它的意思是,如果有了子view捕获事件,但是 当前viewgroup的intercept又是true(表示拦截,不下发),此时,事件的主导权又回到 当前viewGroup。这个时候,这个事件会被包装成 CACEL 事件 传递给子view。

什么时候会有cancel事件呢?

当按下时 父viewGroup不拦截事件,而是由子view去捕获。而在按下之后的滑动动作中,父viewGroup 表示后悔下发了,突然想要拦截事件, 子控件就会收到 Cacel事件。 所以,当我们去自定义View的时候,特别是有滑动组件对我们的自定义view进行包裹时,一定要记得处理cancel事件,否则可能出现UI异常。

总结

事件体系的核心,在于 dispatchTouchEvent 这个分发方法。

  • 判断是否拦截 主要是根据 onInterceptTouchEvent 的返回值(false放行,true拦截)
  • 将down事件分发给子view,这一过程中,如果有子view捕获了down,那么就会对 mFirstTouchTarget进行赋值。
  • down,up,move事件,都会根据 mFirstTouchTarget是否为null,来决定是自己处理事件,还是再次下发。

在此体系中,有一些特点需要特别注意:

    1. down事件比较特殊,它是事件的起点,谁捕获,谁就有权力处理后续的move,up
    1. mFirstTouchTarget的作用,记录捕获消费touch事件的view,它是一个链表结构
    1. cancel事件的触发场景,当父viewGroup先不拦截,然后在move时去拦截,此时view就会收到cancel
相关推荐
Warolitbos17 分钟前
【数据库】SQL进阶15题,窗口函数/索引/事务一页速查
数据库·sql·学习·mysql·面试
hunter_0762 小时前
App 被苹果 4.3 拒绝怎么办?从官方条款到整改、沟通与申诉
面试·代码规范
十年Java程序媛2 小时前
Java 面试常问 this、super:构造方法调用顺序避坑
java·开发语言·spring boot·面试·个人开发
ZhengEnCi2 小时前
MCP与Function Calling怎么选:大模型工具调用三种方式的面试级对比
面试·llm·mcp
BreezeJiang2 小时前
闭包和 useState 是两个知识点?你可能只学了半条作用域链
前端·面试
祥Sir的SRE笔记2 小时前
CPU 100% 不一定是故障:压测打满、业务饱和、突变打满的判断与排查
linux·面试·性能优化·dba·sre·故障定位·性能排查
moshuying3 小时前
DeppInfer系列之 - 矩阵、注意力与 GPU:拆解 DeepGEMM 与 FlashMLA
前端·javascript·面试
Rocky Ding*3 小时前
【三年面试五年模拟】2026-10-20_淘天AI应用研发一面面经全解析
论文阅读·人工智能·深度学习·机器学习·面试·aigc·ai-native
利威尔xu3 小时前
Go IM 源码实战(2):WebSocket 握手 11 步,少 1 个参数连不上
后端·websocket·微服务·面试·golang
洁洁!5 小时前
手搓 C++ 线程池详细讲解以及如何面试应答
java·c++·面试