破解Android悬浮窗遮挡无障碍服务难题:我在可见即可说上踩过的坑

为什么你的Android悬浮窗"挡住"了无障碍服务

Android 可见即可说 (语音悬浮窗口在,无障碍服务扫描不出系统卸载window)

1.完整需求: 语音说一句话:帮我把抖音里面的今天的奖励全部领完,红包领完!

1)语音悬浮窗交互:

能自动点击悬浮窗下方的所有窗口(如抖音红包按钮)

点击悬浮窗空白区域 → 悬浮窗消失

悬浮窗内 RecyclerView 的点击事件需响应

效果图

1.2 具体详细需求:

系统弹窗支持:

当语音指令为"卸载抖音"时:

先显示语音悬浮窗

再触发系统卸载弹窗(com.android.packageinstaller)

用户可通过语音控制点击"确定"或"取消"

2.核心问题:无障碍服务无法识别被悬浮窗完全遮挡的系统弹窗

语音把系统的卸载window弹出,

语音可见即可说扫描不出卸载window,

导致无法实现确认按钮的可见即可说

2.2 问题现象

当语音悬浮窗完全覆盖系统卸载弹窗时:

AccessibilityService 的 windows 列表中扫描不到 com.android.packageinstaller

只能扫描到 launcher 或 systemui

导致"可见即可说"功能失效,无法点击"确定/取消"

2.3 根本原因(Android 系统机制限制)

表格

原因 说明
窗口可见性判定 Android Accessibility 服务不会返回被完全遮挡且不可交互的窗口的节点
安全限制 出于隐私和安全考虑,系统禁止通过无障碍服务读取被其他应用完全覆盖的 UI 层
层级与类型相同 语音悬浮窗 (TYPE_SYSTEM, layer=1) 与 launcher (TYPE_APPLICATION, layer=0) 不同;但卸载弹窗也是 TYPE_APPLICATION, layer=0,与 launcher 同级。当悬浮窗高度 95% 屏幕时,完全遮挡卸载弹窗 → 系统认为其"不可见"

验证结论 :只要悬浮窗未完全遮挡 卸载弹窗(例如高度 ≤85%),无障碍服务就能正常扫描到 packageinstaller

3.分析问题

3.1 之前同事写的代码逻辑:

ini 复制代码
layoutParams = new WindowManager.LayoutParams(
    MATCH_PARENT,
    WRAP_CONTENT,
    TYPE_APPLICATION_OVERLAY,
    FLAG_NOT_FOCUSABLE,
    TRANSLUCENT
);
layoutParams.flags |= FLAG_LAYOUT_IN_SCREEN;
layoutParams.flags |= FLAG_NOT_TOUCH_MODAL;      // ✅ 允许区域外触摸透传
layoutParams.flags |= FLAG_WATCH_OUTSIDE_TOUCH;  // 配合外部点击关闭

3.2 无障碍服务中的"窗口替换"逻辑

ini 复制代码
if (rootNode?.packageName == packageName || rootNode?.childCount == 0) {
    windows.forEach { window ->
        window.root?.takeIf { rn ->
            rn.packageName != packageName && rn.childCount != 0
        }?.apply {
            rootNode = this  // 替换为非自身、非空的窗口根节点
        }
    }
}

目的:跳过语音悬浮窗自身,尝试使用其他窗口(如卸载弹窗)作为操作目标 失败原因:当卸载弹窗被完全遮挡 → windows 列表中根本不存在该窗口 → 替换失败

3.2 无障碍服务扫描的结果

3.2.1 具体结果分3步骤:

