除了app的内容区域外,还有一些其他的固定元素会显示在手机屏幕上,顶部的状态栏、 刘海、 底部的导航栏,还有输入法键盘,它们都是系统的UI, 也叫Insets.
如图所示:
顶部的状态栏通常被用来展示通知, 设备状态等;
底部导航栏通常显示三个导航按钮: back, home, recent.
它们两个合称为system bars.
Android的Insets类描述的是偏移尺寸信息, 确实我们开发中更关注的也就是这些系统UI的尺寸信息.
本文介绍用Compose做UI之后, 借助于Accompanist Insets: Guide - Accompanist.
几种常见的和Insets相关的情形是如何做的.
新创建一个用Compose写的app, 默认是一个没有Inset处理的普通App.
那能不能让app的内容显示在这些system bars区域, 做成edge-to-edge的形式?
当然是可以的.
这里澄清两个概念:
内容延伸到status bar和navigation bar区域很容易, 只需要加一行代码:
WindowCompat.setDecorFitsSystemWindows(window, false)
这个值默认是true, 表示默认行为: app的内容会自动找到内嵌区域绘制.
设置为false之后, app的内容就会延伸到system bars下层.
区别见下图: 左边为默认显示, 右边为添加了这个flag为false的设置之后的情况:
嗯, 内容是绘制出去了, 但是却被遮挡了.
这时候就需要用到systemuicontroller来改颜色:
加上这么几行就可以改自己喜欢的颜色:
val systemUiController = rememberSystemUiController()
val useDarkIcons = MaterialTheme.colors.isLight
SideEffect {
systemUiController.setSystemBarsColor(
color = Color.Green.copy(alpha = 0.1f),
darkIcons = useDarkIcons
)
}
这里改的是system bars, 也即status bar和navigation bar都改了. 也有单独只改一个的方法.
为了demo, 把颜色设置成透明的绿(如左图);
正常应用场景有可能得用Color.Transparent
(如下图).
紧接做了几个页面的UI之后, 发现有的内容遮盖在状态栏和底部, 体验不是很好.
能不能把有文字内容的部分让出来呢?
于是, 添加了这个依赖: Insets for Jetpack Compose
简单两行就把上下的距离留了出来:
ProvideWindowInsets {
Sample1(modifier = Modifier.systemBarsPadding())
}
等等, 这么一处理, 如果忽略system bars颜色的设置.
和最开始默认的情形看起来是一模一样.
那么我们是不是可以直接删掉WindowCompat.setDecorFitsSystemWindows(window, false)
这行, 用默认设置就好了?
如果需求想要的是背景延伸出去, 文字内嵌.
分别给上下两个元素加了不同的padding:
Column(
modifier = modifier.fillMaxSize()
.background(color = Color.Blue.copy(alpha = 0.3f)),
verticalArrangement = Arrangement.SpaceBetween
) {
Text(
modifier = Modifier.fillMaxWidth()
.background(color = Color.Yellow.copy(alpha = 0.5f))
.statusBarsPadding(),
text = "Top Text",
style = MaterialTheme.typography.h2
)
Text(text = "Content", style = MaterialTheme.typography.h2)
Text(
modifier = Modifier.fillMaxWidth()
.navigationBarsPadding()
.background(color = Color.Yellow.copy(alpha = 0.5f)),
text = "Bottom Text",
style = MaterialTheme.typography.h2
)
}
运行以后如下图中右边所示:
注意这里modifier的顺序, 上下延伸出去的颜色是不同的, 下面延伸出去的其实是Column的颜色.
左边是把insets padding加在整体布局的情况, 如果用的是system bars的话, 和默认UI效果是一样的.
具体根据需求定制即可.
有一个非常长的LazyColumn, 在edge-to-edge的设计下应该怎么显示呢?
这里有三种选择:
LazyColumn {}
LazyColumn(modifier = Modifier.systemBarsPadding()) {}
LazyColumn(
contentPadding = rememberInsetsPaddingValues(
insets = LocalWindowInsets.current.systemBars,
applyTop = true,
applyBottom = true,
)
) {}
其实1和2的行为非常类似, 只是显示区域大小的区别.
content padding只是在第一个item的上面和最后一个item的下面加上padding,
在滚动的中间过程中内容是可以全屏的, 只有到头或者到底了才会显示出padding.
content padding用动图更能说明情况:
Insets这个库提供了这么几个Modifier:
Modifier.statusBarsPadding()
Modifier.navigationBarsPadding()
Modifier.systemBarsPadding()
Modifier.imePadding()
Modifier.navigationBarsWithImePadding()
Modifier.cutoutPadding()
如果这些都不满足你的需求, 也可以直接用尺寸:
Modifier.statusBarsHeight()
Modifier.navigationBarsHeight()
Modifier.navigationBarsWidth()
LocalWindowInsets.current
自己获取想要inset类型的相关尺寸.on-screen keyboard, 又叫IME (Input Method Editor),
一般点击输入框会弹出, IME也是一种Inset.
当输入框处于屏幕上半屏的时候, 基本不用考虑键盘遮挡的问题.
但是当输入框在屏幕下半屏, 我们需要在键盘弹出来的时候让输入框完全显示出来而不被盖住.
解决这个问题需要这么几个东西:
android:windowSoftInputMode="adjustResize"
, 表示在键盘弹出时, Activity会改变布局大小, 这种改变是挤压型的.Modifier.imePadding
的使用, 给布局加上一个恰好等于键盘高度的bottom padding. 通常是给输入框的父布局, 加在哪一层视情况而定.根据这个issue下的这条comment,
可以用这个Modifier, 在这个ui获取到焦点的时候, 自己把自己bring into view。
@ExperimentalComposeUiApi
fun Modifier.bringIntoViewAfterImeAnimation(): Modifier = composed {
val imeInsets = LocalWindowInsets.current.ime
var focusState by remember { mutableStateOf(null) }
val relocationRequester = remember { RelocationRequester() }
LaunchedEffect(
imeInsets.isVisible,
imeInsets.animationInProgress,
focusState,
relocationRequester
) {
if (imeInsets.isVisible &&
!imeInsets.animationInProgress &&
focusState?.isFocused == true) {
relocationRequester.bringIntoView()
}
}
relocationRequester(relocationRequester)
.onFocusChanged { focusState = it }
}
这个ReloactionRequest
已经deprecated了, Compose新版的叫BringIntoViewRequester
.
.imePadding()
的值是变化的, 在没有键盘的情况下是0, 等有键盘的时候变为键盘高度.
计算键盘弹出的高度要注意:
.imePadding()
完事, 布局的bottom padding自动和IME贴合..navigationBarsWithImePadding()
, 它是取IME和navigation bar高度的最大值.navigationBarsPadding
. 这时候可以自己做一个减法处理.LazyColumn(
contentPadding = PaddingValues(
bottom = with(LocalDensity.current) {
LocalWindowInsets.current.ime.bottom.toDp() - innerPadding.bottom
}.coerceAtLeast(0.dp)
)
) { /* ... */ }
.imePadding
放在哪里, 关系到什么样的区域会被显示出来, 被包裹的区域会显示在键盘上方.
来举个例子, 有个带输入框的界面.
我们给它整体设置一个.navigationBarsWithImePadding()
, 表示没键盘的时候, 底部留navigation bar的高度, 有键盘的时候留键盘的高度:
Column(
modifier = Modifier.fillMaxSize().statusBarsPadding().navigationBarsWithImePadding()
.background(color = Color.Cyan.copy(alpha = 0.2f)),
verticalArrangement = Arrangement.SpaceBetween
) {
Text(
modifier = Modifier.fillMaxWidth()
.background(color = Color.Yellow.copy(alpha = 0.5f)),
text = "Top Text",
style = MaterialTheme.typography.h2
)
Text(text = "Content", style = MaterialTheme.typography.h2)
MyTextField("Text Field 1")
MyTextField("Text Field 2")
Text(
modifier = Modifier.fillMaxWidth()
.background(color = Color.Yellow.copy(alpha = 0.5f)),
text = "Bottom Text",
style = MaterialTheme.typography.h2
)
}
键盘弹出时, Bottom Text也会被顶上去, 这是因为imePadding作用于整块的布局.
如果我们这样改, 只包裹输入框的部分, 那么键盘就不会把底部的UI顶上去:
Column(
modifier = Modifier.fillMaxSize().statusBarsPadding()
.background(color = Color.Cyan.copy(alpha = 0.2f)),
verticalArrangement = Arrangement.SpaceBetween
) {
Text(
modifier = Modifier.fillMaxWidth()
.background(color = Color.Yellow.copy(alpha = 0.5f)),
text = "Top Text",
style = MaterialTheme.typography.h2
)
Text(text = "Content", style = MaterialTheme.typography.h2)
Text(
modifier = Modifier.fillMaxWidth()
.background(color = Color.Yellow.copy(alpha = 0.5f)),
text = "Bottom Text",
style = MaterialTheme.typography.h2
)
}
两种效果见图:
总结: 输入框键盘的处理包括了:
Insets库里还提供了键盘随着滚动消失和出现的例子. 感兴趣可以看下.
accompanist insets库帮我们做了两部分内容:
CompositionLocalProvider
提供.但是这个库用起来也有一些需要注意的地方, 比如:
WindowCompat.setDecorFitsSystemWindows(window, false)
, 得到的值都是0.consumeWindowInsets
这个值默认是true
, 建议设置为false, 方便内层的ui继续用这些inset的值.@Composable
fun ProvideWindowInsets(
consumeWindowInsets: Boolean = true,
windowInsetsAnimationsEnabled: Boolean = true,
content: @Composable () -> Unit
)
ProvideWindowInsets
, 可能就无法按照预期工作, (不知道是不是暂时性的issue).