Android: 事件分发

1. 事件分发 是什么?

事件分发是指 触摸事件(MotionEvent) 从屏幕产生后,系统如何将其传递给具体的 View 并决定由谁处理的过程。

本质上是 View 系统对触摸事件的一套传递与消费机制。


Android 的事件传递顺序:

Activity → ViewGroup → View

Activity → PhoneWindow → DecorView → ViewGroup → View

  • PhoneWindow 内部持有 DecorView(整个界面的根 View,继承自 FrameLayout)。
  • DecorView 的 superDispatchTouchEvent 会直接调父类 ViewGroup 的 dispatchTouchEvent,正式进入 ViewGroup 分发。

2. 三个核心方法

方法 所属类 作用 返回值含义
dispatchTouchEvent() View / ViewGroup 事件分发入口,决定事件流向 true:事件被自己或子控件消耗,停止上级传递;false:自己及子控件均不处理,回传给上层
onInterceptTouchEvent() 只 ViewGroup 有 拦截事件,将事件拉回给自己处理 true:拦截,事件交给自己的 onTouchEventfalse(默认):不拦截,继续向子控件传递
onTouchEvent() View / ViewGroup 实际处理触摸事件 true:表示自己消耗了事件;false:未消耗,事件回传

3. 整体流程

因为 事件传递顺序 是以 Activity → ViewGroup → View,所以以这三个阶段来说明事件分发的具体流程

3.1 Activity 分发
java 复制代码
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}

可以看出,这儿 Activity.dispatchTouchEvent 内部调用了 windowdispatchTouchEvent 方法,如果所有子View没有消费的话会调用 Activity 自身的 onTouchEvent 方法

text 复制代码
Activity.dispatchTouchEvent()
   └→ getWindow().superDispatchTouchEvent(ev)  // PhoneWindow
        └→ mDecor.superDispatchTouchEvent(ev)   // DecorView
             └→ super.dispatchTouchEvent(ev)    // 实际调的是 ViewGroup.dispatchTouchEvent()
  • 若 ViewGroup 最终消费事件 → 返回 true,事件终止。
  • 若 ViewGroup 没消费(返回 false)→ 会执行 Activity.onTouchEvent(ev)。
3.2 ViewGroup 分发

简陋流程,伪代码:

java 复制代码
boolean dispatchTouchEvent(MotionEvent ev) {
    if (!disallowIntercept && onInterceptTouchEvent(ev)) {
        // 拦截:自己处理,调用自己的 onTouchEvent()
        return super.dispatchTouchEvent(ev);  // 实际上是 View.dispatchTouchEvent
    }
    // 不拦截:遍历所有子 View,找触摸点落在哪个子 View 上
    for (View child : children) {
        if (child 被触摸) {
            if (child.dispatchTouchEvent(ev)) {   // 交给子 View 分发
                return true;  // 子 View 消费了,自己就不处理了
            }
        }
    }
    // 子 View 都不消费,自己处理
    return super.dispatchTouchEvent(ev); // 最终走到自己的 onTouchEvent()
}

大致分为三步:

  • 拦截:重写 onInterceptTouchEvent 返回 true,事件就停止下传,自己处理。

  • 不拦截:找到具体子 View,调用子 View 的 dispatchTouchEvent。

  • 消费与冒泡:子 View 消费(返回 true)则结束;都不消费则"冒泡"回自身。

3.3 View 分发与消费
3.3.1 View 分发
java 复制代码
boolean dispatchTouchEvent(MotionEvent event) {
    if (ENABLED && mOnTouchListener != null
            && mOnTouchListener.onTouch(this, event)) {
        return true;  // onTouch 直接消费,不再往下
    }
    return onTouchEvent(event); // 否则交给 onTouchEvent 处理
}

判断当前的View有没有注册TouchListener,如果注册了,那就不往下走了,说明事件已经被消费了,如果没注册,那就继续往下走,执行onTouchEvent()来消费这个事件。

  • 若 onTouch 返回 true,事件立即被消费,不会触发 onTouchEvent,更不会触发 onClick。
  • 若 onTouch 返回 false 或没有注册,才会进入下一步:View 的消费(即 onTouchEvent)。
