Android 上对 SystemBar 的操作

date
Feb 11, 2022
slug
Android SystemBar
status
Published
tags
Android
summary
Android SystemBar
type
Post
notion image
关于 Android SystemBar 和主屏幕的关系可以看做是,一个白板+画布。白板是手机屏幕空间,画布就是 Activity 的空间。SystemBar 就是白板漏出来的两个区域。上面的那个叫 StatusBar,下面那个叫 NavigationBar,两个统称叫 SystemBar。
由于 SystemBar 和 Activity 不在同一个可操作的屏幕上,是两个分离出的画面,所以 Android 有一套 API 提供给开发者在不同场合进行操作 SystemBar。
StausBar 的默认颜色,一直是开发者在 colors.xml 中定义的 colorPrimaryDark ,NavigationBar 的颜色,根据不同厂商、系统版本、亮色或暗色模式的不同也有不同的效果,在我手上的这台 Samsung SM-A315G Android 11 的测试机的亮色模式下,默认的 NavigationBar 颜色为:FFF2F2F2
在最新版本「Android Studio Bumblebee | 2021.1.1」的 Android Studio 上创建项目,已经没有了 colorPrimaryDark,取而代之是在 Style 中定义具体的 StatusBar 的颜色。
notion image

颜色

操作 SystemBar 的颜色,可以直接使用 window#statusBarColor 或 window#navigationBarColor 进行赋值一个颜色即可,以上的屏幕效果就是通过这样的方式来完成的。
private fun adjustAlpha(@ColorInt color: Int, factor: Float): Int {
    val alpha = (Color.alpha(color) * factor).roundToInt()
    val red = Color.red(color)
    val green = Color.green(color)
    val blue = Color.blue(color)
    return Color.argb(alpha, red, green, blue)
}
// 将初始色进行保存
private val normalStatusBgColor by lazy { window.statusBarColor }
// 更新色值,NavigationBar 的颜色更新与此相同
window.statusBarColor = adjustAlpha(normalStatusBgColor, 1 - fl)

可见性

虽然可以设置一个颜色,但开发者们还是不太满足,搞出了渐变色、Image 延伸到 StatusBar 的区域,这就需要配和设置 StatusBar 的可见性了。
渐变色 StatusBar
渐变色 StatusBar
设置可见性也只是比设置颜色稍微麻烦一点。设置颜色操作的是 Window 属性,而操作可见性则是 window#decorView#systemUiVisibility
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
由于 StatusBar,还是默认的颜色,仅调用以上设置依然不能看到示例效果,还需要将 window#statusBarColor 设置为透明色。
window.statusBarColor = Color.TRANSPARENT
以上就是 StatusBar 的设置部分,针对 NavigationBar 的设置,只是将 window#decorView#systemUiVisibility 的赋值变成了 View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION。同样的别忘记将 NavigationBar 的颜色设置为透明。
如果想同时对 StatusBar 和 NavigationBar 进行操作,那 systemUiVisibility 的赋值就成了
 
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
经过这样的设置就变成这样。
notion image

全部的可见性含义

像以上这种使 Activity 可用空间延伸到 SystemBar 的范围内,只是其中的一种用法,还有其他的状态位,通过单个使用或组合使用能做出不同的 UI 效果。

fitsSystemWindows

notion image
如果你按照以下方式进行设置,并且页面的顶部是一个或多个可操作按钮,则很有可能会遇见这种尴尬场景。
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREENor
        View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATIONor
        View.SYSTEM_UI_FLAG_LAYOUT_STABLE)
window.statusBarColor = Color.TRANSPARENT
window.navigationBarColor = Color.TRANSPARENT
遇到问题,解决它,很简单为根布局加一个 StatusBar 高度的 paddingTop 即可,或者更简单的,在你的页面上最靠近 StatusBar 的 View 或 ViewGroup 设置 fitsSystemWindows 属性即可,属性值为 true。
加上 fitsSystemWindows 属性后,系统会自动为 View 或 ViewGroup 加上一个 StatusBar 高度的 paddingTop,另外你的页面如果有从延伸至 StatusBar 的效果切换到普通效果(页面不延伸到 StatusBar 区域)的需求时,记得手动将 View 或 ViewGroup 的 paddingTop 重置为 0。