1).在桌面扫描的结果:
bash 复制代码
2026-01-06 15:48:48.918 59779-59779 VoiceAcces...ityService com.tencent.voice              E  开始windows-------index: 0, window: AccessibilityWindowInfo[title=导航栏, displayId=0, id=258, type=TYPE_SYSTEM, layer=3, region=SkRegion((0,1824,864,1920)), bounds=Rect(0, 1824 - 864, 1920), focused=false, active=false, pictureInPicture=false, hasParent=false, isAnchored=false, hasChildren=false]; com.android.systemui
2026-01-06 15:48:48.919 59779-59779 VoiceAcces...ityService com.tencent.voice              E  开始windows-------index: 1, window: AccessibilityWindowInfo[title=null, displayId=0, id=252, type=TYPE_SYSTEM, layer=2, region=SkRegion((0,0,864,115)), bounds=Rect(0, 0 - 864, 115), focused=false, active=false, pictureInPicture=false, hasParent=false, isAnchored=false, hasChildren=false]; com.android.systemui
2026-01-06 15:48:48.919 59779-59779 VoiceAcces...ityService com.tencent.voice              E  开始windows-------index: 2, window: AccessibilityWindowInfo[title=null, displayId=0, id=265, type=TYPE_SYSTEM, layer=1, region=SkRegion((0,192,864,1824)), bounds=Rect(0, 192 - 864, 1824), focused=true, active=true, pictureInPicture=false, hasParent=false, isAnchored=false, hasChildren=false]; com.tencent.voice
2026-01-06 15:48:48.920 59779-59779 VoiceAcces...ityService com.tencent.voice              E  开始windows-------index: 3, window: AccessibilityWindowInfo[title=大桌面, displayId=0, id=250, type=TYPE_APPLICATION, layer=0, region=SkRegion((0,0,864,1920)), bounds=Rect(0, 0 - 864, 1920), focused=false, active=false, pictureInPicture=false, hasParent=false, isAnchored=false, hasChildren=false]; com.tencent.launcher

launcher: type=TYPE_APPLICATION, layer=0

2). 在桌面,然后弹出卸载框:
复制代码
 com.android.systemui -
com.android.packageinstaller 

卸载window:type=TYPE_APPLICATION, layer=0 不会扫描到桌面的window,被卸载弹框全部挡住, 处于同一层级 得出层级关系:卸载在最底层,layer=0

3).在桌面,语音悬浮窗出现之后,然后弹出卸载框:

不正常是这个:

yaml 复制代码
2025-12-29 13:58:34.996  1900-1900  VoiceAcces...ityService com.tencent.voice              D  windows size: 4 
                                                                                                     windowArrStr: com.android.systemui - visible:true - active:false 
                                                                                                    com.android.systemui - visible:true - active:false 
                                                                                                    com.tencent.voice - visible:true - active:true 
                                                                                                    com.tencent.launcher - visible:true - active:false 
2025-12-29 13:58:34.998  1900-1900  VoiceAcces...ityService com.tencent.voice              D  packageName:com.android.systemui
                                                                                                    isActive:false
                                                                                                    isAccessibilityFocused:false
2025-12-29 13:58:34.999  1900-1900  VoiceAcces...ityService com.tencent.voice              E  替换节点前.......com.tencent.voice
2025-12-29 13:58:34.999  1900-1900  VoiceAcces...ityService com.tencent.voice              E  替换节点后.......com.android.systemui
2025-12-29 13:58:35.000  1900-1900  VoiceAcces...ityService com.tencent.voice              D  packageName:com.android.systemui
                                                                                                    isActive:false
                                                                                                    isAccessibilityFocused:false
2025-12-29 13:58:35.000  1900-1900  VoiceAcces...ityService com.tencent.voice              E  替换节点前.......com.android.systemui
2025-12-29 13:58:35.000  1900-1900  VoiceAcces...ityService com.tencent.voice              E  替换节点后.......com.android.systemui
2025-12-29 13:58:35.001  1900-1900  VoiceAcces...ityService com.tencent.voice              D  packageName:com.tencent.voice
                                                                                                    isActive:true
                                                                                                    isAccessibilityFocused:false
2025-12-29 13:58:35.003  1900-1900  VoiceAcces...ityService com.tencent.voice              D  packageName:com.tencent.launcher
                                                                                                    isActive:false
                                                                                                    isAccessibilityFocused:false
