本文章发布的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,并很快得到了回复,称这个功能很快就会到来。

issuetracker 202190489

令人蛋疼的是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") }
  )
}