WindowInsets

从 Android API 30 开始,View#systemUiVisibility 的设置及相关的状态位均被标记为废弃,需要开发者迁移到 WindowInsets API 完成对 SystemBar 的适配。而且 Google 有意将 WindowInsets 打造成适配手机系统的各种区域的 API,比如 UI 绘制适配系统手势区域、监听手机键盘的打开和关闭、控制手机键盘的打开和关闭等。
使用 WindowInsets 相关 API 对 SystemBar 的操作,可以做到更便捷。之前提到的通过 window#decorView#systemUiVisibility 设置的各种状态,在 WindowInsets 中被拆分成了两个维度:Action、Behavior。
Action 是对 WindowInsets 可见性的操作,比如显示或隐藏 SystemBar,Behavior 则是在执行隐藏 SystemBar 时具体产生的效果。其中,Action 又分为显示、隐藏、切换 SystemBar 色彩模式,Behavior 则分为 3 种:
  • BEHAVIOR_SHOW_BARS_BY_TOUCH 隐藏 SystemBar 后点击屏幕显示 SystemBar
  • BEHAVIOR_SHOW_BARS_BY_SWIPE 隐藏 SystemBar 后滑动屏幕边缘方向显示 SystemBar
  • BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE 等同于第二种,但 SystemBar 显示为透明状态
Action 使用 WindowInsetsController 进行控制。
val windowInsetsController:WindowInsetsControllerCompat? = ViewCompat.getWindowInsetsController(window.decorView)

windowInsetsController?.let { controller ->
    // controller.hide(WindowInsetsCompat.Type.statusBars())
    //  controller.hide(WindowInsetsCompat.Type.statusBars())
    // statusBars + statusBars = systemBars
    controller.hide(WindowInsetsCompat.Type.systemBars())
    controller.show(WindowInsetsCompat.Type.systemBars())

    // 亮色模式
		controller.isAppearanceLightStatusBars = isLight
		controller.isAppearanceLightNavigationBars = isLight
}

EdgeToEdge

在某些场景下,我们期望页面获取屏幕上的所有空间以供显示,这就是 EdgeToEdge。在 SystemUiVisibility 时代,需要对 SystemUiVisibility 设置多种状态的合集,而且还只能是设配 SystemBar,现在 WindowInsets 为我们带来了更多的功能。
WindowCompat.setDecorFitsSystemWindows(window, false)
window.statusBarColor = Color.TRANSPARENT
window.navigationBarColor = Color.TRANSPARENT
// 仅靠此代码,如果页面的顶部或底部是一个或多个可操作按钮,会遇见内容被 SystemBar 遮挡的尴尬局面。
// 这时你就需要对可能需要处理的 View 进行设置 
ViewCompat.setOnApplyWindowInsetsListener(mBinding.btnToSetting) { v, insets ->
		val statusInsets = insets.getInsets(WindowInsetsCompat.Type.statusBars())
		Log.d(TAG, "statusInsets area ${statusInsets.top}")
		v.updateLayoutParams<ViewGroup.MarginLayoutParams> {
		    topMargin = statusInsets.top
		}
		insets
}
ViewCompat.setOnApplyWindowInsetsListener(mBinding.tvHint) { v, insets ->
    val navigationBarInsets = insets.getInsets(WindowInsetsCompat.Type.navigationBars())
    Log.d(TAG, "navigationInsets area ${navigationBarInsets.bottom}")
    v.updatePadding(bottom = navigationBarInsets.bottom)
    insets
}

说明

  1. 控制 Window#statusBarColor 和 Window#navigationBarColor 为 Android 5.0 及以后才有的 API
  1. 代码已上传至 GitHub

更新

符合 Android Material Design 的一些 View 或 ViewGroup 已经内置了对 systemUiVisibility 的设置,仅仅通过使用 fitSystemWindows 就可处理 StatusBar 的效果,具体详情,参见郭霖在 2022.02.22 发布的公众号文章。

© Craig Hart 2021 - 2025