阅读 941

Jetpack Compose处理“导航栏、状态栏、键盘” 影响内容显示的问题集锦

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如何把px转dp,dp转px

那么我们如何获取“状态栏”的高度呢?
在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"
复制代码

如有其他疑惑的小伙伴,可以在评论区留言


键盘遮挡问题修复

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方法


往期文章推荐:
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正确的保活方案,不要掉进保活需求死循环陷进

文章分类
Android