2025-12-29 13:58:35.003  1900-1900  VoiceAcces...ityService com.tencent.voice              E  替换节点前.......com.android.systemui
2025-12-29 13:58:35.003  1900-1900  VoiceAcces...ityService com.tencent.voice              E  替换节点后.......com.tencent.launcher

原因应该是优先级:他们type相同 语音: type=TYPE_SYSTEM, layer=1, launcher: type=TYPE_APPLICATION, layer=0

期望正常是这个结果:
arduino 复制代码
2025-12-29 13:55:54.989   565-565   VoiceAcces...ityService com.tencent.voice              D  windows size: 4 
                                                                                                     windowArrStr: com.android.systemui - visible:true - active:false 
                                                                                                    com.android.systemui - visible:true - active:false 
                                                                                                    com.tencent.voice - visible:true - active:true 
                                                                                                    com.android.packageinstaller - visible:true - active:false 
2025-12-29 13:55:54.990   565-565   VoiceAcces...ityService com.tencent.voice              D  packageName:com.android.systemui
                                                                                                    isActive:false
                                                                                                    isAccessibilityFocused:false
2025-12-29 13:55:54.990   565-565   VoiceAcces...ityService com.tencent.voice              E  替换节点前.......com.tencent.voice
2025-12-29 13:55:54.990   565-565   VoiceAcces...ityService com.tencent.voice              E  替换节点后.......com.android.systemui
2025-12-29 13:55:54.991   565-565   VoiceAcces...ityService com.tencent.voice              D  packageName:com.android.systemui
                                                                                                    isActive:false
                                                                                                    isAccessibilityFocused:false
2025-12-29 13:55:54.992   565-565   VoiceAcces...ityService com.tencent.voice              E  替换节点前.......com.android.systemui
2025-12-29 13:55:54.992   565-565   VoiceAcces...ityService com.tencent.voice              E  替换节点后.......com.android.systemui
2025-12-29 13:55:54.992   565-565   VoiceAcces...ityService com.tencent.voice              D  packageName:com.tencent.voice
                                                                                                    isActive:true
                                                                                                    isAccessibilityFocused:false
2025-12-29 13:55:54.994   565-565   VoiceAcces...ityService com.tencent.voice              D  packageName:com.android.packageinstaller
                                                                                                    isActive:false
                                                                                                    isAccessibilityFocused:false
2025-12-29 13:55:54.995   565-565   VoiceAcces...ityService com.tencent.voice              E  替换节点前.......com.android.systemui
2025-12-29 13:55:54.995   565-565   VoiceAcces...ityService com.tencent.voice              E  替换节点后.......com.android.packageinstaller

4. 解决方案

4.1 模仿手机厂商怎么处理的! 看了下VIVO手机,

先卸载, 发现语音悬浮后,不能再进行下一步的可见即可说! 命中的是大模型,卸载按钮一样无法命中,如下图!

如果悬浮窗完全挡住了下面的window,下面的window就不会被系统扫描到! 发现一个问题,我的悬浮窗完全挡住了.com.android.packageinstaller,window里面就扫描不出,扫描出了com.tentent.launcher, 如果我的悬浮窗没用完全挡住,就可以window里面就扫描出了.com.android.packageinstaller

📌 总结

问题原因 系统将完全被悬浮窗遮挡的窗口视为"不可见",故 AccessibilityService 无法获取其根节点
根本限制 Android 安全机制:不能通过 Accessibility 读取被完全遮挡/不可交互的窗口
推荐做法 1. 避免完全覆盖 2. 过滤掉自身和 SystemUI/Launcher 3. 结合 UsageStats 做兜底

当时产品看了竞品没用实现,让我放手! 我觉得修改系统framwork层可以解决,但是公司的framwork层太low了,只有自己想办法

4.1 避免完全遮挡(推荐临时方案)

修改悬浮窗高度,留出顶部或底部空间

