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 响应。

相关推荐
liang_jy1 天前
文件管理(一)—— 初识文件管理
面试·操作系统
liang_jy1 天前
文件管理(二)—— 文件结构
面试·操作系统
Sirens.1 天前
MySQL表设计进阶-约束范式连接索引与事务
android·数据库·mysql
zh_xuan1 天前
安卓上 mmap读取并修改文件内容
android·mmap
二流小码农1 天前
鸿蒙开发:实现文本渐变效果
android·ios·harmonyos
光头闪亮亮1 天前
Fyne ( go跨平台GUI )项目实战-项目开发必备基础知识(中)
android·c++·go
光头闪亮亮1 天前
Fyne ( go跨平台GUI )项目实战-项目开发必备基础知识(上)
android·sqlite·go
HeiSenBerg1 天前
Android Lifecycle 原理完整剖析:从监听、事件分发到状态驱动
面试
光头闪亮亮1 天前
Fyne ( go跨平台GUI )项目实战-项目开发必备基础知识(下)
android·c++·go
CoderYanger1 天前
A.每日一题:1979. 找出数组的最大公约数
java·程序人生·算法·leetcode·面试·职场和发展·学习方法