1.前言
写Compose相关例子的时候,突然不怎么使用xml,有些东西不清楚怎么下手,就比如Compose中状态栏,导航栏沉浸,键盘遮挡等问题如何处理,对这方面不清楚的同学,请往下翻看看我们如何去处理它的
前方高能预警:一定要记得收藏起来,划走了可就再也找不到了😅😅🙈🙈
2.初始态
默认创建一个工程,添加如下代码,页面除了“内容区域”之外,还有“导航栏、状态栏”
setContent {
Surface(color = Color(0xFFF74C4C),modifier = Modifier.fillMaxSize()){}
}
我们可以看到状态栏颜色是style.xml中配置的,导航栏不处理的话,默认是黑色
我们下面来看Compose中如何处理“导航栏、状态栏、键盘”遮挡的问题
3.思考并解决
3.1-内容延伸到状态栏
上面的红色背景有点突兀,不方便对比,我们用图片来代替红色背景
首先把状态栏颜色设置透明,下面提供大家简单的三种方式来设置状态栏颜色,原理都是一样的
,你随便使用哪种方式都可以
- 一:style.xml设置状态栏透明色
<item name="android:statusBarColor">@android:color/transparent</item>
- 二:window设置状态栏透明色
window.statusBarColor = ResourcesCompat.getColor(resources,android.R.color.transparent,null)
- 三:systemuicontroller设置状态栏透明色 我们需要使用这个组件库: Jetpack Compose - Accompanist 组件库
implementation "com.google.accompanist:accompanist-systemuicontroller:<version>"
代码使用如下,原理也是一样的最终都是通过window来设置的
val systemUiController = rememberSystemUiController()
systemUiController.setStatusBarColor(Color.Transparent, darkIcons = false)
我们要把内容延伸到状态栏,可以使用如下方法:
WindowCompat.setDecorFitsSystemWindows(window,false)
3.2-状态栏遮挡列表问题
上面我们使用图片可以看到,效果还是不错的,如果不是图片,我们是列表,这个时候第一位的内容是不是跑到屏幕外面了?
如果列表足够长,最后一个条目也无法完整显示因为第一个条目顶出到状态栏外面了
列表“第一个”和“最后一个”都无法正常显示
我们来一步一步的解决这个问题,我们先解决状态栏遮挡第一个条目的内容;
我们需要有一个状态栏这么高的大小,作为内边距
来保证安全边距,我们在使用LazyColumn的时候,可以看到内部有个contentPadding属性:为整个内容添加的内部填充,不是针对单独的item进行填充的
;
那么我们如何获取“状态栏”的高度呢?
在Compose中有两种方式获取:
//方式一:系统状态栏的高度
fun Resources.getStatusBarHeight():Int {
var statusBarHeight = 0
val resourceId = getIdentifier("status_bar_height", "dimen", "android")
if(resourceId >0 ){
statusBarHeight = getDimensionPixelSize(resourceId)
}
return statusBarHeight
}
//方式二:系统状态栏的高度
//1.先依赖lib库
implementation "com.google.accompanist:accompanist-insets:<version>"
//2.依赖lib库之后,使用ProvideWindowInsets可以访问LocalWindowInsets.current值
//本质是通过CompositionLocalProvider传值的
ProvideWindowInsets {
//这个返回的类型是PaddingValues,打印一下就可以看到状态栏的高度了
rememberInsetsPaddingValues(LocalWindowInsets.current.statusBars)
}
使用示例如下:
ProvideWindowInsets {
//方式一获取到的“状态栏高度”
//注意:此种方式获取到,设置仅PaddingValues是四个方向全部都有值
/*val sbPaddingValues = PaddingValues(with(LocalDensity.current) {
LocalContext.current.resources.getStatusBarHeight().toDp()
})*/
//方式二获取到的“状态栏高度”
//注意:这个方式获取到的PaddingValues只有顶部状态栏方向有值,其他方向为0.dp
val sbPaddingValues = rememberInsetsPaddingValues(LocalWindowInsets.current.statusBars)
LazyColumn(modifier = Modifier.fillMaxSize()
.background(Color(0xFFDA8E70)),
contentPadding = sbPaddingValues
) {
items(12) { index ->
Text(
text = "Item:$index", color = Color.Black, fontSize = 20.sp,
modifier = Modifier.padding(5.dp).height(60.dp)
)
Divider()
}
}
}
状态栏遮挡问题处理
我们可以看到状态栏不再遮挡,但是导航栏会遮挡,上面我们使用的是方式二,只有顶部才会有值,不建议设置四个方向都是状态栏高度
,设置之后就会影响界面其他元素边距,那么如何正确的让底部导航栏不遮挡住列表最后一个条目呢?请往下看
3.3-导航栏遮挡列表问题
看完上面状态栏遮挡的处理方法之后,导航栏不也是同样吗?稍微不同的是,我们不能在通过类似resId方式获取,现在手机都可以动态更换状态栏了,所以我们还是使用LocalWindowInsets来动态获取
ProvideWindowInsets {
//获取导航栏的高度
val navPaddingValues = rememberInsetsPaddingValues(LocalWindowInsets.current.navigationBars)
LazyColumn(modifier = Modifier
.fillMaxSize()
.background(Color(0xFFDA8E70)),
contentPadding = navPaddingValues
) {
......
}
}
导航栏遮挡问题处理
可能有人看到这里突然来了句,底部怎么这么大的距离?正常吗?请仔细看一下,每个item的高度都是一样的哦,仔细看一下是正常的;
我们把导航栏背景色设置为透明色,再来看一下效果,效果更明显
val systemUiController = rememberSystemUiController()
//设置导航栏透明色
systemUiController.setNavigationBarColor(Color.Transparent, darkIcons = false)
导航栏遮挡问题处理
3.4-同时处理状态栏和导航栏遮挡
我们使用LocalWindowInsets.current.systemBars来给contentPadding赋值,它可以同时获取“状态栏、导航栏”所占的高度,防止遮挡,这个方法不含键盘高度
同时处理 状态栏和导航栏 遮挡问题
3.5-Modifier方式
为什么使用这个方式呢?不是所有控件都有contentPadding属性可以使用,那么我们可以通过Modifier修饰符来处理,我们同时添加navigationBarPadding()和statusBarsPadding(),或者只添加一个systemBarsPadding()(这个方法不含键盘高度)
来处理遮挡问题,本质上内部是使用了Modifier.padding
ProvideWindowInsets {
LazyColumn(modifier = Modifier
.fillMaxSize()
.background(Color(0xFFDA8E70)).systemBarsPadding()
) {
......
}
}
但是Modifier的方式无法让内容延伸到“状态栏和导航栏”下方,视图整体是在“状态栏和导航栏”上方
Modifier.systemBarsPadding() 效果
3.6-处理键盘遮挡问题
我们上面提到键盘高度,那么键盘会有什么问题呢?我们先看下面这样的一个例子,以经典的文本输入框为例
@Composable
fun LoginTextField(
name : String,
updateName : (String) -> Unit,
pwd : String,
updatePwd : (String) -> Unit
){
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Bottom,
//先让它在导航栏上方显示,否则会显示在导航栏下方
//因为我们文章上面设置了内容延伸
modifier = Modifier.fillMaxSize().navigationBarsPadding()
) {
OutlinedTextField(
value = name,
onValueChange = updateName ,
label = { Text("用户名") },
placeholder = { Text(text = "请输入用户名") },
......
)
OutlinedTextField(
value = pwd,
onValueChange = updatePwd ,
label = { Text("密码") },
placeholder = { Text(text = "请输入密码") },
......
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.NumberPassword),
//隐藏密码内容
visualTransformation = PasswordVisualTransformation('*')
)
}
}
//使用如下:
ProvideWindowInsets {
var name by rememberSaveable { mutableStateOf("") }
val updateName = { _name : String ->
name = _name
}
var password by rememberSaveable { mutableStateOf("") }
val updatePassword = { _pwd : String ->
password = _pwd
}
LoginTextField(
name = name,
updateName = updateName,
pwd = password,
updatePwd = updatePassword
)
}
看一下,上面例子出现的问题,动图如下:
键盘遮挡问题
很明显,我们需要知道键盘的高度,才能做到不遮挡,我们可以使用Modifier.navigationBarsWithImePadding() 来做到安全不遮挡,会自动计算键盘打开和关闭以及导航栏的高度最大值;
我们看替换完之后的效果,一定要注意一个事情,不要因为使用了Compose而把AndroidManifest.xml遗忘了,一定要给你所在的Activity配置如下属性:
android:windowSoftInputMode="adjustResize"
如有其他疑惑的小伙伴,可以在评论区留言
键盘遮挡问题修复
3.7-全屏隐藏导航栏和状态栏
我们在写这篇文章的时候,忘记写这个场景了,评论区有个小伙提了个问题:
compose 中使用systemUiController 将状态栏或者导航栏隐藏掉,只要点击屏幕,又会显示出来,并且一直存在
我们在评论区给他提供了答案,考虑到其他小伙伴,不一定会看评论,所以这里补充进来;
我们先来看看systemUiController隐藏状态栏或者导航栏,会出现什么问题:
// systemUiController 隐藏的代码示例
@Composable
fun ExampleList(){
val systemUiController = rememberSystemUiController()
//我们再这里调用:隐藏导航栏和状态栏
systemUiController.isSystemBarsVisible = false
LazyColumn{
for (index in 0 until 100){
item {
Text(text = "Item$index",modifier = Modifier
.fillMaxWidth()
.height(50.dp),fontSize = 20.sp)
Divider()
}
}
}
}
我们看一下上面示例执行后在不同版本系统上的效果:
Android5.0 ~ Andoid11系统
我们可以看到,触摸到屏幕,系统栏自己就跑出来了,还不能自动隐藏,我们再看它再Android12+ 系统是什么效果,请往下看:
Android12+系统
很显然上面的效果并不是我们要的,为什么会出现这种情况呢?我们看一下源码
rememberSystemUiController() 里面记录的是AndroidSystemUiController
当我们调用了systemUiController.isSystemBarsVisible = false
windowInsetsController是WindowInsetsControllerCompat类型的数据
实际上内部调用的是
windowInsetsController.hide(WindowInsetsCompat.Type.statusBars())
windowInsetsController.hide(WindowInsetsCompat.Type.navigationBars())
我们找到有四个关联hide方法注释的地方:
// (【已废弃】,高版本Android12+不再支持)
// 如果有用户交互,系统栏将被强制显示
int BEHAVIOR_SHOW_BARS_BY_TOUCH = 0;
// 通过此行为隐藏的,窗口会保持此行为,我们可以通过手势从边缘滑动,再把系统栏显示出来,滑出来之后,无法自动隐藏
int BEHAVIOR_DEFAULT = 1;
// 同上(此行为【已废弃】,如需使用:建议用BEHAVIOR_DEFAULT)
int BEHAVIOR_SHOW_BARS_BY_SWIPE = BEHAVIOR_DEFAULT;
// 在此模式下隐藏系统栏时,可以通过系统手势临时显示系统栏,
// 例如从隐藏栏的屏幕边缘滑动。这些临时系统栏将覆盖应用程序的内容,
// 可能具有一定程度的透明度,并在短时间超时后自动隐藏。
int BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE = 2;
从上面的示例演示的效果,我们发现Android12+使用的行为是1
,Android12以下系统使用的行为是0
,
我们在原来hide方法的基础上,修改一下systemBarsBehavior
// 全屏隐藏系统栏,如:你看视频或者玩游戏的时候,就可以通过此种方式,体验是一样的
private fun hideSystemUI() {
WindowCompat.setDecorFitsSystemWindows(window, false)
// 如果你在Composable里面,可以参考rememberSystemUiController() 一样使用LocalView.current也可以
WindowInsetsControllerCompat(window, window.decorView).let { controller ->
controller.hide(WindowInsetsCompat.Type.systemBars())
// 修改行为
controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
}
}
// 从全屏隐藏状态下,恢复系统栏的显示
private fun showSystemUI() {
WindowCompat.setDecorFitsSystemWindows(window, true)
WindowInsetsControllerCompat(window, window.decorView).show(WindowInsetsCompat.Type.systemBars())
}
别忘了一点哈,如果你的设备有刘海屏,一定要配置窗口是可以显示在刘海区域,我们需要在themes.xml中配置如下属性,如果你不懂什么是刘海屏,请看官方的文档
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
来看最终效果吧,这个才是我们平时玩游戏,看视频的时候一模一样的效果:
最终全屏效果
可以看到,这才是我们要的全屏效果:只有我们在触摸“状态栏”和“导航栏”,才会显示出来,等几秒自动隐藏
4.总结
(1). 我们需要如下两个Lib库帮助我们
implementation "com.google.accompanist:accompanist-systemuicontroller:<version>"
implementation "com.google.accompanist:accompanist-insets:<version>"
(2). 状态栏和导航栏变色
val systemUiController = rememberSystemUiController()
//分开设置,考虑到背景颜色,我们需要动态更新图标颜色嘛
systemUiController.setStatusBarColor(Color.Transparent, darkIcons = true)
systemUiController.setNavigationBarColor(Color.Transparent, darkIcons = false)
//或者使用,直接统一两个栏
systemUiController.setSystemBarsColor(.....)
(2). 如果是列表,需要内容延伸出状态栏和导航栏,可以使用contentPadding属性,设置内容边距
ProvideWindowInsets {
//做到导航栏和状态栏都可以延伸内容
val paddingValues = rememberInsetsPaddingValues(LocalWindowInsets.current.systemBars)
LazyColumn(modifier = Modifier
.fillMaxSize()
.background(Color(0xFFDA8E70)),
contentPadding = navPaddingValues
) {
......
}
}
(3). 如果使用Modifier方式处理遮挡问题,无法做到内容延伸出“状态栏和导航栏”
Modifier.navigationBarsPadding()
Modifier.statusBarsPadding()
Modifier.systemBarsPadding()
(4). 键盘遮挡问题
AndroidManifer.xml配置:
android:windowSoftInputMode="adjustResize"
//防止键盘遮挡,文本输入框
Modifier.navigationBarsWithImePadding()
有些小伙伴可能看完会有个疑问:列表使用contentPadding的时候和Modifier.padding内部做什么,导致它们效果不同的呢?
感兴趣的同学看一下:LazyListMeasure里面的calculateItemsOffsets方法
(5). 全屏并隐藏系统导航栏和状态栏
// 全屏隐藏系统栏,如:你看视频或者玩游戏的时候,就可以通过此种方式,体验是一样的
private fun hideSystemUI() {
WindowCompat.setDecorFitsSystemWindows(window, false)
// 如果你在Composable里面,可以参考rememberSystemUiController() 一样使用LocalView.current也可以
WindowInsetsControllerCompat(window, window.decorView).let { controller ->
controller.hide(WindowInsetsCompat.Type.systemBars())
// 修改行为
controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
}
}
// 从全屏隐藏状态下,恢复系统栏的显示
private fun showSystemUI() {
WindowCompat.setDecorFitsSystemWindows(window, true)
WindowInsetsControllerCompat(window, window.decorView).show(WindowInsetsCompat.Type.systemBars())
}
支持刘海屏,需要在themes.xml中进行如下配置:
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
然后我们调用hideSystemUI() 就可以全屏显示了,调用showSystemUI() 就可以恢复系统栏显示了
往期文章推荐:
1.Android跨进程传大图思考及实现——附上原理分析
2.闲聊Android悬浮的“系统文本选择菜单”和“ActionMode解析”——附上原理分析
3.Jetpack Compose实现bringToFront功能——附上原理分析
4.Jetpack Compose UI创建布局绘制流程+原理 —— 内含概念详解(满满干货)
5.Jetpack App Startup如何使用及原理分析
6.Jetpack Compose - Accompanist 组件库
7.源码分析 | ThreadedRenderer空指针问题,顺便把Choreographer认识一下
8.源码分析 | 事件是怎么传递到Activity的?
9.聊聊CountDownLatch 源码
10.Android正确的保活方案,不要掉进保活需求死循环陷进