具体措施如下: 第一种临时方案:语音的悬浮窗不完全挡住系统的弹框,是可以扫描出来的! 翻看了源码: 无障碍服务扫描原理,从上往下扫描,当同级layer 的window,会根据优先级判断! 前面的窗口完全挡住,会扫描不出 所以很容易想到临时方案:修改悬浮窗的宽度或者高度,

ini 复制代码
    // 设置窗口大小和位置
            int screenHeight = DisplayUtils.getScreenHeight(MainApplication.getApplication());
            layoutParams.height = (int) (screenHeight * 0.85);
            layoutParams.format = PixelFormat.TRANSLUCENT;
            layoutParams.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL;

因为治标不治本!我不考虑,打算先看现象

4.1.1 第二种临时方案 解决方案:.改变点击事件

核心思路:

默认悬浮窗可点击(处理内部 RecyclerView)

当检测到系统弹窗出现时(如通过 UsageStats 或广播):

将悬浮窗设为 不可点击 + 事件透传

即:FLAG_NOT_TOUCHABLE | FLAG_NOT_FOCUSABLE

用户操作完成后,恢复可点击状态

点击事件的详细分析: 1.如果是点击了安装之后,2s后,设置为不可点击, 上报后,如果是采集到了,设置为可点击!

arduino 复制代码
   protected WindowManager.LayoutParams getLayoutParams(boolean clickable,int orientation) {
        LogUtils.d(TAG, "getLayoutParams: clickable = " + clickable + " cardClickable = " + cardClickable);
        mCurAsrClickable = clickable;
        if (layoutParams == null) {
            layoutParams = new WindowManager.LayoutParams(WindowManager.LayoutParams.TYPE_APPLICATION);
            layoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
            layoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
            layoutParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
            layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
            layoutParams.format = PixelFormat.TRANSLUCENT;// 支持透明
        }

        int canTouchFlag = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
        int cantTouchFlag = WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
        if (clickable) {
            layoutParams.flags |= canTouchFlag;
            layoutParams.flags |= cantTouchFlag;
            layoutParams.flags ^= cantTouchFlag;
        } else {
            layoutParams.flags |= cantTouchFlag;
            layoutParams.flags |= canTouchFlag;
            layoutParams.flags ^= canTouchFlag;
        }

4.2 方案二: 最优化终极解决方案 结合悬浮窗属性优化 + 无障碍服务智能处理:

通过悬浮窗属性+ 替换可见即可说的window对象,替换节点解决!

4.2.1 悬浮窗的属性核心代码如下:

arduino 复制代码
  protected WindowManager.LayoutParams getLayoutParams(boolean clickable) {
        LogUtils.d(TAG, "getLayoutParams: clickable = " + clickable + " cardClickable = " + cardClickable);
        mCurAsrClickable = clickable;
        if (layoutParams == null) {
            layoutParams = new WindowManager.LayoutParams(
                    WindowManager.LayoutParams.MATCH_PARENT,
                    WindowManager.LayoutParams.WRAP_CONTENT,
                    WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
                    WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
                    PixelFormat.TRANSLUCENT);

            layoutParams.format = PixelFormat.TRANSLUCENT;
            layoutParams.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL;

            layoutParams.flags |= WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;

            // 设置窗口标志 - 允许点击事件传递到下层
            layoutParams.flags |= WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
            layoutParams.flags |= WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
        }

        // 设置窗口大小和位置
        int screenHeight = DisplayUtils.getScreenHeight(MainApplication.getApplication());
        layoutParams.height = (int) (screenHeight * 0.95);

        //根据屏幕方向,动态设置宽度
        if (DisplayUtils.isLandscape(GlobalApplication.mGlobalContext)) {
            layoutParams.width = DisplayUtils.dp2px(MainApplication.getApplication(), 348f);
        } else {
            layoutParams.width = WindowManager.LayoutParams.MATCH_PARENT;
        }

        LogUtils.d(TAG, "-----height: " + layoutParams.height);
        return layoutParams;
    }

4.2.2 可见即可说,无障碍服务替换window的核心代码如下:

javascript 复制代码
            // 第一步:在 threadRoot 线程中获取根节点
            GlobalScope.launch(threadRoot) {
                try {
                    val startTime = System.currentTimeMillis()
                    // 获取根节点
                    var rootNode = rootInActiveWindow
                    activeRootNodeVisible = rootNode?.isVisibleToUser ?: false
                    printLogOnDebug("rootInActiveWindow-------${rootNode?.packageName}; ${rootNode?.windowId}")

                    // 窗口过滤逻辑(已注释,可根据需要启用)
                    if (rootNode?.packageName == packageName || rootNode?.childCount == 0) {
                        windows?.forEach { window ->
                            window?.root?.takeIf { rn ->
                                rn.packageName != packageName && rn.childCount != 0
                            }?.apply {
                                rootNode = this
                                LogUtils.d(TAG, "替换节点.......")
                            }
                        }
                    }

更详细一点的代码片段:

kotlin 复制代码
    override fun onAccessibilityEvent(event: AccessibilityEvent?) {
//        windows.forEach {
//            LogUtils.e(TAG, "开始windows-------$it; ${it.root?.packageName}")
//        }

        if(!SettingsChangeObserver.checkIsCarSystem()){
            var rootNodeOrigin = getRootNodeWithCache(event)

            lastCallBackTime = System.currentTimeMillis()
            lastActiveRootNodePackage = "${rootNodeOrigin?.packageName?.toString() ?: ""} child:${rootNodeOrigin?.childCount}"

            windowArrStr = ""
            if(isAbleToLog){
                windows.forEach {
                    windowArrStr += "${it.root?.packageName} - visible:${it.root?.isVisibleToUser} - active:${it.isActive} \n"
                }
            }
            activeRootNodeVisible = rootNodeOrigin?.isVisibleToUser ?: false
            printLogOnDebug("rootInActiveWindow-------${rootNodeOrigin?.packageName}; ${rootNodeOrigin?.windowId}")

//        for (w in windows) {
//            w.getBoundsInScreen(mRect)
//            //过滤顶部状态栏窗口
//            if (mRect.equalsXY(0, 0, 1920, 32)) continue
//            if (mRect.equalsXY(144, 0, 1920, 32)) continue
//            //过滤左边Dock栏窗口
//            if (mRect.equalsXY(0, 0, 144, 720)) continue
//            val pkName = w.root?.packageName
//
//            if (pkName.isNullOrEmpty()) break
//            rootNode
//            // TODO V2处理
////            rootNode = HotwordsExecutorFactory.getHotwordsExecutor(pkName).interceptRootNode(windows, rootNode)
//            break
//        }

            if (rootNodeOrigin?.packageName == packageName || rootNodeOrigin?.childCount == 0) {
                windows.forEach {
                Log.d(
                    TAG, """
                packageName:${it?.root?.packageName}
                isActive:${it?.isActive}
                isAccessibilityFocused:${it?.isAccessibilityFocused}
                """.trimIndent()
                )
                    it.root?.takeIf { rn ->
                        rn.packageName != packageName && rn.childCount != 0
                    }?.apply {
                        LogUtils.d(TAG, "替换节点......."+ rootNodeOrigin?.packageName )
                        rootNodeOrigin = this
                    }
                }
            }
            val rootNode = rootNodeOrigin

            lastLoadRootNodePackage = rootNode?.packageName?.toString() ?: ""

            printLogOnDebug("rootNode-------${rootNode?.packageName}; ${rootNode?.windowId}")

            if (rootNode == null || event == null|| event.packageName.isNullOrEmpty()) {
                return
            }

            if (rootNode.packageName == packageName) {
                return
            }

            try {
                logNodeInfo(rootNode, event)
            } catch (e: Exception) {

            }
            GlobalScope.launch(threadContext) {
                try {
                    processBusinessLogic(rootNode)
                } catch (e: Exception) {
                    LogUtils.e(TAG, e.message)
                }
            }
        }else{

            // 第一步:在 threadRoot 线程中获取根节点
            GlobalScope.launch(threadRoot) {
                try {
                    val startTime = System.currentTimeMillis()
                    // 获取根节点
                    var rootNode = rootInActiveWindow
                    val endTime = System.currentTimeMillis()
                    val timeCost = endTime - startTime
                    if (timeCost > 1000) {
                        LogUtils.e(TAG, "获取最新rootInActiveWindow耗时超过1秒: ${timeCost}ms"+Thread.currentThread().name)
                        val eventType = event?.let { AccessibilityEvent.eventTypeToString(it.eventType) } ?: "unknown"
                        voiceAccessibilityPerformance.reportAccessibility(timeCost,eventType)
                    } else if(isAbleToWindowLog){
                        LogUtils.i(TAG, "获取最新rootInActiveWindow耗时: ${timeCost}ms"+Thread.currentThread().name)
                    }

                    lastCallBackTime = System.currentTimeMillis()
                    lastActiveRootNodePackage = "${rootNode?.packageName?.toString() ?: ""} child:${rootNode?.childCount}"
                    windowArrStr = ""

                    activeRootNodeVisible = rootNode?.isVisibleToUser ?: false
                    printLogOnDebug("rootInActiveWindow-------${rootNode?.packageName}; ${rootNode?.windowId}")

                    // 窗口过滤逻辑(已注释,可根据需要启用)
                    if (rootNode?.packageName == packageName || rootNode?.childCount == 0) {
                        windows?.forEach { window ->
                            window?.root?.takeIf { rn ->
                                rn.packageName != packageName && rn.childCount != 0
                            }?.apply {
                                rootNode = this
                                LogUtils.d(TAG, "替换节点.......")
                            }
                        }
                    }

为什么修改悬浮窗的属性和替换window可以解决呢?

先整理需求:

1.).需要能把悬浮窗的事件能触摸,就要用到 WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;

2).期望不影响下面window的交互,FLAG_NOT_FOCUSABLE

FLAG_NOT_FOCUSABLE : 焦点是干嘛的? 只有设置了这个,才能扫描到window对象

场景1:设置了 FLAG_NOT_FOCUSABLE

// 你的悬浮窗代码: layoutParams.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; // 用户操作和结果:

  1. 用户点击悬浮窗内的 EditText → ❌ 软键盘不会弹出
  2. 用户按物理键盘 → ❌ 悬浮窗收不到按键事件
  3. 用户按返回键 → ❌ 悬浮窗收不到返回键事件
  4. 用户按音量键 → ❌ 悬浮窗收不到音量键事件

场景2:没有设置 FLAG_NOT_FOCUSABLE // 不设置 FLAG_NOT_FOCUSABLE: // layoutParams.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; // 注释掉

// 用户操作和结果:

  1. 用户点击悬浮窗内的 EditText → ✅ 软键盘弹出
  2. 用户按物理键盘 → ✅ 悬浮窗收到按键事件
  3. 用户按返回键 → ✅ 悬浮窗收到返回键事件
  4. 用户按音量键 → ✅ 悬浮窗收到音量键事件

Q1:设置了 FLAG_NOT_FOCUSABLE,为什么还能收到触摸事件? A:焦点和触摸是分开的。FLAG_NOT_FOCUSABLE 只影响键盘/按键事件,不影响触摸事件。要阻止触摸,需要设置 FLAG_NOT_TOUCHABLE。

Q2:WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, layoutParams.flags |= WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL; 和只设置了WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,有什么区别

FLAG_NOT_TOUCH_MODAL 的作用? 默认情况下,悬浮窗会模态拦截所有触摸事件(即使点击空白区,底层也收不到) 加上此 flag 后: 悬浮窗区域内:自己处理事件 区域外:事件传递给下层窗口 Q3:// 设置窗口标志 - 允许点击外部关闭 params.flags |= WindowManager.LayoutParams.FLAG_DIM_BEHIND;

Q4:// 设置窗口标志 - 允许点击外部关闭 这个必须配合flag一起使用, 否则也不生效 WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;

Q5:这3个步骤到底事什么意思

int canTouchFlag = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL; int cantTouchFlag = WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;

ini 复制代码
    layoutParams.flags |= canTouchFlag;
    layoutParams.flags |= cantTouchFlag;
    layoutParams.flags ^= cantTouchFlag;

这个是什么意思 经过这三步运算后:

✅ 设置了 FLAG_NOT_TOUCH_MODAL

❌ 移除了 FLAG_NOT_TOUCHABLE

❌ 移除了 FLAG_NOT_FOCUSABLE

等价于:

// 设置 NOT_TOUCH_MODAL layoutParams.flags |= WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;

4.3 总结: 焦点和触摸事件是2回事!

1).触摸响应: 只和这个有关系

layoutParams.flags |= WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;

2). 悬浮窗的事件分发问题:

设置 FLAG_NOT_FOCUSABLE + FLAG_NOT_TOUCH_MODAL onTouchEvent 返回 false, 我发现下面的launcher应用,不能接收到点击事件

结论:事件透传,(只和FLAG_NOT_TOUCHABLE有关系,注意不是FLAG_NOT_FOCUSABLE ,不遵循事件分发原理)! 权限和系统限制:一些系统为了安全,限制了悬浮窗与底层应用的交互

系统安全机制禁止应用直接干预其他应用的事件流。

悬浮窗事件透传机制

● FLAG_NOT_FOCUSABLE:窗口不获取焦点,但仍可接收触摸事件(自己得到事件)

● FLAG_NOT_TOUCH_MODAL:窗口区域内的触摸事件自己处理,区域外的传递给后面

● FLAG_NOT_TOUCHABLE:完全不接收任何触摸事件,全部传递给后面(全部给了底层)

Android悬浮窗事件分发遵循以下基本流程:

  1. 输入系统检测触摸事件
  2. WindowManagerService确定目标窗口
  3. 悬浮窗的事件分发只能针对自己的窗口,不能分发给其他窗口
csharp 复制代码
    /**
     * 设置红色区域点击关闭功能, 点击到cardView不消失
     */
    private void setupTransparentAreaClick() {
        if (mAsrView == null) {
            LogUtils.d(TAG, "setupTransparentAreaClick: mAsrView is null");
            return;
        }

        // 获取根布局
        View rootView = mAsrView.findViewById(R.id.root_view);

        if (rootView == null || cardView == null) {
            LogUtils.d(TAG, "setupTransparentAreaClick: rootView or cardView is null");
            return;
        }

        LogUtils.d(TAG, "setupTransparentAreaClick: rootView=" + rootView + ", cardView=" + cardView);

        rootView.setOnTouchListener(new View.OnTouchListener() {
            private long touchStartTime = 0;
            private float touchStartX = 0;
            private float touchStartY = 0;

            @Override
            public boolean onTouch(View v, MotionEvent event) {
                if (mAsrView == null) {
                    LogUtils.e(TAG, "onTouch: mAsrView=null!!");
                    return false;
                }
                int action = event.getAction();
                float currentX = event.getX();
                float currentY = event.getY();

                switch (action) {
                    case MotionEvent.ACTION_DOWN:
                        touchStartTime = System.currentTimeMillis();
                        touchStartX = currentX;
                        touchStartY = currentY;

                        LogUtils.d(TAG, "ACTION_DOWN: x=" + currentX + ", y=" + currentY);
                        LogUtils.d(TAG, "rootView bounds: width=" + rootView.getWidth() + ", height=" + rootView.getHeight());
                        LogUtils.d(TAG, "cardView bounds: left=" + cardView.getLeft() + ", top=" + cardView.getTop() +
                                ", right=" + cardView.getRight() + ", bottom=" + cardView.getBottom());

                        // 判断点击是否在cardView区域内
                        boolean isInsideCardView = isPointInsideView(currentX, currentY, cardView);
                        LogUtils.d(TAG, "isInsideCardView: " + isInsideCardView);

                        // 如果点击在cardView内,不处理,让子View处理
                        if (isInsideCardView) {
                            LogUtils.d(TAG, "点击在cardView内,不处理触摸事件");
                            return false;
                        }
                        return true;

                    case MotionEvent.ACTION_MOVE:
//                        LogUtils.d(TAG, "ACTION_MOVE: x=" + currentX + ", y=" + currentY);
                        break;

                    case MotionEvent.ACTION_UP:
                    case MotionEvent.ACTION_CANCEL:
                        long touchDuration = System.currentTimeMillis() - touchStartTime;
                        float deltaX = Math.abs(currentX - touchStartX);
                        float deltaY = Math.abs(currentY - touchStartY);

                        LogUtils.d(TAG, "ACTION_UP: x=" + currentX + ", y=" + currentY +
                                ", duration=" + touchDuration + "ms, deltaX=" + deltaX + ", deltaY=" + deltaY);

                        // 判断是否是点击事件(不是滑动)
                        if (touchDuration < 500 && deltaX < 50 && deltaY < 50) {
                            boolean isInside = isPointInsideView(currentX, currentY, cardView);
                            LogUtils.d(TAG, "点击事件处理: isInsideCardView=" + isInside);

                            // 如果点击在cardView外,关闭界面
                            if (!isInside) {
                                LogUtils.i("baidu", "点击在cardView外部,触发关闭");
                                stopDialog();
                                LogUtils.i("baidu", "点击在cardView外部,触发关闭=====");
                                AsrUiManger.getPhone().sendTimeoutDelayTime(1500);

                                return true;
                            } else {
                                LogUtils.d(TAG, "点击在cardView内部,不触发关闭");
                            }
                        } else {
                            LogUtils.d(TAG, "滑动事件,不触发关闭");
                        }
                        break;
                }
                return false;
            }
        });
    }
3).FLAG 标志含义

FLAG_NOT_FOCUSABLE: 窗口不获取输入焦点,但可接收触摸

FLAG_NOT_TOUCH_MODAL: 窗口外部触摸传递给下层

FLAG_WATCH_OUTSIDE_TOUCH: 可接收窗口外部的触摸事件

FLAG_NOT_TOUCHABLE: 完全不接收触摸事件

5.最终建议

采用方案三的优化悬浮窗属性 + 智能无障碍服务处理

悬浮窗设置为半透明、不获取焦点、允许触摸穿透

在系统弹框出现时,自动将悬浮窗缩小/淡化

无障碍服务实现智能根节点查找算法

这样既能保证悬浮窗的功能,又能让系统弹框正常被无障碍服务扫描到,实现"可见即可说"的完整功能。

相关推荐
Kapaseker5 小时前
前端已死...了吗
android·前端·javascript
m0_471199635 小时前
【自动化】前端开发,如何将 Jenkins 与 Gitee 结合实现自动化的持续集成(构建)和持续部署(发布)
前端·gitee·自动化·jenkins
w***95495 小时前
spring-boot-starter和spring-boot-starter-web的关联
前端
Moment5 小时前
富文本编辑器技术选型,到底是 Prosemirror 还是 Tiptap 好 ❓❓❓
前端·javascript·面试
xkxnq5 小时前
第二阶段:Vue 组件化开发(第 18天)
前端·javascript·vue.js
晓得迷路了6 小时前
栗子前端技术周刊第 112 期 - Rspack 1.7、2025 JS 新星榜单、HTML 状态调查...
前端·javascript·html
怕浪猫6 小时前
React从入门到出门 第五章 React Router 配置与原理初探
前端·javascript·react.js
jinmo_C++6 小时前
从零开始学前端 · HTML 基础篇(一):认识 HTML 与页面结构
前端·html·状态模式
Winston Wood6 小时前
Android图形与显示系统经典故障解决方案:从源码到实操
android·图形系统·显示系统