Compose 中的窗口边衬区

Android 平台负责绘制系统界面,如 状态栏和导航栏。无论 用户使用的应用

WindowInsets 提供有关系统的信息 确保应用在正确的区域绘制且界面不会被遮挡的界面 由系统界面提供

在系统栏后面全屏绘制
图 1. 在系统栏后面全屏绘制。

在 Android 14(API 级别 34)及更低版本中,应用界面不会在其底层绘制 系统栏和刘海屏

在 Android 15(API 级别 35)及更高版本中,应用会在系统下绘制 当您的应用以 SDK 35 为目标平台后,通知栏和刘海屏。这样,您可以获得 让您的应用能够充分利用 可用的窗口空间

在系统界面后面显示内容的过程称为全屏显示。在此 页面,您将了解不同类型的边衬区、如何实现无边框 以及如何使用边衬区 API 为界面添加动画效果,并确保应用的内容 不会被系统界面元素遮挡

边衬区基础

当应用全屏显示时,您需要确保 互动不会被系统界面遮挡。例如,如果按钮是 放置在导航栏后面,用户可能无法点击。

指定系统界面的大小以及有关其放置位置的信息 通过边衬区实现。

系统界面的每个部分都有相应的边衬区类型, 以及放置位置例如,状态栏边衬区可提供 而导航栏边衬区则提供 导航栏的尺寸和位置每种类型的边衬区都由 像素尺寸:顶部、左侧、右侧和底部。这些维度指定了 系统界面从应用窗口的相应侧边扩展。为避免 与此类系统界面重叠,因此应用界面必须使用该类型的 金额。

以下内置 Android 边衬区类型可通过 WindowInsets 提供:

WindowInsets.statusBars

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

WindowInsets.statusBarsIgnoringVisibility

状态栏用于设置何时可见。如果状态栏当前处于隐藏状态(由于进入沉浸式全屏模式),则主状态栏边衬区将为空,但这些边衬区将是非空。

WindowInsets.navigationBars

描述导航栏的边衬区。这些是设备左侧、右侧或底部的系统界面栏,用于描述任务栏或导航图标。它们可能会在运行时发生变化,具体取决于用户的首选导航方法以及与任务栏的交互情况。

WindowInsets.navigationBarsIgnoringVisibility

导航栏会带有边衬区(当它们处于可见状态时)。如果导航栏当前处于隐藏状态(由于进入沉浸式全屏模式),则主导航栏边衬区将为空,但这些边衬区将是非空。

WindowInsets.captionBar

描述系统界面窗口装饰(如果是在自由窗口)中的边衬区,例如顶部标题栏。

WindowInsets.captionBarIgnoringVisibility

字幕栏会在其可见的情况下插入。如果字幕栏当前处于隐藏状态,则主字幕栏边衬区将为空,但这些边衬区将是非空的。

WindowInsets.systemBars

系统栏边衬区的集合,包括状态栏、导航栏和标题栏。

WindowInsets.systemBarsIgnoringVisibility

系统栏插入,用于指明何时可见。如果系统栏当前处于隐藏状态(由于进入沉浸式全屏模式),则主系统栏边衬区将为空,但这些边衬区将是非空。

WindowInsets.ime

描述软件键盘底部所占空间的边衬区。

WindowInsets.imeAnimationSource

描述当前键盘动画之前软件键盘所占空间的边衬区。

WindowInsets.imeAnimationTarget

描述当前键盘动画播放完后软件键盘将占据的空间量的边衬区。

WindowInsets.tappableElement

一种边衬区,用于描述有关导航界面的更多详细信息,并提供“点按”空间。将由系统(而非应用)进行处理。对于带有手势导航的透明导航栏,某些应用元素可通过系统导航界面点按。

WindowInsets.tappableElementIgnoringVisibility

可点按的元素在它们可见时插入。如果可点按元素当前处于隐藏状态(由于进入沉浸式全屏模式),则可点按元素的主要边衬区将为空,但这些边衬区将是非空的。

WindowInsets.systemGestures

