Android 事件分发机制(二)—— 点击事件透传

1. 点击透传逻辑

Android 事件分发遵循 Activity -> Window -> ViewGroup -> View 的链路,透传的关键在于 ViewGroup 如何分发事件给子 View

ViewGroup.dispatchTouchEvent 中,处理 ACTION_DOWN 时会遍历子 View 寻找消费者,透传的本质就是控制这个遍历过程。

要实现透传,必须让上层 View 在 dispatchTransformedTouchEvent 中返回 false,或者在第一步就被判定为 continue 跳过。

点击阅读:Android 事件分发机制(一)------ 全流程源码解析

2. 点击透传的实现

2.1 属性控制

通过属性设置上层 View 不可点击。

xml 复制代码
<View
    android:clickable="false"
    android:focusable="false" />

源码原理

View.onTouchEvent 默认实现中,只有当 View 是 CLICKABLELONG_CLICKABLE 等状态时才会返回 true。如果设为 false,onTouchEvent 返回 false,ViewGroup 的循环会继续找下一个子 View(即下层 View)。而 focusable="false" 则是为了防止非触摸模式下的焦点抢占和无障碍干扰。

注:给 View 设置 setOnClickListener 时会直接 setClickable(true)。

java 复制代码
public boolean onTouchEvent(MotionEvent event) {
    // 是否"可点击" (clickable)
    // 只要满足 CLICKABLE、LONG_CLICKABLE、CONTEXT_CLICKABLE 任意一个,即视为可点击。
    // 注意:focusable 属性不参与这个 clickable 变量的计算
    final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
            || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
            || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

    // ... 
    // 只有当 View 是 clickable 的(或有 Tooltip),才会进入此块。
    if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
        switch (action) {
            case MotionEvent.ACTION_UP:
                // ... 
                // 处理 Focusable 逻辑 (仅在 View 已经消费事件的前提下)
                boolean focusTaken = false;
                // 点击会获取焦点?
                // 条件:View 是 focusable 的 + 在 Touch 模式下允许 focus + 当前没焦点
                if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                    focusTaken = requestFocus(); 
                }

                // 执行点击回调
                if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                    if (!focusTaken) { // 如果刚才获取焦点失败(或不需要),才执行点击
                        performClickInternal(); // -> OnClickListener.onClick()
                    }
                }
                break;
                
            // ... (省略 DOWN/MOVE 处理) ...
        }

        // 消费事件
        // 只要进入了 if (clickable) 块,最终一定返回 true。
        return true; 
    }

    // 不消费事件 
    return false;
}

2.2 重写 onTouchEvent 返回 false

直接重写 onTouchEvent,返回 false。

注:如果你只在 ACTION_DOWN 返回 false,那么后续的 MOVEUP 事件将不会再分发给这个 View。你只能监听到 DOWN。

2.3 Window 级别的透传 (悬浮窗/系统层)

若是 WindowManager 添加的 View。

java 复制代码
WindowManager.LayoutParams params = new WindowManager.LayoutParams();
// 关键 Flag:FLAG_NOT_TOUCHABLE
// 设置后,事件直接穿透这个 Window,传递给后面的 Window
params.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;

// 如果想要自己处理一部分区域,其他区域透传,需配合 FLAG_NOT_TOUCH_MODAL
params.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;

Android 12+ 对非系统应用的触摸事件安全性做了限制(防止 Tapjacking 攻击),设置透明 Window 透传事件时,如果透明度过低或遮挡敏感区域,事件可能会被系统拦截,但在同个 UID 应用内通常不受限。

3. 实战

3.1 透明度陷阱

  1. View.setVisibility(INVISIBLE/GONE) : View 不参与绘制,且 canReceivePointerEvents() 返回 false,事件天然透传
  2. View.setAlpha(0) : View 只是全透明,但依然存在于布局中,依然参与事件分发。如果不设 clickable=false,它就是一堵透明墙,会拦截所有事件。

3.2 事件序列断裂

现象: 上层 View 想通过 onTouchEvent 监听用户的滑动轨迹(MOVE),同时让下层 View 也能响应点击。于是上层在 DOWN 时返回 false

结果: 上层 View 根本收不到 MOVE 和 UP。

解法: 这种"既要又要"的需求(双层响应),通常不能通过简单的 return false 实现。

  • 方法 A: 上层拦截事件(return true),自己在 onTouchEvent 里处理完后,手动 调用下层 View 的 dispatchTouchEvent(这种叫事件注入,比较 Hack)。
  • 方法 B: 使用 onInterceptTouchEvent

3.3 ImageView 透传

与 Button 不同,ImageView 默认 android:clickable="false"

这意味着,如果你只是在一个 FrameLayout 中把一个 ImageView 盖在 Button 上,不做任何代码设置,点击事件会自动"穿透" ImageView,被底下的 Button 响应。

相关推荐
零雲2 小时前
java面试:知道java的反射机制吗
java·开发语言·面试
圆号本昊4 小时前
Flutter Android Live2D 2026 实战:模型加载 + 集成渲染 + 显示全流程 + 10 个核心坑( OpenGL )
android·flutter·live2d
努力学算法的蒟蒻5 小时前
day46(12.27)——leetcode面试经典150
算法·leetcode·面试
冬奇Lab5 小时前
ANR实战分析:一次audioserver死锁引发的系统级故障排查
android·性能优化·debug
Maxkim5 小时前
「✍️JS原子笔记 」深入理解JS数据类型检测的4种核心方式
前端·javascript·面试
冬奇Lab5 小时前
Android车机卡顿案例剖析:从Binder耗尽到单例缺失的深度排查
android·性能优化·debug
ZHANG13HAO6 小时前
调用脚本实现 App 自动升级(无需无感、允许进程中断)
android
踏浪无痕7 小时前
JobFlow 的延时调度:如何可靠地处理“30分钟后取消订单”
后端·面试·开源