3.3.2 View消费
  • onTouchEvent 内部根据控件是否可点击来决定是否消费事件:
    • 可点击(CLICKABLE 或 LONG_CLICKABLE 为 true):
      • 在 ACTION_UP 时调用 performClick(),进而触发 onClick()。
      • 返回 true,表示事件被当前 View 消费。
    • 不可点击:
    • 返回 false,事件向上层传递(冒泡给父 View)。

完整执行顺序:onTouch → onTouchEvent → onClick

4. 相关结论&问题

结论:

  1. 事件序列的连续性
    • 事件序列:在一次事件分发中,以每个 DOWN 事件为开始, UP / CANCEL 事件为停止,在 DOWN -> MOVE -> MOVE -> UP / CANCEL 整个过程看做是一个事件序列
    • 一旦某个 View 决定不处理 DOWN 事件(onTouchEvent 返回 false),同一序列的后续事件(MOVE, UP)都不会再给它。
    • 某个 View 处理了 DOWN 事件,那么同序列的后续事件(MOVE, UP) 都会交给它
    • 如果 ViewGroup 拦截了非 DOWN 事件,子 View 会收到 CANCEL。
  2. ViewGroup 拦截注意事项
    • onInterceptTouchEvent 只存在于 ViewGroup,View 没有这个方法。
    • 子 View 可通过 requestDisallowInterceptTouchEvent(true) 阻止父 View 拦截。
  3. 返回值的含义
    • dispatchTouchEvent 返回 true:事件被当前层级或子层级消费。
    • dispatchTouchEvent 返回 false:自身及子层级都不处理,事件向上传递。
    • onTouchEvent 返回 true:表示当前 View 消费了事件,终止传递。
  4. onTouch 和 onClick 的关系
    • 注册了 OnTouchListener 且 onTouch() 返回 true → onClick 不会再调用。
    • 只有 onTouch() 返回 false,才会走到 onTouchEvent,最终触发 onClick。

问题:

Q1:ViewGroup 为什么没有 onTouchEvent?

有。ViewGroup 继承自 View,天然有 onTouchEvent。当它拦截事件或没有子 View 消费时,就会执行自己的 onTouchEvent

Q2:如何解决滑动冲突?

两种方案:

  • 外部拦截法 :在父 View 的 onInterceptTouchEvent 中根据手势逻辑返回 true/false
  • 内部拦截法 :子 View 调用 requestDisallowInterceptTouchEvent(true),父 View 配合不要拦截 DOWN 事件。

Q3:为什么推荐默认不拦截 DOWN 事件?

因为一旦父 View 拦截了 DOWN,子 View 将收不到任何事件序列,后续 MOVE/UP 都不会传给它,容易出现事件丢失。

Q4:如果所有 View 都不消费事件,最终会怎样?

事件会回溯到 Activity,调用 Activity.onTouchEvent(),如果它也不消费,系统最终丢弃该事件。

相关推荐
吕氏春秋i15 小时前
android kotlin Compose 蓝牙库推荐
android·gitee·kotlin
鹏晨互联15 小时前
《Kotlin高阶函数完全指南:从入门到精通的15个核心函数》
android·开发语言·kotlin
三少爷的鞋15 小时前
Android 离线优先架构实践:网络只是本地数据库的同步触发器
android
巴博尔1 天前
UNIAPP中NVUE页面 动画
android·前端·javascript·ios·uni-app
abc_ABC123A1 天前
flutter开发安卓APP所需搭建的环境
android
xq95271 天前
Google 授权登录 V2 接入文档 王者归来
android
李少兄1 天前
MySQL分页重复问题深度剖析
android·数据库·mysql
_李小白1 天前
【android opencv学习笔记】Day 24: 最大稳定极值区域
android·opencv·学习
问心无愧05131 天前
ctf show web入门257
android·前端·笔记