表示系统将在其中拦截导航手势的边衬区的边衬区。应用可以通过 Modifier.systemGestureExclusion 手动指定处理有限数量的手势。

WindowInsets.mandatorySystemGestures

系统手势的子集,将始终由系统处理,且无法通过 Modifier.systemGestureExclusion 选择停用。

WindowInsets.displayCutout

边衬区表示避免与刘海屏(凹口或针孔)重叠所需的间距量。

WindowInsets.waterfall

表示瀑布显示屏曲线区域的边衬区。瀑布式显示屏具有沿屏幕边缘的曲线区域,该区域开始沿着设备侧边的屏幕包围。

这些类型归纳为三个“安全”类型,这些边衬区类型可确保内容 遮挡:

这些“安全”的边衬区类型以不同的方式保护内容,具体取决于 底层平台边衬区:

边衬区设置

如需允许应用完全控制其绘制内容的位置,请遵循以下设置 步骤。如果不执行这些步骤,您的应用可能会在 系统界面,或者不与软件键盘同步添加动画效果。

  1. 目标 SDK 35 或更高版本,以便在 Android 15 及更高版本上强制执行全屏。您的应用 显示在系统界面后面。您可以通过处理 边衬区。
  2. (可选)在以下位置调用 enableEdgeToEdge()Activity.onCreate(),可让应用在上一个 Android 版本。
  3. 在 activity 的 activity 中设置 android:windowSoftInputMode="adjustResize" AndroidManifest.xml 个条目。此设置允许您的应用接收 作为边衬区使用,您可以使用这些边衬区来填充和设置内容 。

    <!-- in your AndroidManifest.xml file: -->
    <activity
      android:name=".ui.MainActivity"
      android:label="@string/app_name"
      android:windowSoftInputMode="adjustResize"
      android:theme="@style/Theme.MyApplication"
      android:exported="true">
    

Compose API

一旦您的 activity 控制了所有边衬区的处理,您就可以使用 Compose 用于确保内容不被遮挡、可交互元素的 API 与系统界面重叠这些 API 还会将您的应用布局与 边衬区更改。

例如,这是将边衬区应用到内容的最基本方法。 您的整个应用:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    enableEdgeToEdge()

    setContent {
        Box(Modifier.safeDrawingPadding()) {
            // the rest of the app
        }
    }
}

此代码段将 safeDrawing 窗口边衬区作为 应用的全部内容虽然这可确保可交互元素 与系统界面重叠,这也意味着任何应用都不会 来实现全屏效果。为了充分利用 您需要逐个屏幕微调边衬区的应用位置 或逐个组件创建。

所有这些边衬区类型均通过 IME 动画自动添加动画效果 已向后移植到 API 21。总而言之,所有使用这些边衬区的布局都是 也会随着边衬区值的变化而自动添加动画效果。

您可以通过两种主要方式使用这些边衬区类型调整可组合项 布局:内边距修饰符和边衬区大小修饰符。

内边距修饰符

Modifier.windowInsetsPadding(windowInsets: WindowInsets) 会应用 指定的窗口边衬区作为内边距,其行为与 Modifier.padding 一样。 例如,Modifier.windowInsetsPadding(WindowInsets.safeDrawing) 适用于 将安全绘图边衬区全部 4 条边设为内边距

还有几种内置实用程序方法适用于最常见的边衬区类型。 Modifier.safeDrawingPadding() 就是这样一种方法,等效于 Modifier.windowInsetsPadding(WindowInsets.safeDrawing)。还有类似的 修饰符。

边衬区大小修饰符

以下修饰符通过设置 组件设置为边衬区的大小:

Modifier.windowInsetsStartWidth(windowInsets: WindowInsets)

