Android 上对 SystemBar 的操作
date
Feb 11, 2022
slug
Android SystemBar
status
Published
tags
Android
summary
Android SystemBar
type
Post
关于 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 的颜色。
颜色
操作 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 的可见性了。
设置可见性也只是比设置颜色稍微麻烦一点。设置颜色操作的是 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
经过这样的设置就变成这样。
全部的可见性含义
像以上这种使 Activity 可用空间延伸到 SystemBar 的范围内,只是其中的一种用法,还有其他的状态位,通过单个使用或组合使用能做出不同的 UI 效果。
fitsSystemWindows
如果你按照以下方式进行设置,并且页面的顶部是一个或多个可操作按钮,则很有可能会遇见这种尴尬场景。
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
}
说明
- 控制 Window#statusBarColor 和 Window#navigationBarColor 为 Android 5.0 及以后才有的 API
- 代码已上传至 GitHub
更新
符合 Android Material Design 的一些 View 或 ViewGroup 已经内置了对 systemUiVisibility 的设置,仅仅通过使用 fitSystemWindows 就可处理 StatusBar 的效果,具体详情,参见郭霖在 2022.02.22 发布的公众号文章。