Jetpack Compose TopAppBar 标题居中

Material Design 的标题栏是真的有毒,开发Android好几年了,基本上所有App都是自定义标题栏,不会真的有人使用Material的原生Toolbar吧,灵活性也太低了,想标题居中都困难。对比起 Flutter Material 的 AppBar 真的有很大的差距,后者的 AppBar 非常好用,可以高度的自定义。

即将到来的 compose.material3

不过在今年7月份,Android 发布了 material-1.4.0 版本,终于支持标题居中了,多少年了,可歌可泣,我们应该是不需要再使用自定义的标题栏了,毕竟不方便主题配置。

但是 Jetpack Compose 的更新并没有跟上,标题栏依然无法居中,我已经给他们提交了一个 issue: TopAppBar centering title,并很快得到了回复,称这个功能很快就会到来。

issuetracker

令人蛋疼的是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。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
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") }
)
}