将 windowInsets 的起始侧作为宽度应用(如 Modifier.width

Modifier.windowInsetsEndWidth(windowInsets: WindowInsets)

将 windowInsets 的末端作为宽度应用(例如 Modifier.width

Modifier.windowInsetsTopHeight(windowInsets: WindowInsets)

将 windowInsets 的顶部作为高度应用(如 Modifier.height

Modifier.windowInsetsBottomHeight(windowInsets: WindowInsets)

将 windowInsets 的底部作为高度应用(如 Modifier.height

Spacer 占据 边衬区:

LazyColumn(
    Modifier.imePadding()
) {
    // Other content
    item {
        Spacer(
            Modifier.windowInsetsBottomHeight(
                WindowInsets.systemBars
            )
        )
    }
}

边衬区消耗

边衬区的内边距修饰符(windowInsetsPadding 和诸如此类的辅助函数) safeDrawingPadding)会自动使用 作为内边距应用深入到组合树后,嵌套边衬区 内边距修饰符和边衬区大小修饰符知道 边衬区已被外边边衬区内边距修饰符使用, 多次使用边衬区的同一部分, 额外存储空间

边衬区尺寸修饰符还可以避免多次使用相同边衬区部分 如果边衬区已被使用,则会发生此错误。不过,由于他们 尺寸,它们本身不会使用边衬区。

因此,嵌套内边距修饰符会自动改变 内边距。

看一下前面的 LazyColumn 示例,LazyColumn 通过 imePadding 修饰符调整大小。在 LazyColumn 中,最后一项是 调整为系统栏底部的高度:

LazyColumn(
    Modifier.imePadding()
) {
    // Other content
    item {
        Spacer(
            Modifier.windowInsetsBottomHeight(
                WindowInsets.systemBars
            )
        )
    }
}

当 IME 关闭时,imePadding() 修饰符不会应用任何内边距,因为 IME 没有高度。由于 imePadding() 修饰符未应用任何内边距, 没有使用任何边衬区,并且 Spacer 的高度将是 。

当 IME 打开时,IME 边衬区会进行动画处理以匹配 IME 的大小,并且 imePadding() 修饰符开始应用底部内边距来调整 LazyColumn。当 imePadding() 修饰符开始应用 底部内边距,也会开始消耗相应数量的边衬区。因此, Spacer 的高度开始减少,作为系统间距的一部分 条形已由 imePadding() 修饰符应用。部署 imePadding() 修饰符会应用更大的底部内边距 则 Spacer 的高度为零。

当 IME 关闭时,更改会反过来:Spacer 开始 一旦 imePadding() 应用的视图小于 直到 Spacer 与 在 IME 完全呈现动画后,显示在系统栏的底部。

<ph type="x-smartling-placeholder">
</ph>
图 2.使用 TextField 的边缘延迟列。

此行为是通过 windowInsetsPadding 修饰符,可能会受到一些其他 方法。

Modifier.consumeWindowInsets(insets: WindowInsets) 也会使用边衬区 与 Modifier.windowInsetsPadding 一样,但它并不适用于 将已使用的边衬区作为内边距。与边衬区结合使用时, size 修饰符,用于向同级元素表明特定数量的边衬区 :

Column(Modifier.verticalScroll(rememberScrollState())) {
    Spacer(Modifier.windowInsetsTopHeight(WindowInsets.systemBars))

    Column(
        Modifier.consumeWindowInsets(
            WindowInsets.systemBars.only(WindowInsetsSides.Vertical)
        )
    ) {
        // content
        Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.ime))
    }

    Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.systemBars))
}

Modifier.consumeWindowInsets(paddingValues: PaddingValues) 的行为非常 版本与使用 WindowInsets 参数的版本类似,但前者采用 任意 PaddingValues 使用。这些信息非常有用 如果填充或间距是由 边衬区内边距修饰符,例如普通 Modifier.padding 或固定高度 分隔符:

@OptIn(ExperimentalLayoutApi::class)
Column(Modifier.padding(16.dp).consumeWindowInsets(PaddingValues(16.dp))) {
    // content
    Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.ime))
}

如果无需使用原始窗口边衬区,请使用 WindowInsets 值,或使用 WindowInsets.asPaddingValues() 返回不受消耗影响的边衬区的 PaddingValues。 不过,由于以下注意事项,最好使用窗口边衬区内边距 和窗口边衬区的尺寸修饰符。

