Jetpack Compose System UI 兼容以及沉浸式状态栏

1,036 阅读6分钟

Android 的页面除了应用本身之外一般还包含系统绘制的部分,包括顶部状态栏和底部导航栏等,默认情况下,状态栏和导航栏是全部交给系统绘制的,系统会按照主题自动设置其背景,剩下的区域才是应用绘制部分。

但这看起来并不好看,很多时候我们希望应用绘制区域可以延伸到 System UI 内,或者动态设置他们的颜色,以此达到更好的用户体验。

很多年前,沉浸式状态栏需要设置一堆 flag,xml 配置等等,但现在时代变了,AndroidX 提供了更便捷的工具,本文就介绍一下在 Jetpack Compose 中应该如何实现沉浸式的效果。

为了实现沉浸式 UI,我们需要两个步骤。

  1. 将应用的绘制区域延伸到 System UI 部分。
  2. 根据需求设置 System UI 的背景色以及页面的边距。

enableEdgeToEdge()

默认情况下,System UI 部分的背景是系统绘制的色块,如下图所示。

为了解决这个问题,需要在 Activity.onCreate 中调用 enableEdgeToEdge() 函数,它会使应用的页面延伸到 System UI 的后面,这样我们才能控制 System UI 的背景区域的绘制,大部分情况下,我们希望是一个纯色块,但有些时候也可能希望它是透明的。

此时,页面会变成如下所示的样子,可以看到应用的绘制区域已经延伸到了 System UI 部分了。

然后,给 Activity 设置 android:windowSoftInputMode="adjustResize" ,这样可以使 Activity 按照标准的 WindowInsets 方式接收到 IME 的大小。

设置 System UI 背景以及边距

按照上面的步骤设置好之后,还需要解决另一个问题,就是页面的边距,因为此时应用的内容会被 System UI 遮挡,我们需要给页面加上相应的边距。

WindowInsets

WindowInsets 用于表示一个 System UI 的位置和大小。

@Stable
interface WindowInsets {
    /**
     * The space, in pixels, at the left of the window that the inset represents.
     */
    fun getLeft(density: Density, layoutDirection: LayoutDirection): Int

    /**
     * The space, in pixels, at the top of the window that the inset represents.
     */
    fun getTop(density: Density): Int

    /**
     * The space, in pixels, at the right of the window that the inset represents.
     */
    fun getRight(density: Density, layoutDirection: LayoutDirection): Int

    /**
     * The space, in pixels, at the bottom of the window that the inset represents.
     */
    fun getBottom(density: Density): Int

    companion object
}

我们通过获取到对应 System UI 的 WindowInsets 对象来设置页面的边距。

WindowInsets 包含多种类型,不同的 System UI 对应不同的 WindowInsets。

// 描述状态栏的边衬区。这些是包含通知图标和其他指示器的顶部系统界面栏。
WindowInsets.statusBars

//状态栏边衬区在可见时使用。如果状态栏当前处于隐藏状态(由于进入沉浸模式),则主状态栏边衬区将为空,但这些边衬区将不为空。
WindowInsets.statusBarsIgnoringVisibility

// 描述导航栏的边衬区。它们是位于设备左侧、右侧或底部的系统界面栏,用于描述任务栏或导航图标。这些控件可能会在运行时根据用户的首选导航方法以及与任务栏的互动发生变化。
WindowInsets.navigationBars

// 导航栏边衬区可见时的边衬区。如果导航栏当前处于隐藏状态(由于进入沉浸模式),则主导航栏边衬区将为空,但这些边衬区为非空。
WindowInsets.navigationBarsIgnoringVisibility

// ... and more ...

应用页面内边距

在上面的例子中,我们的页面目前是会延伸到 System UI 内的,这导致内容被遮挡了一部分。

现在可以通过 WindowInsets 来获取到边距并应用。

val density = LocalDensity.current
val statusBarHeight = WindowInsets.statusBars.getTop(density).pxToDp(density)
val navigatorBarHeight = WindowInsets.navigationBars.getBottom(density).pxToDp(density)

Box(
    modifier = Modifier
        .fillMaxSize()
        .padding(top = statusBarHeight, bottom = navigatorBarHeight),
) 

然后,页面会变成这样:

这样看起来是不是就正常多了。

Composable 中的自适应边距

但是,每个页面都这样设置岂不是很麻烦?好在 Compose 提供了很多好用的工具辅助开发者设置边距。

例如,上面的例子中也可以这样:

Box(
    modifier = Modifier
        .fillMaxSize()
        .statusBarsPadding()
        .navigationBarsPadding(),
)

// or Modifier.systemBarsPadding()
// or Modifier.safeDrawingPadding()

上面的代码最终都是通过添加 padding 来防止内容被遮挡。

