如何通过显示系统悬浮窗的Android前台服务检测状态栏可见性变化
状态栏可见性检测实现方案
问题场景
当前运行带系统悬浮窗的前台服务,需要精准捕获两类状态栏状态变更:
- 状态栏隐藏:用户观看视频、运行游戏时,前台Activity进入全屏模式,状态栏收起
- 状态栏显示:用户从屏幕顶部下滑唤出通知中心、新通知到达时,状态栏弹出显示
此前尝试的两类方案均存在覆盖不全问题:
- 挂载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 仅能识别应用启动进入全屏、手动开关通知栏的基础状态,无法覆盖全场景
- 接入无障碍服务监听系统事件
对应实现代码:
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帧回调做低功耗高度差判断即可,不需要高频轮询:
- 先获取系统原生状态栏高度,不要硬编码数值
fun getSystemStatusBarHeight(): Int { val resId = resources.getIdentifier("status_bar_height", "dimen", "android") return if (resId > 0) resources.getDimensionPixelSize(resId) else 72 // 兜底值 } - 绑定帧回调,对比屏幕真实高度和悬浮窗可用高度的差值判断状态
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
相关产品推荐
相关产品推荐