边衬区和 Jetpack Compose 阶段

Compose 使用底层 AndroidX 核心 API 来更新边衬区和添加动画效果, 它们使用底层平台 API 管理边衬区。有了这个平台 边衬区与 Jetpack 的各个阶段 写邮件

边衬区的值在组合阶段之后但在 布局阶段。这意味着,读取组合中边衬区的值 通常使用的边衬区值会晚一帧。内置的 修饰符可以延迟使用 边衬区直至布局阶段,这可确保将边衬区值用于 与更新后显示的相同帧。

使用 WindowInsets 的键盘 IME 动画

您可以将 Modifier.imeNestedScroll() 应用于滚动容器,以打开和 在用户滚动到容器底部时自动关闭输入法。

class WindowInsetsExampleActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        WindowCompat.setDecorFitsSystemWindows(window, false)

        setContent {
            MaterialTheme {
                MyScreen()
            }
        }
    }
}

@OptIn(ExperimentalLayoutApi::class)
@Composable
fun MyScreen() {
    Box {
        LazyColumn(
            modifier = Modifier
                .fillMaxSize() // fill the entire window
                .imePadding() // padding for the bottom for the IME
                .imeNestedScroll(), // scroll IME at the bottom
            content = { }
        )
        FloatingActionButton(
            modifier = Modifier
                .align(Alignment.BottomEnd)
                .padding(16.dp) // normal 16dp of padding for FABs
                .navigationBarsPadding() // padding for navigation bar
                .imePadding(), // padding for when IME appears
            onClick = { }
        ) {
            Icon(imageVector = Icons.Filled.Add, contentDescription = "Add")
        }
    }
}

<ph type="x-smartling-placeholder">
</ph> 展示界面元素上下滚动以便为键盘腾出空间的动画
图 3.IME 动画。

对 Material 3 组件的边衬区支持

为了便于使用,许多内置的 Material 3 可组合项 (androidx.compose.material3 个) 根据可组合项在应用中的放置方式自行处理边衬区 根据 Material 规范

边衬区处理可组合项

下面列出了 Material Design 组件 自动处理边衬区

应用栏

内容容器

Scaffold

默认情况下 Scaffold 提供边衬区作为参数 paddingValues,供您使用和使用。 Scaffold 不会将边衬区应用于内容;您的责任。 例如,如需在 Scaffold 中使用 LazyColumn 使用这些边衬区,请使用以下代码:

Scaffold { innerPadding ->
    // innerPadding contains inset information for you to use and apply
    LazyColumn(
        // consume insets as scaffold doesn't do it by default
        modifier = Modifier.consumeWindowInsets(innerPadding),
        contentPadding = innerPadding
    ) {
        items(count = 100) {
            Box(
                Modifier
                    .fillMaxWidth()
                    .height(50.dp)
                    .background(colors[it % colors.size])
            )
        }
    }
}

替换默认边衬区

您可以将传递给可组合项的 windowInsets 形参更改为 配置可组合项的行为。此参数可以是不同类型的 window inset 改为应用,或者通过传递空实例来停用: WindowInsets(0, 0, 0, 0)

例如,要停用 LargeTopAppBar, 将 windowInsets 参数设置为空实例:

LargeTopAppBar(
    windowInsets = WindowInsets(0, 0, 0, 0),
    title = {
        Text("Hi")
    }
)

与 View 系统边衬区互操作

如果屏幕同时包含 View 和 Compose 代码位于同一层次结构。在这种情况下,您需要在 哪个应该使用边衬区,哪个应该忽略边衬区。

例如,如果您最外层的布局是 Android View 布局,您应该 使用 View 系统中的边衬区,并针对 Compose 忽略它们。 或者,如果您最外层的布局是可组合项,那么您应该使用 边衬区,并相应地填充 AndroidView 可组合项。

默认情况下,每个 ComposeView 均使用 能耗水平WindowInsetsCompat。要更改此默认行为,请将 ComposeView.consumeWindowInsets 发送至 false

资源