推荐阅读:部门总监对Android组件化的最佳实战总结 ,覆盖整个互联网公司(附源码)
推荐阅读:作为一个Android程序员,精通音视频开发,寒冬再冷也冻不到你
从 Android 4.4 开始,Android 支持了状态栏和导航栏的透明效果,并在 Android 5.0 上加强了这种效果,但是实现方法却和 Android 4.4 完全不同,之后在 Android 6.0、Android 8.0 以及 Android 10.0 上都增加了一些新的特性,使得在不同 Android 版本上,要实现状态栏和导航栏同样的效果异常困难,为此,我很久以前写了一个库 UltimateBar,但是随着时间的推移以及本人的成长,我越发觉得这个库设计的不好,存在太多不合理的地方,有较多的 bug 无法解决,后来我决定设计一个更完美更强大更好用的库,于是便有了今天的主角。
UltimateBarX
这是个全新的库,采用了全新的实现方案,在设计的过程中,我考虑了各种业务场景,适配各个 Android 版本和各家手机厂商,经过几个非正式版本的迭代,现在我终于敢拍胸脯自信地说,这就是 github 上最好用的 Android 状态栏导航栏的库,没有之一。
关于命名
Ultimate 翻译过来是「终极」的意思,在设计第一个库的时候,我写了一篇文章叫透明状态栏和导航栏的终极解决方案(虽然后来觉得并不终极),于是就命名为了「UltimateBar」,现在命名为「UltimateBarX」,是借鉴了 Google 爸爸的「AndroidX」,Google 也是嫌弃 support 库太乱,弄了 AndroidX 来统一,这个命名倒是有异曲同工之妙。
基本实现方案
首先说说什么是「沉浸式状态栏」,什么是「透明状态栏」,关于这一点,郭神在很久以前有一篇文章已经说的很清楚了
Android状态栏微技巧,带你真正理解沉浸式模式
我简单总结一下,如下图所示,很多人称这种效果为「沉浸式」,其实这并不是真正的沉浸式,而只能叫「透明」,真正的沉浸式是状态栏完全不可见,这个暂不讨论,先说说这里的「透明」,可以看到,它效果就是状态栏和导航栏本身是透明的,然后布局内容侵入到状态栏和导航栏内部
那么这种效果要怎么实现呢,在 Android 4.4 上是这样的
private fun test() {
window?.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
window?.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION)
}
复制代码
Android 5.0 以上则是
private fun test() {
val flag = (View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE)
window?.decorView?.systemUiVisibility = flag
window?.statusBarColor = Color.TRANSPARENT
window?.navigationBarColor = Color.TRANSPARENT
}
复制代码
再看一下另外一种常见的效果
用语言描述一下就是,状态栏和导航栏的颜色都是红色,状态栏下面的 Toolbar 的颜色也是红色,并且布局内容没有侵入到状态栏和导航栏内部。
这种效果在 Android 5.0 以上非常好实现,两行代码就可以
private fun test() {
window?.statusBarColor = Color.RED
window?.navigationBarColor = Color.RED
}
复制代码
但是在 Android 4.4 上就比较麻烦了,因为 Android 4.4 是无法直接给状态栏和导航栏设置颜色的,要实现这种效果,比较常见的解决方案就是在 Activity
的 DecroView
中,在状态栏和导航栏的位置分别添加一个 View
,根据需要给 View
设置背景色,然后让布局内容不侵入到状态栏和导航栏内部,就可以造成这种视觉效果了,代码如下
private fun test() {
window?.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
window?.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION)
val decorView = window?.decorView as FrameLayout?
val contentView = decorView?.findViewById(android.R.id.content)?.getChildAt(0)
contentView?.fitsSystemWindows = true
val statusBarView = View(this)
statusBarView.setBackgroundColor(Color.RED)
val statusBarLP = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, getStatusBarHeight())
statusBarLP.gravity = Gravity.TOP
decorView?.addView(statusBarView, statusBarLP)
val navigationBarView = View(this)
navigationBarView.setBackgroundColor(Color.RED)
val navigationBarLP = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, getNavigationBarHeight())
navigationBarLP.gravity = Gravity.BOTTOM
decorView?.addView(navigationBarView, navigationBarLP)
}
复制代码
上面一直有提到布局内容侵入到状态栏和导航栏,那么怎么设置让布局内容侵入或者不侵入呢,其实很简单,调用布局根 View
的 setFitsSystemWindows
方法即可(true
表示不侵入,默认 false
),但是这个方法有个缺陷,它对状态栏和导航栏是同时生效的,也就是说,要么都侵入,要么都不侵入,那如果现在的需求是状态栏侵入,但导航栏不侵入该怎么办呢,显然就实现不了了, 为了解决这个问题,我在设计 UltimateBarX 的时候,就用了很极端的方法,先让状态栏和导航栏都侵入,当遇到不需要侵入的情况时,给 DecroView
增加 paddingTop
和 paddingBottom
就可以了。
到这里,思路已经很明显了,不管是 Android 4.4 还是 Android 5.0 以上,都给状态栏和导航栏设置透明效果并侵入,获取状态栏的高度为 statusBarHeight
, 然后如果需要状态栏不透明,就在状态栏的位置给 DecroView
增加一个有背景色高度为 statusBarHeight
的 View
,姑且称它为 StatusBarView
,如果状态栏需要不侵入,就设置 DecroView
的 paddingTop
为 statusBarHeight
,另外需要提一点的是,由于 StatusBarView
也是 Decorview
的子 View
,而 DecorView
设置了 paddingTop
,这时候 StatusBarView
的实际位置会跑到状态栏的下方,所以需要给它设置 marginTop
为 -statusBarHeight
,同时需要调用 DecroView
的 setClipToPadding(false)
方法,保证 StatusBarView
可见,导航栏的设置方法也是类似,这样就可以实现状态栏和导航栏完全分开设置,不再耦合了,最终代码如下
private fun test(
statusBarFitWindow: Boolean,
@ColorInt statusBarColor: Int,
navigationBarFitWindow: Boolean,
@ColorInt navigationBarColor: Int
) {
transparentBar()
setStatusBarView(statusBarFitWindow, statusBarColor)
setNavigationBarView(navigationBarFitWindow, navigationBarColor)
}
private fun transparentBar() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val flag = (View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE)
window?.decorView?.systemUiVisibility = flag
window?.statusBarColor = Color.TRANSPARENT
window?.navigationBarColor = Color.TRANSPARENT
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
window?.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
window?.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION)
}
val decorView = window?.decorView as FrameLayout?
val contentView = decorView?.findViewById(android.R.id.content)?.getChildAt(0)
contentView?.fitsSystemWindows = false
decorView?.clipToPadding = false
}
private fun setStatusBarView(statusBarFitWindow: Boolean, @ColorInt statusBarColor: Int) {
val decorView = window?.decorView as FrameLayout?
var statusBarView = decorView?.findViewWithTag("status_bar")
if (statusBarView == null) {
statusBarView = View(this)
statusBarView.tag = "status_bar"
}
statusBarView.setBackgroundColor(statusBarColor)
val statusBarLP =
FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, getStatusBarHeight())
if (statusBarFitWindow) {
statusBarLP.topMargin = -getStatusBarHeight()
decorView?.setPadding(0, getStatusBarHeight(), 0, decorView.paddingBottom)
} else {
statusBarLP.topMargin = 0
decorView?.setPadding(0, 0, 0, decorView.paddingBottom)
}
statusBarLP.gravity = Gravity.TOP
decorView?.addView(statusBarView, statusBarLP)
}
private fun setNavigationBarView(navigationBarFitWindow: Boolean, @ColorInt navigationBarColor: Int) {
val decorView = window?.decorView as FrameLayout?
var navigationBarView = decorView?.findViewWithTag("navigation_bar")
if (navigationBarView == null) {
navigationBarView = View(this)
navigationBarView.tag = "navigation_bar"
}
navigationBarView.setBackgroundColor(navigationBarColor)
val navigationBarLP =
FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, getNavigationBarHeight())
if (navigationBarFitWindow) {
navigationBarLP.bottomMargin = -getNavigationBarHeight()
decorView?.setPadding(0, decorView.paddingTop, 0, getNavigationBarHeight())
} else {
navigationBarLP.bottomMargin = 0
decorView?.setPadding(0, decorView.paddingTop, 0, 0)
}
navigationBarLP.gravity = Gravity.BOTTOM
decorView?.addView(navigationBarView, navigationBarLP)
}
复制代码
到此,UltimateBarX 的最基本的功能已经实现了。
light 模式
在 Android 6.0 的以上,状态栏支持字体变灰色,Android 8.0 以上,导航栏支持导航按钮变灰色,效果如下所示
我们可以称它为「light 模式」,调用 DecroView
的 setSystemUiVisibility(int visibility)
方法给它设置一些 flag
即可实现,代码如下
private fun test() {
val flag = (View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR)
window?.decorView?.systemUiVisibility = flag
window?.statusBarColor = Color.TRANSPARENT
window?.navigationBarColor = Color.TRANSPARENT
}
复制代码
而前面的状态栏导航栏透明效果也是依赖这些 flag
,并且状态栏和导航栏的 light 模式也是耦合在一起的,假设一种场景,如果一开始使用 SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
给状态栏设置了 light 模式,然后需要设置导航栏的 light 模式,需要重新调用 setSystemUiVisibility(int visibility)
方法并设置 SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
,这时候按理说状态栏的灰色字体应该保持不变,所以要同时加上 SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
,但是在 Activity
的外部,并不知道上次设置了状态栏的 light 模式,因此,在每一次设置完状态栏或者导航栏的 light 模式时,都需要把它们的 light 状态记下来。
那么这个状态记在哪里呢?UltimateBarX 中使用了单例类来保存,为了可以记录多个 Activity 的状态,在单例类中创建了两个 Map
,分别用于保存状态栏和导航栏的 light 模式状态,Map
的 key
就是 Activity
对象,那么问题来了,我们知道,单例的生命周期是贯穿整个应用的生命周期的,在单例中持有 Activity
对象会导致 Activity
不能被回收,造成内存泄漏,所以必须要 Activity
关闭的时候把 Map
中的对应数据移除掉,那么怎么监听 Activity
的关闭呢?
比较常规的一个方法就是在 Activity
中添加一个看不见的 Fragment
,只要监听 Fragment
的 onDestroy
方法即可,大名鼎鼎的图片加载库「Glide」就是用的这种套路,不过 Google 在「JetPack」中增加了很多好用的组件,Lifecycle
就是其中一种,通过Lifecycle
就可以非常方便的监听 Activity
的各个生命周期方法,而不需要繁琐的添加 Fragment
了,UltimateBarX 就是采用 Lifecycle
来监听的,代码如下
internal class UltimateBarXObserver: LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun onDestroy(owner: LifecycleOwner) {
UltimateBarXManager.getInstance().removeAllData(owner)
}
}
private fun test() {
addObserver(UltimateBarXObserver())
}
复制代码
适配全面屏导航栏
在非全面屏手机上,如果不对导航栏做任何设置,那么它的背景就是一个黑条,上面有三个白色的导航按钮,如下图所示
前面提到,UltimateBarX 的基本原理就是先让状态栏和导航栏全部透明并侵入,然后再添加 View
,设置 padding
和 margin
如果现在只需要设置状态栏而不设置导航栏,那么根据前面讲的原理,这时候导航栏也要透明并且侵入的,但是要让视觉效果上没有被设置过,怎么办?方法就是增加一种默认设置效果,在导航栏的位置上增加一个黑色背景的 View
,并设置它的 marginBottom
和 DecroView
的 paddingBottom
,但是全面屏的手机,它的导航栏默认是白色的,并且导航按钮是灰色的,如下图所示
显然,对于全面屏的手机,就不能用这种默认的方法了,否则就会在设置状态栏的时候导致导航栏变黑色
这个问题该如何解决呢?首先,全面屏手机不好判断,另外,即使可以判断,也不见得所有的全面屏手机默认导航栏都是白色的,所以直接在设置默认效果的地方判断是不是全面屏并设置不同的效果显然不合理,UltimateBarX 使用的方法是在第一次给 Activity
设置透明效果之前,先调用 Window
的 getNavigationBarColor
方法拿到当前 Activity
的导航栏颜色,并根据导航栏颜色判断是否是 light 模式,然后把导航栏的初始颜色和 light 模式状态也保存在单例中,后面如果需要给导航栏设置默认效果时,直接从单例里面取数据设置即可
private fun putOriginColor() {
val navigationBarColor = window?.navigationBarColor ?: Color.TRANSPARENT
originColorMap[this] = navigationBarColor
val navConfig = getNavigationBarConfig(this)
navConfig.light = calculateLight(navigationBarColor)
putNavigationBarConfig(this, navConfig)
}
private fun calculateLight(@ColorInt color: Int) = color > (Color.BLACK + Color.WHITE / 2)
复制代码
这样在设置状态栏的时候,在视觉效果上就不会对导航栏造成影响了
到此为止,UltimateBarX 在 Activity
中的功能已经基本实现了,它的使用方法非常简单,并且功能非常强大,可以在同一个 Activity
中多次设置不同的效果,并且把状态栏和导航栏的设置彻底解耦,真正做到了单独设置,互不影响,纵观 github 上的关于 Android 状态栏和导航栏的库,能做到这两点的可以说没有,只此一家,绝无分店,所以说,这就是 github 上最好用的 Android 状态栏导航栏库,没有之一,使用方法也非常简单,一行代码链式调用即可
private fun test() {
UltimateBarX.with(this) // 在当前 Activity 生效
.fitWindow(true) // 是否侵入状态栏 (true: 不侵入)
.color(Color.BLACK) // 状态栏颜色(色值)
.colorRes(R.color.deepSkyBlue) // 状态栏颜色(资源 id)
.drawableRes(R.drawable.bg_gradient) // 状态栏背景(drawable 资源)
.light(false) // light 模式(true: 字体变灰)
.applyStatusBar() // 应用到状态栏
}
复制代码
如果要应用到导航栏,把最后一行代码改成applyNavigationBar()
即可。
另外,UltimateBarX 也是支持直接设置 Fragment
的,由于篇幅有限,关于 UltimateBarX 在 Fragment
中的实现原理,请听下回分晓。
作者:Zackratos
链接:https://juejin.cn/post/6903165109485436935