本文章发布的20天后,Jetpack Compose 发布了 material3:1.1.0-alpha01,支持标题居中了。
Material Design 的标题栏是真的有毒,开发Android好几年了,基本上所有App都是自定义标题栏,不会真的有人使用Material的原生Toolbar吧,灵活性也太低了,想标题居中都困难。对比起 Flutter Material 的 AppBar 真的有很大的差距,后者的 AppBar 非常好用,可以高度的自定义。
即将到来的 compose.material3
不过在今年7月份,Android 发布了 material-1.4.0 版本,终于支持标题居中了,多少年了,可歌可泣,我们应该是不需要再使用自定义的标题栏了,毕竟不方便主题配置。
但是 Jetpack Compose 的更新并没有跟上,标题栏依然无法居中,我已经给他们提交了一个 issue: TopAppBar centering title,并很快得到了回复,称这个功能很快就会到来。
令人蛋疼的是4楼,这位工程师好像没有完全理解我们的需求。
至于3楼说的 compose.material3,具体发布日期是什么时候我并不清楚,issue在我做出回复前已经被关闭了,不好再次询问。
不过确实已经看到这个功能在计划当中了,并且看起来工程规模挺大的。2021/10/06: Adds a Material3 SmallCenteredTopAppBar support.
临时性的 Workround
在 compose.material3 到来之前,我们可以简单的自定义一个CenterTopAppBar,等它发布后再替换掉其中的实现即可。
标题居中的实现方式参照这个问答:How to align title at layout center in TopAppBar?
我比较喜欢Flutter中Appbar的使用方式,所以入参也参照Flutter Appbar。
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
private val AppBarHeight = 56.dp
private val AppBarHorizontalPadding = 4.dp
private val ContentPadding = PaddingValues(
start = AppBarHorizontalPadding,
end = AppBarHorizontalPadding
)
@Composable
fun CenterTopAppBar(
modifier: Modifier = Modifier,
backgroundColor: Color = MaterialTheme.colors.primarySurface,
contentColor: Color = contentColorFor(backgroundColor),
elevation: Dp = AppBarDefaults.TopAppBarElevation,
contentPadding: PaddingValues = ContentPadding,
navController: NavController? = null,
leading: @Composable (RowScope.() -> Unit)? = null,
trailing: @Composable (RowScope.() -> Unit)? = null,
title: @Composable () -> Unit,
) {
Surface(
color = backgroundColor,
contentColor = contentColor,
elevation = elevation,
modifier = modifier
) {
var leftSectionWidth = 0.dp
var rightSectionWidth = 0.dp
var titlePadding by remember { mutableStateOf(PaddingValues()) }
val calculateTitlePadding = fun() {
val dx = leftSectionWidth - rightSectionWidth
var start = 0.dp
var end = 0.dp
if (dx < 0.dp) start += dx else end += dx
titlePadding = PaddingValues(start = start, end = end)
}
Row(
Modifier
.fillMaxWidth()
.padding(contentPadding)
.height(AppBarHeight),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically
) {
// leading
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
with(LocalDensity.current) {
Row(
Modifier
.fillMaxHeight()
.onGloballyPositioned { coordinates ->
val width = coordinates.size.width.toDp()
if (width != leftSectionWidth) {
leftSectionWidth = width
calculateTitlePadding()
}
},
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically,
content = leading ?: {
val previous = navController?.previousBackStackEntry
if (previous != null) {
IconButton(onClick = { navController.popBackStack() }) {
Icon(Icons.Filled.ArrowBack, null)
}
}
}
)
}
}
// title
Row(
Modifier
.fillMaxHeight()
.weight(1f)
.padding(titlePadding),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
ProvideTextStyle(value = MaterialTheme.typography.h6) {
CompositionLocalProvider(
LocalContentAlpha provides ContentAlpha.high,
content = title
)
}
}
// trailing
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
with(LocalDensity.current) {
Row(
Modifier
.fillMaxHeight()
.onGloballyPositioned { coordinates ->
val width = coordinates.size.width.toDp()
if (width != rightSectionWidth) {
rightSectionWidth = width
calculateTitlePadding()
}
},
horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically,
content = trailing ?: {}
)
}
}
}
}
}
// 因为标题是在布局位置确定后再次调整的,所以预览时看到标题不是居中的,实际运行App后才能
// 看到居中效果。
@Preview
@Composable
private fun DefaultPreview() {
CenterTopAppBar(
leading = {
IconButton(onClick = { }) {
Icon(Icons.Filled.ArrowBack, null)
}
}
title = { Text("TitleBar Center") }
)
}