You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何通过显示系统悬浮窗的Android前台服务检测状态栏可见性变化

状态栏可见性检测实现方案

问题场景

当前运行带系统悬浮窗的前台服务,需要精准捕获两类状态栏状态变更:

  • 状态栏隐藏:用户观看视频、运行游戏时,前台Activity进入全屏模式,状态栏收起
  • 状态栏显示:用户从屏幕顶部下滑唤出通知中心、新通知到达时,状态栏弹出显示
    此前尝试的两类方案均存在覆盖不全问题:
  1. 挂载1px宽度系统悬浮窗,绑定布局监听与系统UI可见性监听
    对应实现代码:
private fun setupListenerToDetectStatusBar() {
        val p = WindowManager.LayoutParams()
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
            p.type = WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY
        else
            p.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
        p.gravity = Gravity.RIGHT or Gravity.TOP
        p.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
        p.width = 1
        p.height = ViewGroup.LayoutParams.MATCH_PARENT
        p.format = PixelFormat.OPAQUE
        helperWnd = View(ctx) //View helperWnd;
        windowManager.addView(helperWnd, p)
        Log.d("status-bar", "added-global-layout listener")
        helperWnd?.viewTreeObserver?.addOnGlobalLayoutListener(layoutListener)
        helperWnd?.setBackgroundColor(Color.BLACK)
        helperWnd?.setOnSystemUiVisibilityChangeListener{
            Log.d("status-bar", "UI visibility: $it $isFullScreen")
        }
    }

测试结果:

  • GlobalLayoutListener 完全失效,无法捕获任何状态栏可见性变化
  • 已废弃的setOnSystemUiVisibilityChangeListener 仅能识别应用启动进入全屏、手动开关通知栏的基础状态,无法覆盖全场景
  1. 接入无障碍服务监听系统事件
    对应实现代码:
val LOG_TAG = "MyAccessibilityService"
    var recordScreen = false

    override fun onAccessibilityEvent(event: AccessibilityEvent?) {
        event?.let {
            var canIgnore = false
            val eventPackage = it.packageName
            logger.log("onAccessibilityEvent: A event package $eventPackage")
        }
    }

测试结果:无障碍服务可正常捕获其他类型系统事件,但无法识别状态栏可见性变化。

可落地实现方案

方案1:WindowInsets 全屏悬浮窗监听(Android 11+ 首选)

之前的悬浮窗参数配置有问题,不需要做1px宽度的窄条,直接挂一个全屏透明的悬浮窗,配合新版WindowInsets监听即可,核心实现如下:

private fun setupStatusBarDetectOnR() {
    val params = WindowManager.LayoutParams().apply {
        type = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
            WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
        else WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY
        gravity = Gravity.TOP
        // 注意必须加上这几个flag
        flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
                WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE or
                WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
        width = ViewGroup.LayoutParams.MATCH_PARENT
        height = ViewGroup.LayoutParams.MATCH_PARENT
        format = PixelFormat.TRANSLUCENT
    }
    val detectView = View(ctx).apply {
        ViewCompat.setOnApplyWindowInsetsListener(this) { _, insets ->
            val statusBarShown = insets.isVisible(WindowInsets.Type.statusBars())
            Log.d("status-bar", "状态栏当前可见:$statusBarShown")
            // 在这里写你的状态变更逻辑即可
            insets
        }
    }
    windowManager.addView(detectView, params)
    // 主动触发一次insets申请
    ViewCompat.requestApplyInsets(detectView)
}

该方案在Android 11及以上机型可覆盖全屏视频/游戏状态栏隐藏、下滑唤出通知栏、新通知弹出状态栏显示的所有场景,且无功耗问题。

方案2:帧回调高度差检测(兼容Android 10及以下版本)

低版本系统没有稳定的WindowInsets回调,用Choreographer帧回调做低功耗高度差判断即可,不需要高频轮询:

  1. 先获取系统原生状态栏高度,不要硬编码数值
    fun getSystemStatusBarHeight(): Int {
        val resId = resources.getIdentifier("status_bar_height", "dimen", "android")
        return if (resId > 0) resources.getDimensionPixelSize(resId) else 72 // 兜底值
    }
    
  2. 绑定帧回调,对比屏幕真实高度和悬浮窗可用高度的差值判断状态
    private var lastStatusBarState = false
    private val statusBarThreshold = (getSystemStatusBarHeight() * 0.8).toInt()
    private val detectCallback = object : Choreographer.FrameCallback {
        override fun doFrame(frameTimeNanos: Long) {
            // 获取屏幕真实高度
            val realScreenHeight = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
                windowManager.currentWindowMetrics.bounds.height()
            } else {
                val display = windowManager.defaultDisplay
                val metrics = DisplayMetrics()
                display.getRealMetrics(metrics)
                metrics.heightPixels
            }
            // 读取全屏悬浮窗的当前可见高度
            val visibleHeight = helperWnd.height
            val currentState = (realScreenHeight - visibleHeight) >= statusBarThreshold
            // 状态发生变化时触发回调
            if (currentState != lastStatusBarState) {
                lastStatusBarState = currentState
                Log.d("status-bar", "状态栏状态变更:可见=$currentState")
            }
            Choreographer.getInstance().postFrameCallback(this)
        }
    }
    // 启动检测
    Choreographer.getInstance().postFrameCallback(detectCallback)
    // 不需要检测时务必移除回调,避免内存泄漏
    // Choreographer.getInstance().removeFrameCallback(detectCallback)
    

方案3:无障碍服务兜底(适配特殊定制ROM)

之前的无障碍服务无法检测是因为配置不全,需要补充配置项:

  • 在无障碍服务的xml配置文件中,添加android:accessibilityEventTypes="typeWindowStateChanged|typeWindowsChanged"
  • 开启android:flagRetrieveInteractiveWindows="true"和android:canRetrieveWindowContent="true"权限
  • 在事件回调中遍历当前系统所有可见窗口,判断位于屏幕顶部的TYPE_SYSTEM类型窗口(即状态栏)的可见性即可
    该方案作为前两个方案的兜底,部分国产ROM会限制无障碍读取系统窗口信息,不需要作为主方案使用。

内容的提问来源于stack exchange,提问作者saurabhlahoti

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 00:24:24