但还是有点麻烦,难道每个页面都要这么写吗?当然不是,Compose 的 Scaffold 也会帮助我们解决这个问题。

在使用 MaterialDesign3 的 Scaffold 时,正确的使用 innerPadding 可以帮助我们自动添加页面边距。

Scaffold { innerPadding ->
    Box(
        modifier = Modifier
            .fillMaxSize()
            .padding(innerPadding),
    ) {
        // ...
    }
}

这里的 innerPadding 已经包括了状态栏和导航栏的边距,直接应用即可。

另外,Scaffold 的这个行为是可以改变的,如果我们的页面并不需要添加边距(比如图片查看器,视频播放器),最好的方式并不是不使用 ScaffoldinnerPadding,而是通过参数控制。

@Composable
fun Scaffold(
    modifier: Modifier = Modifier,
    topBar: @Composable () -> Unit = {},
    bottomBar: @Composable () -> Unit = {},
    snackbarHost: @Composable () -> Unit = {},
    floatingActionButton: @Composable () -> Unit = {},
    floatingActionButtonPosition: FabPosition = FabPosition.End,
    containerColor: Color = MaterialTheme.colorScheme.background,
    contentColor: Color = contentColorFor(containerColor),
    **contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets,**
    content: @Composable (PaddingValues) -> Unit
)

可以看到,Scaffold 入参包含一个 contentWindowInsets 参数,这个参数默认是会加上 System UI 的 WindowInsets,如果希望改变这个行为,我们设置一个空的 WindowInsets 即可。

TopAppBar

在使用 Scaffold 时,自适用的边距是通过应用 innerPadding 来做到的,但是 ScaffoldtopBar 并没有 innerPadding,我们仍然不需要手动设置其 padding,原因在于,作为 TopAppBar 这种特殊的组件,其内部也会自动设置边距。

@Composable
fun TopAppBar(
    title: @Composable () -> Unit,
    modifier: Modifier = Modifier,
    navigationIcon: @Composable () -> Unit = {},
    actions: @Composable RowScope.() -> Unit = {},
    **windowInsets: WindowInsets = TopAppBarDefaults.windowInsets,**
    colors: TopAppBarColors = TopAppBarDefaults.topAppBarColors(),
    scrollBehavior: TopAppBarScrollBehavior? = null
)

上面的入参中的 windowInsets 的作用就是用来设置顶部的 padding。我们也可以通过参数来改变这一行为。

NavigationBar

作为页面底部的 NavigationBar 同样支持自适用页面内边距。

fun NavigationBar(
    modifier: Modifier = Modifier,
    containerColor: Color = NavigationBarDefaults.containerColor,
    contentColor: Color = MaterialTheme.colorScheme.contentColorFor(containerColor),
    tonalElevation: Dp = NavigationBarDefaults.Elevation,
    **windowInsets: WindowInsets = NavigationBarDefaults.windowInsets,**
    content: @Composable RowScope.() -> Unit
)

StatusBar 和 NavigationBar 背景色

其实经过上面几个步骤的设置,背景色已经很好设置了,因为这都是页面内容的一部分,毕竟毕竟也是你写的代码,无论怎么控制都行。

对于 Compose 来说,这也会自动设置,如果我们的页面使用了 Scaffold,那么Scaffold 本身是有背景色的,背景色就是 MaterialDesign 中的 backgroundColor

@Composable
fun Scaffold(
    modifier: Modifier = Modifier,
    topBar: @Composable () -> Unit = {},
    bottomBar: @Composable () -> Unit = {},
    snackbarHost: @Composable () -> Unit = {},
    floatingActionButton: @Composable () -> Unit = {},
    floatingActionButtonPosition: FabPosition = FabPosition.End,
    **containerColor: Color = MaterialTheme.colorScheme.background**,
    contentColor: Color = contentColorFor(containerColor),
    contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets,
    content: @Composable (PaddingValues) -> Unit
)

上面代码中的 containerColor 就是页面的背景色,又因为 Scaffold 默认会给顶部和底部加上 padding,所以 StatusBar/NavigationBar 的背景色自然就是页面的背景色了。

如果你用了 TopAppBar 的话,它也会自动设置颜色,并且会根据滑动状态有变化效果,所以如果页面有顶部的 bar,那么最好还是用官方的,因为确实会漂亮很多,而且也帮我们省了不少事。

总结

总之,按照最新的规范来设置沉浸式状态栏会非常方便,并且 Compose 中的很多组件都帮我们省去了很多工作,就算遇到一些特殊情况也有简洁的应对办法。

另外也欢迎大家关注我,我会持续输出一些不同类型的原创内容。

参考文档:

developer.android.com/develop/ui/…

developer.android.google.cn/develop/ui/…