注册

Android 沉浸式状态栏,透明状态栏 采用系统api,超简单近乎完美的实现

前言


沉浸式的适配有多麻烦,相信大家既然来搜索这个,就说明都在为此苦恼,那么看看这篇文章吧,也许对你有所帮助(最下面有源码链接)


有写的不对的地方,欢迎指出


从adnroid 6.0开始,官方逐渐完善了这方面的api,直到android 11...


... 让我们直接开始吧


导入核心包


老项目非androidx的请自行研究下,这里使用的是androidx,并且用的kotlin语言
本次实现方式跟windowInsets息息相关,这可真是个好东西
首先是需要导入核心包
androidx.core:core

kotlin可选择导入这个:
androidx.core:core-ktx
我用的版本是
androidx.core:core-ktx:1.12.0

开启 “沉浸式” 支持


沉浸式原本的意思似乎是指全屏吧。。。算了,不管那么多,喊习惯了 沉浸式状态栏,就这么称呼吧。

在activity 的oncreate里调用
//将decorView的fitSystemWindows属性设置为false
WindowCompat.setDecorFitsSystemWindows(window, false)
//设置状态栏颜色为透明
window.statusBarColor = Color.TRANSPARENT
//是否需要改变状态栏上的 图标、字体 的颜色
//获取InsetsController
val insetsController = WindowCompat.getInsetsController(window, window.decorView)
//mask:遮罩 默认是false
//mask = true 状态栏字体颜色为黑色,一般在状态栏下面的背景色为浅色时使用
//mask = false 状态栏字体颜色为白色,一般在状态栏下面的背景色为深色时使用
var mask = true
insetsController.isAppearanceLightStatusBars = mask
//底部导航栏是否需要修改
//android Q+ 去掉虚拟导航键 的灰色半透明遮罩
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
window.isNavigationBarContrastEnforced = false
}
//设置虚拟导航键的 背景色为透明
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
//8.0+ 虚拟导航键图标颜色可以修改,所以背景可以用透明
window.navigationBarColor = Color.TRANSPARENT
} else {
//低版本因为导航键图标颜色无法修改,建议用黑色,不要透明
window.navigationBarColor = Color.BLACK
}
//是否需要修改导航键的颜色,mask 同上面状态栏的一样
insetsController.isAppearanceLightNavigationBars = mask

修改 状态栏、虚拟导航键 的图标颜色,可以在任意需要的时候设置,防止图标和字体颜色和背景色一致导致看不清

补充一下:
状态栏和虚拟导航栏的背景色要注意以下问题:
1.在低于6.0的手机上,状态栏上的图标、字体颜色是白色且不支持修改的,MIUI,Flyme这些除外,因为它们有自己的api能实现修改颜色
2.在低于8.0的手机上,虚拟导航栏的图标、字体颜色是白色且不支持修改的,MIUI,Flyme这些除外,因为他们有自己的api能实现修改颜色
解决方案:
低于指定版本的系统上,对应的颜色就不要用透明,除非你的APP页面是深色背景,否则,建议采用半透明的灰色

在带有刘海或者挖孔屏上,横屏时刘海或者挖孔的那条边会有黑边,解决方法是:
给APP的主题v27加上
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
参考图:

image.png


监听OnApplyWindowInsetsListener


//准备一个boolean变量 作为是否在跑动画的标记
var flagProgress = false

//这里可以使用decorView或者是任意view
val view = window.decorView

