Android应用内容全屏绘制并保持系统栏不透明
2025-03-11 16:49:09
如何在 Android 中实现应用内容绘制到系统栏区域并保持系统栏完全不透明?
开发 Android 应用时,你可能会遇到这样一个需求:想让应用的内容扩展到状态栏和导航栏下方,同时保持系统栏(状态栏和导航栏)完全不透明,而且所有区域都要可以接收到 AMotionEvent
(触摸事件)。 简单的 AWINDOW_FLAG_*
系列标志位似乎解决不了。别急,这篇博客就来好好聊聊这个问题。
一、问题根源:默认的窗口行为
默认情况下,Android 系统会把应用窗口限制在系统栏之间。这样做有它的道理:
- 保证了系统栏可见,方便用户查看时间、电量、通知等信息。
- 防止应用界面元素被系统栏遮挡,影响用户操作。
但是,这种行为不满足所有需求,特别是在一些需要沉浸式体验或者全屏展示的场景。
二、解决方案:边缘到边缘 (Edge-to-Edge) 布局
要让应用内容绘制到系统栏下方,核心思路是采用"边缘到边缘" (Edge-to-Edge) 布局,同时控制系统栏的可见性和外观。以下是几种可行的方案,以及它们分别在非全屏模式和全屏模式下的具体实现:
方案 1:WindowInsetsControllerCompat
+ setAppearanceLightNavigationBars
(API 级别 27+)
这个方案通过 WindowInsetsControllerCompat
来精确控制系统栏外观。兼容性更好,能向后支持到 API Level 27 及以上。
原理:
WindowInsetsControllerCompat
: 提供控制窗口内边距(insets)行为和外观的能力,比如系统栏的显示/隐藏、行为以及外观。setSystemBarsBehavior()
: 设置系统栏行为,BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
允许通过边缘滑动临时显示系统栏。setAppearanceLightStatusBars()
/setAppearanceLightNavigationBars()
:设置状态栏/导航栏的前景色是否为浅色(背景通常为深色)。
非全屏模式:
// 在 Activity 的 onCreate() 方法中调用
fun setupEdgeToEdgeNonFullscreen() {
val decorView = window.decorView
val windowInsetsController = WindowCompat.getInsetsController(window, decorView)
// 将内容延伸到系统栏
WindowCompat.setDecorFitsSystemWindows(window, false)
// 设置状态栏背景透明
window.statusBarColor = Color.TRANSPARENT
//设置导航栏背景为你需要的纯色,以保证“不透明”的需求,假设用黑色
window.navigationBarColor = Color.BLACK
// 使状态栏内容具有可读性,当statusBarColor是透明色时,通过设置成true或false来将系统状态栏图标的着色变为白色或黑色.
windowInsetsController.isAppearanceLightStatusBars = false
//使导航栏上的图标/文字 可读
windowInsetsController.isAppearanceLightNavigationBars = false
}
全屏模式:
fun setupEdgeToEdgeFullscreen() {
val decorView = window.decorView
val windowInsetsController = WindowCompat.getInsetsController(window, decorView)
// 将内容延伸到系统栏下方
WindowCompat.setDecorFitsSystemWindows(window, false)
// 隐藏系统栏, 同时让内容全屏显示.
windowInsetsController.hide(WindowInsetsCompat.Type.systemBars())
// 设置系统栏在滑动边缘时出现.
windowInsetsController.systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
// 设置状态栏和导航栏背景完全透明
window.statusBarColor = Color.TRANSPARENT
window.navigationBarColor = Color.TRANSPARENT
// 使状态栏图标可读
windowInsetsController.isAppearanceLightStatusBars = false
//使导航栏上的图标/文字 可读.
windowInsetsController.isAppearanceLightNavigationBars = false
}
注意: 非全屏模式,如果为了确保“不透明” 需求,你或许需要一个纯色 navigationBarColor
值,来代替Color.TRANSPARENT
.
全屏模式则可以直接把 statusBarColor
和 navigationBarColor
设置成Color.TRANSPARENT
, 滑出系统栏,就会应用原本设置好的颜色.
安全性建议:
- 隐藏系统栏时,要提供明确的退出全屏的方式(比如手势或按钮)。
- 在进行 Edge-to-Edge 布局的时候,应妥善处理内边距(比如通过
padding
)来避免你的 UI 元素被系统栏覆盖。
进阶使用技巧:
可以使用setOnApplyWindowInsetsListener
监听WindowInsets的变化,以便动态的调整你UI的位置和大小。
方案 2: 旧版 Flags + View.SYSTEM_UI_FLAG_* (适用于旧版系统, 较复杂)
如果需要支持更旧的 Android 版本 (API Level 27 以下),可以结合使用 window.decorView.systemUiVisibility
属性和旧版的标志位,这个方式更加复杂,处理兼容性也更麻烦。
原理:
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
: 保证布局稳定,避免内容因系统栏的显示/隐藏而跳动。View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
: 将内容布局延伸到导航栏下方。View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
: 将内容布局延伸到状态栏下方。View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
: 隐藏导航栏。View.SYSTEM_UI_FLAG_FULLSCREEN
: 隐藏状态栏。View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
: 沉浸模式,用户从边缘滑动可以临时显示系统栏,并自动再次隐藏.
非全屏模式:
fun setupLegacyEdgeToEdgeNonFullscreen() {
val decorView = window.decorView
val flags = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
decorView.systemUiVisibility = flags
// 设置状态栏颜色. 保证可见性和内容不被遮挡.
window.statusBarColor = Color.TRANSPARENT
//假设黑色导航栏. 实际情况根据你应用配色而定
window.navigationBarColor = Color.BLACK
}
全屏模式:
fun setupLegacyEdgeToEdgeFullscreen() {
val decorView = window.decorView
val flags = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_FULLSCREEN
or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY)
decorView.systemUiVisibility = flags
//设置透明以便在滑出状态栏时显示已经设置的系统栏颜色
window.statusBarColor = Color.TRANSPARENT
window.navigationBarColor = Color.TRANSPARENT
}
安全性建议:
- 和方案一相同。
进阶技巧:
由于不同版本的行为有所不同,要充分测试。可以组合设置 decorView.setOnSystemUiVisibilityChangeListener
来响应系统 UI 可见性的变化。
接收 AMotionEvent
不论你选择哪一种方法去绘制你的内容,让所有的区域都能接收 AMotionEvent
的重点是保证两点:
- 确保
Window
开启FLAG_LAYOUT_IN_SCREEN
标志, 这个 flag 让 window 的 layout 会占据整个屏幕,即使系统栏存在,系统栏会绘制在你的内容之上, 但内容会“透出”到这些区域中:
window.addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN)
- 你的布局必须填充满整个屏幕。这通常意味你的 root view 需要使用
match_parent
for both width and height:<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <!-- 你的内容 --> </FrameLayout>
三、小结
实现应用内容绘制到系统栏区域并保持系统栏完全不透明,需要综合考虑 Android 系统版本、用户体验和开发成本。优先推荐使用 WindowInsetsControllerCompat
方案,因为它更简洁、易用,并且具有更好的兼容性。如果必须支持旧版本,再考虑使用旧版 Flags 方案。记得要妥善处理布局,避免 UI 元素与系统栏冲突,同时要让全屏模式可退出,提供流畅的用户体验。