关于 WindowInsetsRulers

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

WindowInsetsRulers 的优势

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

WindowInsetsRulers 的缺点

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

使用修饰符方法对齐内容

Modifier.fitInside 允许应用将内容与系统栏和显示屏凹口对齐。它可以替代 WindowInsetsModifier.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 处理带有 IME 的底部元素,请传入一个 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,该标尺采用 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)内可能会导致意外行为,因为滚动容器提供无界高度约束,这与标尺的逻辑不兼容。