//监听windowInsets变化
ViewCompat.setOnApplyWindowInsetsListener(view) { view: View, insetsCompat: WindowInsetsCompat ->
//如果要配合下面的setWindowInsetsAnimationCallback一起用的话,一定要记得,onProgress的时候,这里做个拦截,直接返回 insets
if (flagProgress) return@setOnApplyWindowInsetsListener insetsCompat
//在这里开始给需要的控件分发windowInsets

//最后,选择不消费这个insets,也可以选择消费掉,不在往子控件分发
insetsCompat
}
//带平滑过渡的windowInsets变化,ViewCompat中的这个,官方提供了 api 21-api 29的支持,本来这个只支持 api 30+的,相当不错!
//启用setWindowInsetsAnimationCallback的同时,也必须要启用上面的setOnApplyWindowInsetsListener,否则在某些情况下,windowInsets改变了,但是因为不会触发setWindowInsetsAnimationCallback导致padding没有更新到UI上
//DISPATCH_MODE_CONTINUE_ON_SUBTREE这个代表动画事件继续分发下去给子View
ViewCompat.setWindowInsetsAnimationCallback(view, object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_CONTINUE_ON_SUBTREE) {
override fun onProgress(insetsCompat: WindowInsetsCompat, runningAnimations: List<WindowInsetsAnimationCompat>): WindowInsetsCompat {
//每一帧的windowInsets
//可以在这里分发给需要的View。例如给一个聊天窗口包含editText的布局设置这个padding,可以实现键盘弹起时,在底部的editText跟着键盘一起滑上去,参考微信聊天界面,这个比微信还丝滑(android 11+最完美)。
//最后,直接原样return,不消费
return insetsCompat
}

override fun onEnd(animation: WindowInsetsAnimationCompat) {
super.onEnd(animation)
//动画结束,将标记置否
flagProgress = false
}

override fun onPrepare(animation: WindowInsetsAnimationCompat) {
super.onPrepare(animation)
//动画准备开始,在这里可以记录一些UI状态信息,这里将标记设置为true
flagProgress = true
}
})

读取高度值


通过上面的监听,我们能拿到WindowInsetsCompat对象,现在,我们从这里面取到我们需要的高度值


先定义几个变量,我们需要拿的包含:
1. 刘海,挖空区域所占据的宽度或者是高度
2. 被系统栏遮挡的区域
3. 被输入法遮挡的区域

//cutoutPadding 刘海,挖孔区域的padding
var cutoutPaddingLeft = 0
var cutoutPaddingTop = 0
var cutoutPaddingRight = 0
var cutoutPaddingBottom = 0

//获取刘海,挖孔的高度,因为这个不是所有手机都有,所以,需要判空
insetsCompat.displayCutout?.let { displayCutout ->
cutoutPaddingTop = displayCutout.safeInsetTop
cutoutPaddingLeft = displayCutout.safeInsetLeft
cutoutPaddingRight = displayCutout.safeInsetRight
cutoutPaddingBottom = displayCutout.safeInsetBottom
}


//systemBarPadding 系统栏区域的padding
var systemBarPaddingLeft = 0
var systemBarPaddingTop = 0
var systemBarPaddingRight = 0
var systemBarPaddingBottom = 0

//获取系统栏区域的padding
//系统栏 + 输入法
val systemBars = insetsCompat.getInsets(WindowInsetsCompat.Type.ime() or WindowInsetsCompat.Type.systemBars())
//左右两侧的padding通常直接赋值即可,如果横屏状态下,虚拟导航栏在侧边,那么systemBars.left或者systemBars.right的值就是它的宽度,竖屏情况下,一般都是0
systemWindowInsetLeft = systemBars.left
systemWindowInsetRight = systemBars.right
//这里判断下输入法 和 虚拟导航栏是否存在,如果存在才设置paddingBottom
if (insetsCompat.isVisible(WindowInsetsCompat.Type.ime()) || insetsCompat.isVisible(WindowInsetsCompat.Type.navigationBars())) {
systemWindowInsetBottom = systemBars.bottom
}
//同样判断下状态栏
if (insetsCompat.isVisible(WindowInsetsCompat.Type.statusBars())) {
systemWindowInsetTop = systemBars.top
}

到这里,我们需要的信息已经全部获取到了,接下来就是根据需求,设置padding属性了

补充一下:
我发现在低于android 11的手机上,insets.isVisible(Type)返回始终为true
并且,即使系统栏被隐藏,systemBars.top, systemBars.bottom也始终会有高度
所以这里


保留原本的Padding属性


上述获取的值,直接去设置padding的话,会导致原本的padding属性失效,所以我们需要在首次设置监听,先保存一份原本的padding属性,在最后设置padding的时候,把这份原本的padding值加上即可,就不贴代码了。


第一次写文章,写的粗糙了点

可能我写的不太好,没看懂也没关系,直接去看完整代码吧


我专门写了个小工具,可以去看看:
沉浸式系统栏 小工具


如果有更好的优化方案,欢迎在github上提出,我们一起互相学习!


作者:Matchasxiaobin
来源:juejin.cn/post/7275943802938130472

0 个评论

要回复文章请先登录注册