今天在这儿系统梳理一下 Android 系统中状态栏(Status Bar)和导航栏(Navigation Bar)的显示控制,以及它们随着操作系统版本演进的变化。
核心概念
- System Bars(系统栏):统称状态栏和导航栏。
- Edge-to-Edge(边到边):指应用的内容可以绘制到屏幕的边缘,延伸到系统栏的后面。这是现代 Android UI 的推荐做法。
- Insets(边衬区):系统栏、挖孔、手势区域等占用的空间。应用需要响应 Insets 来避免内容被遮挡。
- Window Flags:用于控制窗口的各种属性,包括系统栏的可见性和行为。
- System UI Visibility Flags(View 层级):在旧版本中用于控制系统栏的可见性和布局的标志。
- WindowInsetsController:较新的 API,用于更细致地控制系统栏的外观和行为。
- WindowCompat.setDecorFitsSystemWindows(window, false):启用 Edge-to-Edge 的关键方法。
演进历程和控制方法
Android 4.0 (API 14) 及之前 – 基本控制
- 状态栏:默认可见且不透明。
- 导航栏:物理按键为主,部分设备开始出现虚拟导航栏。
- 控制方式:
- 通过 AndroidManifest.xml 中的 android:theme 设置主题,例如 @android:style/Theme.NoTitleBar.Fullscreen 来隐藏状态栏和标题栏。
- 通过 Window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN) 动态隐藏状态栏。
- 通过 Window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN) 动态显示状态栏。
- 导航栏的控制非常有限。
Android 4.1 (API 16) – “System UI Visibility” 和精细化控制
- 引入了 View.setSystemUiVisibility() 和一系列 SYSTEM_UI_FLAG_* 常量,允许更灵活地控制系统栏。
- 关键 Flags:
- SYSTEM_UI_FLAG_FULLSCREEN:隐藏状态栏(类似 FLAG_FULLSCREEN)。
- SYSTEM_UI_FLAG_HIDE_NAVIGATION:隐藏导航栏。用户与屏幕交互后,导航栏会重新出现。
- SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN:即使状态栏可见,应用内容也会布局到状态栏下方。开发者需要处理状态栏遮挡。
- SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION:即使导航栏可见,应用内容也会布局到导航栏下方。开发者需要处理导航栏遮挡。
- SYSTEM_UI_FLAG_LAYOUT_STABLE:与 LAYOUT_FULLSCREEN 或 LAYOUT_HIDE_NAVIGATION 结合使用,当系统栏显示/隐藏时,保持布局稳定,避免内容跳动。
- SYSTEM_UI_FLAG_IMMERSIVE:与 HIDE_NAVIGATION 和 FULLSCREEN 结合,当用户从屏幕边缘滑动时才显示系统栏,提供更沉浸的体验。
- SYSTEM_UI_FLAG_IMMERSIVE_STICKY: IMMERSIVE 的变体,系统栏在短暂显示后会自动再次隐藏,更适合游戏或视频播放。
- 状态栏着色:非常有限,通常是黑色或白色。
Android 4.4 (API 19) – Translucent System Bars (半透明系统栏)
- 允许将状态栏和导航栏设置为半透明。
- 控制方式:
- 在主题中设置 android:windowTranslucentStatus 和 android:windowTranslucentNavigation 为 true。
- 或者通过代码 Window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS) 和 Window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION)。
- 注意:应用内容会自动延伸到半透明系统栏的下方,需要配合 fitsSystemWindows="true" 或手动处理 Insets 来避免内容遮挡。
- 问题: fitsSystemWindows 属性的行为有时比较复杂和难以预测,尤其是在嵌套布局中。
Android 5.0 (API 21) – Material Design 和着色控制
- 状态栏着色:引入了
Window.setStatusBarColor(int color) 和
Window.setNavigationBarColor(int color),允许开发者设置系统栏为任意不透明颜色。
- 需要在主题中或代码中先添加 Window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) 并清除 FLAG_TRANSLUCENT_STATUS / FLAG_TRANSLUCENT_NAVIGATION。
- fitsSystemWindows 的改进:行为更加一致,但仍是处理 Insets 的主要(且有时棘手)方式。
- Light Status Bar(浅色状态栏图标):
- SYSTEM_UI_FLAG_LIGHT_STATUS_BAR (API 23+):当状态栏背景为浅色时,可以将状态栏图标(时间、电量、通知图标)变为深色,以保证可见性。
Android 6.0 (API 23) – Light Status Bar (正式引入)
- 如上所述,正式引入 SYSTEM_UI_FLAG_LIGHT_STATUS_BAR。
Android 8.0 (API 26) – Light Navigation Bar(浅色导航栏图标)
- 引入
SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR:当导航栏背景为浅色时,可以将导航栏按钮(返回、主页、概览)变为深色。
- 需要设备制造商支持,并且导航栏是半透明或透明的。
Android 9 (API 28) – Display Cutouts(屏幕挖孔/刘海屏)
- 随着刘海屏和挖孔屏的出现,需要处理这些区域。
-
windowLayoutInDisplayCutoutMode:在主题或窗口属性中设置,控制内容如何与挖孔区域交互:
- default:默认行为,内容在竖屏时不会延伸到挖孔区域。
- shortEdges:允许内容在竖屏和横屏时都延伸到屏幕短边上的挖孔区域。
- never:内容永远不会延伸到挖孔区域。
- WindowInsets.getDisplayCutout():获取挖孔区域的详细信息和安全边距。
Android 10 (API 29) – Gesture Navigation(手势导航)和 Edge-to-Edge 强制
- 手势导航成为主流:系统导航栏变为一个细条或者完全隐藏,通过手势操作。这对应用UI布局提出了新的要求,因为底部和侧边的手势区域可能与 UI 元素冲突。
- Edge-to-Edge 变得更加重要:Google 开始强烈推荐应用采用 Edge-to-Edge 设计。
- 强制 Edge-to-Edge 的开端:如果 targetSdkVersion >= 29,系统会更倾向于让应用内容延伸到系统栏后面,即使没有显式设置 LAYOUT_FULLSCREEN 或 LAYOUT_HIDE_NAVIGATION。
- Mandatory Gesture Areas:引入了系统手势区域的概念,应用应避免在这些区域放置关键交互元素。使用 WindowInsetsCompat.Type.systemGestures() 或 WindowInsetsCompat.Type.mandatorySystemGestures() 获取这些区域的 Insets。
Android 11 (API 30) – WindowInsetsController 和更一致的 API
- 废弃 View.setSystemUiVisibility():这个 API 因为其复杂性和难以理解的行为而被废弃。
- 引入 WindowInsetsController:提供了一套更现代化、更直观的 API 来控制系统栏的可见性、行为和外观。
- WindowCompat.getInsetsController(window, view) 获取控制器。
- hide(WindowInsets.Type.statusBars()) / hide(WindowInsets.Type.navigationBars())
- show(WindowInsets.Type.statusBars()) / show(WindowInsets.Type.navigationBars())
- setSystemBarsBehavior(BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE):类似于 IMMERSIVE_STICKY。
- BEHAVIOR_SHOW_BARS_BY_TOUCH:用户触摸即显示。
- BEHAVIOR_SHOW_BARS_BY_SWIPE:用户滑动边缘显示。
- setSystemBarsAppearance(APPEARANCE_LIGHT_STATUS_BARS, APPEARANCE_LIGHT_STATUS_BARS):控制状态栏图标颜色。
- setSystemBarsAppearance(APPEARANCE_LIGHT_NAVIGATION_BARS, APPEARANCE_LIGHT_NAVIGATION_BARS):控制导航栏图标颜色。
- WindowCompat.setDecorFitsSystemWindows(window, false):成为启用 Edge-to-Edge 的标准方法。当设置为 false 时,应用内容会延伸到系统栏后面。开发者必须使用 setOnApplyWindowInsetsListener 来处理 Insets。
Android 12 (API 31) 及之后 – 默认启用 Edge-to-Edge(针对特定情况)
- 系统进一步推动 Edge-to-Edge。
- 对于使用手势导航且 targetSdkVersion >= 31 的应用,系统可能会默认强制应用以 Edge-to-Edge 方式运行。
Android 15 (API 35) – 默认启用 Edge-to-Edge(全面)
- targetSdk 升级到 35 后,应用会默认启用 edge-to-edge 显示。这意味着 WindowCompat.setDecorFitsSystemWindows(window, false) 的行为成为默认,开发者必须处理 Insets。
现代(推荐)的控制方法 (API 30+)
- 启用 Edge-to-Edge:在 Activity 的 onCreate 方法中, super.onCreate() 之后, setContentView() 之前调用: WindowCompat.setDecorFitsSystemWindows(window, false)
- 处理 Insets:使用 ViewCompat.setOnApplyWindowInsetsListener 来获取系统栏、挖孔、手势区域等的大小,并相应地调整你布局中视图的 padding 或 margin。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
[crayon-688ef7574c07e475897672 inline="true" class="language-kotlin" lang="kotlin"]ViewCompat.setOnApplyWindowInsetsListener(yourRootViewOrSpecificView) { view, windowInsets -> val systemBarsInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) val displayCutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout()) val imeInsets = windowInsets.getInsets(WindowInsetsCompat.Type.ime()) // 软键盘 view.updatePadding( left = systemBarsInsets.left + displayCutoutInsets.left, top = systemBarsInsets.top + displayCutoutInsets.top, right = systemBarsInsets.right + displayCutoutInsets.right, bottom = systemBarsInsets.bottom + displayCutoutInsets.bottom // 或 imeInsets.bottom 当键盘弹出时 ) // 或者只处理特定方向,例如只为顶部 AppBar 添加状态栏高度的 padding // appBar.updatePadding(top = systemBarsInsets.top) // bottomNav.updatePadding(bottom = systemBarsInsets.bottom) WindowInsetsCompat.CONSUMED // 或者返回修改后的 insets } |
[/crayon]
- 控制系统栏可见性和外观( WindowInsetsController):
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
[crayon-688ef7574c082631712303 inline="true" class="language-kotlin" lang="kotlin"]val insetsController = WindowCompat.getInsetsController(window, window.decorView) // 隐藏状态栏 insetsController?.hide(WindowInsetsCompat.Type.statusBars()) // 显示状态栏 insetsController?.show(WindowInsetsCompat.Type.statusBars()) // 设置状态栏图标为深色 (当状态栏背景为浅色时) insetsController?.isAppearanceLightStatusBars = true // 设置导航栏按钮为深色 (当导航栏背景为浅色时) insetsController?.isAppearanceLightNavigationBars = true // 控制系统栏在隐藏后的行为 (例如滑动边缘显示) insetsController?.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE |
[/crayon]
- 设置系统栏颜色:即使内容延伸到系统栏后面,你仍然可以为它们设置半透明或透明的背景色,以获得更好的视觉效果。 在主题 themes.xml(推荐 values-v21 及以上):
|
1 2 |
[crayon-688ef7574c086324509580 inline="true" class="language-xml" lang="xml"]<item name="android:statusBarColor">@android:color/transparent</item> <item name="android:navigationBarColor">@android:color/transparent</item> |
[/crayon]
或者代码中(需要在
FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS 被设置后):
|
1 2 |
[crayon-688ef7574c08a618338972 inline="true" class="language-kotlin" lang="kotlin"]window.statusBarColor = Color.TRANSPARENT window.navigationBarColor = Color.TRANSPARENT |
[/crayon]
如果你希望系统栏有一个半透明的遮罩效果(例如在深色模式下),可以使用类似
#80000000(50% 透明度的黑色)的颜色。
总结和最佳实践
- 拥抱 Edge-to-Edge:这是现代 Android 应用的趋势。
- 使用 WindowCompat.setDecorFitsSystemWindows(window, false) 启用它。
- 核心是处理 Insets:使用 ViewCompat.setOnApplyWindowInsetsListener 和 WindowInsetsCompat.Type.*。
- 使用 WindowInsetsController (API 30+) 控制系统栏的可见性、行为和图标颜色。
- 避免使用废弃的 View.setSystemUiVisibility()。
- 为系统栏设置透明或半透明背景色,以配合 Edge-to-Edge 设计。
- 在不同设备和 Android 版本上充分测试,特别注意有挖孔和使用手势导航的设备。
- 利用 Material Components:许多 Material Design 组件(如 AppBarLayout、 BottomAppBar、 CoordinatorLayout)已经内置了对 Insets 的良好处理。
这个演进过程确实比较复杂,但理解了这些核心概念和 API 的变化,就能更好地掌控应用的视觉表现,并提供更沉浸的用户体验。
