关于 WindowInsetsRulers

WindowInsets 是 Jetpack Compose 中的标准 API,用于处理部分或完全被系统界面遮挡的屏幕区域。这些区域包括状态栏、导航栏和屏幕键盘。您也可以传递预定义的 WindowInsetsRulers(例如 SafeDrawing)给 Modifier.fitInsideModifier.fitOutside,以使内容与系统栏和刘海屏对齐,或者创建自定义 WindowInsetsRulers

WindowInsetsRulers 的优势

  • 避免了消费复杂性:它在布局的放置阶段运行。这意味着,它完全绕过了插边消耗链,并且无论父布局做了什么,都可以始终提供系统栏和显示屏刘海的正确绝对位置。当祖先可组合项错误地使用边衬区时,使用 Modifier.fitInsideModifier.fitOutside 方法有助于解决问题。
  • 轻松避开系统栏:有助于应用内容避开系统栏和刘海屏,并且比直接使用 WindowInsets 更简单。
  • 高度可自定义:开发者可以将内容与自定义标尺对齐,并通过自定义布局精确控制布局。

WindowInsetsRulers 的缺点

  • 无法用于衡量:由于它在展示位置阶段运行,因此在之前的衡量阶段,它提供的位置信息是不可用的。

使用修饰符方法调整内容

Modifier.fitInside 允许应用将内容与系统栏和显示屏刘海对齐。它可以代替 WindowInsets 使用。Modifier.fitOutside 通常是 Modifier.fitInside 的逆函数。

例如,如需验证应用内容是否避开了系统栏和刘海屏,您可以使用 fitInside(WindowInsetsRulers.safeDrawing.current)

@Composable
fun FitInsideDemo(modifier: Modifier) {
    Box(
        modifier = modifier
            .fillMaxSize()
            // Or DisplayCutout, Ime, NavigationBars, StatusBar, etc...
            .fitInside(WindowInsetsRulers.SafeDrawing.current)
    )
}

下表显示了使用 Modifier.fitInsideModifier.fitOutside 的预定义标尺时,应用内容的外观。

预定义标尺类型

Modifier.fitInside

Modifier.fitOutside

DisplayCutout

Ime

不适用

NavigationBars

SafeDrawing

不适用(请改用 StatusBarCaptionBarNavigationBar

StatusBar

使用 Modifier.fitInsideModifier.fitOutside 需要对可组合项进行限制。这意味着您必须定义 Modifier.sizeModifier.fillMaxSize 等修饰符。

某些指标(例如 SafeDrawingSystemBars 上的 Modifier.fitOutside)会返回多个指标。在这种情况下,Android 会使用左侧、顶部、右侧、底部的一把标尺放置可组合项。

避免使用带有 Modifier.fitInside 的 IME

如需使用 Modifier.fitInside 处理底部元素,请传入一个 RectRuler,该 RectRuler 接受 NavigationBarIme 的最内层值。

@Composable
fun FitInsideWithImeDemo(modifier: Modifier) {
    Box(
        modifier = modifier
            .fillMaxSize()
            .fitInside(
                RectRulers.innermostOf(
                    WindowInsetsRulers.NavigationBars.current,
                    WindowInsetsRulers.Ime.current
                )
            )
    ) {
        TextField(
            value = "Demo IME Insets",
            onValueChange = {},
            modifier = modifier.align(Alignment.BottomStart).fillMaxWidth()
        )
    }
}

使用 Modifier.fitInside 避免状态栏和字幕栏

同样,如需验证顶部元素是否避开了状态栏和标题栏以及 Modifier.fitInsider,请传递一个 RectRuler,该 RectRuler 会获取 StatusBarsCaptionBar 的最内层值。

@Composable
fun FitInsideWithStatusAndCaptionBarDemo(modifier: Modifier) {
    Box(
        modifier = modifier
            .fillMaxSize()
            .fitInside(
                RectRulers.innermostOf(
                    WindowInsetsRulers.StatusBars.current,
                    WindowInsetsRulers.CaptionBar.current
                )
            )
    )
}

创建自定义 WindowInsetsRulers

您可以将内容与自定义标尺对齐。例如,假设某个父可组合项错误地处理了边衬区,导致下游子项出现内边距问题。虽然此问题可以通过其他方式解决,包括使用 Modifier.fitInside,但您也可以创建自定义标尺来精确对齐子可组合项,而无需在上游父级中修复此问题,如以下示例和视频所示:

@Composable
fun WindowInsetsRulersDemo(modifier: Modifier) {
    Box(
        contentAlignment = BottomCenter,
        modifier = modifier
            .fillMaxSize()
            // The mistake that causes issues downstream, as .padding doesn't consume insets.
            // While it's correct to instead use .windowInsetsPadding(WindowInsets.navigationBars),
            // assume it's difficult to identify this issue to see how WindowInsetsRulers can help.
            .padding(WindowInsets.navigationBars.asPaddingValues())
    ) {
        TextField(
            value = "Demo IME Insets",
            onValueChange = {},
            modifier = modifier
                // Use alignToSafeDrawing() instead of .imePadding() to precisely place this child
                // Composable without having to fix the parent upstream.
                .alignToSafeDrawing()

            // .imePadding()
            // .fillMaxWidth()
        )
    }
}

fun Modifier.alignToSafeDrawing(): Modifier {
    return layout { measurable, constraints ->
        if (constraints.hasBoundedWidth && constraints.hasBoundedHeight) {
            val placeable = measurable.measure(constraints)
            val width = placeable.width
            val height = placeable.height
            layout(width, height) {
                val bottom = WindowInsetsRulers.SafeDrawing.current.bottom
                    .current(0f).roundToInt() - height
                val right = WindowInsetsRulers.SafeDrawing.current.right
                    .current(0f).roundToInt()
                val left = WindowInsetsRulers.SafeDrawing.current.left
                    .current(0f).roundToInt()
                measurable.measure(Constraints.fixed(right - left, height))
                    .place(left, bottom)
            }
        } else {
            val placeable = measurable.measure(constraints)
            layout(placeable.width, placeable.height) {
                placeable.place(0, 0)
            }
        }
    }
}

以下视频展示了一个示例,其中左侧的图片显示了由上游父级导致的 IME 边衬区使用问题,右侧显示了如何使用自定义标尺来解决此问题。TextField 可组合项下方显示了额外的内边距,因为导航栏内边距未被父级使用。如上一个代码示例所示,使用自定义标尺将子元素放置在右侧图片中的正确位置。

验证家长是否受到限制

为了安全使用 WindowInsetsRulers,请确保父级提供有效的约束条件。父级必须具有已定义的大小,并且不能依赖于使用 WindowInsetsRulers 的子级的大小。在父可组合项上使用 fillMaxSize 或其他尺寸修饰符。

同样,将使用 WindowInsetsRulers 的可组合项放置在 verticalScroll 等滚动容器内可能会导致意外行为,因为滚动容器提供的是无界的高度限制,这与标尺的逻辑不兼容。