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(),如果它也不消费,系统最终丢弃该事件。

相关推荐
麻瓜生活睁不开眼4 分钟前
Android16修改全局桌面视图边框四直角显示为弧边圆角
android·java·深度学习
AFinalStone3 小时前
Android 7系统休眠唤醒(六)唤醒全链路
android·电源管理·休眠唤醒
雨白4 小时前
深入理解 Kotlin 协程 (九):互通有无,解构 Channel 缓冲策略与底层无锁机制
android·kotlin
灯塔@kuaidao6 小时前
平台交叉编译名词解释与基础流程
android
__Witheart__6 小时前
3568 Android otg模式下adb热拔插不识别
android·adb·rockchip
明天…ling8 小时前
Upload-Labs (Pass1-Pass21) 完整通关思路与源码分析
android·网络安全·渗透测试·burpsuite·upload-labs·文件上传绕过·靶场复现
__Witheart__8 小时前
3568 Android ntp校时使用
android·rockchip
wddptwd289 小时前
android studio 报错怎么处理 java.lang.NullPointerException
android·java·android studio
美狐美颜SDK开放平台10 小时前
直播APP开发,美颜SDK和相机SDK有什么区别?
android·深度学习·数码相机·ios·直播美颜sdk·视频美颜sdk
唐诺11 小时前
Android Open Accessory (AOA) 协议完全解析
android·usb·aoa