Android 平台负责绘制系统 UI,例如状态栏和导航栏。无论用户使用哪个应用,此系统 UI 都会显示。
WindowInsets
提供有关系统 UI 的信息,以确保您的应用在正确区域绘制,并且您的 UI 不会被系统 UI 遮挡。
在 Android 14(API 级别 34)及更低版本中,应用的 UI 默认情况下不会在系统栏和显示切口下方绘制。
在 Android 15(API 级别 35)及更高版本中,当您的应用面向 SDK 35 时,您的应用会在系统栏和显示切口下方绘制。这将带来更流畅的用户体验,并允许您的应用充分利用可用的窗口空间。
在系统 UI 后面显示内容称为边缘到边缘。在本页中,您将了解不同类型的内边距、如何边缘到边缘,以及如何使用内边距 API 来为 UI 设置动画并确保您的应用内容不会被系统 UI 元素遮挡。
内边距基础知识
当应用边缘到边缘时,您需要确保重要的内容和交互不会被系统 UI 遮挡。例如,如果按钮放置在导航栏后面,用户可能无法点击它。
系统 UI 的大小以及其放置位置的信息通过内边距指定。
系统 UI 的每个部分都有一个对应的内边距类型,该类型描述其大小及其放置位置。例如,状态栏内边距提供状态栏的大小和位置,而导航栏内边距提供导航栏的大小和位置。每种类型的内边距都包含四个像素尺寸:顶部、左侧、右侧和底部。这些尺寸指定系统 UI 从应用窗口的相应边延伸的距离。因此,为了避免与该类型的系统 UI 重叠,应用 UI 必须缩进该距离。
这些内置的 Android 内边距类型可通过 WindowInsets
获取
描述状态栏的内边距。这些是包含通知图标和其他指示器的顶部系统 UI 栏。 |
|
状态栏内边距,用于它们可见时。如果状态栏当前隐藏(由于进入沉浸式全屏模式),则主状态栏内边距将为空,但这些内边距将不为空。 |
|
描述导航栏的内边距。这些是位于设备左侧、右侧或底部的系统 UI 栏,描述任务栏或导航图标。这些可以在运行时根据用户的首选导航方法和与任务栏的交互而改变。 |
|
导航栏内边距,用于它们可见时。如果导航栏当前隐藏(由于进入沉浸式全屏模式),则主导航栏内边距将为空,但这些内边距将不为空。 |
|
描述系统 UI 窗口装饰的内边距,如果它处于自由窗体窗口中,例如顶部的标题栏。 |
|
标题栏内边距,用于它们可见时。如果标题栏当前隐藏,则主标题栏内边距将为空,但这些内边距将不为空。 |
|
系统栏内边距的并集,包括状态栏、导航栏和标题栏。 |
|
系统栏内边距,用于它们可见时。如果系统栏当前隐藏(由于进入沉浸式全屏模式),则主系统栏内边距将为空,但这些内边距将不为空。 |
|
描述软件键盘占据的底部空间量的内边距。 |
|
描述软件键盘在当前键盘动画之前占据的空间量的内边距。 |
|
描述软件键盘在当前键盘动画之后将占据的空间量的内边距。 |
|
一种内边距类型,描述有关导航 UI 的更详细信息,提供系统处理“点击”而不是应用的空间量。对于具有手势导航的透明导航栏,某些应用元素可以通过系统导航 UI 点击。 |
|
可点击元素内边距,用于它们可见时。如果可点击元素当前隐藏(由于进入沉浸式全屏模式),则主可点击元素内边距将为空,但这些内边距将不为空。 |
|
内边距表示系统将拦截手势以进行导航的空间量。应用可以通过 |
|
系统手势的一个子集,始终由系统处理,并且无法通过 |
|
表示避免与显示切口(凹口或针孔)重叠所需的空间量的内边距。 |
|
表示瀑布显示的弯曲区域的内边距。瀑布显示在屏幕边缘具有弯曲区域,屏幕开始沿着设备侧面包裹。 |
这些类型由三种“安全”内边距类型汇总,以确保内容不会被遮挡
这些“安全”内边距类型基于底层平台内边距以不同的方式保护内容
- 使用
WindowInsets.safeDrawing
来保护不应该在任何系统 UI 下面绘制的内容。这是内边距最常见的用法:防止绘制被系统 UI(部分或全部)遮挡的内容。 - 使用
WindowInsets.safeGestures
来保护具有手势的内容。这将避免系统手势与应用手势(例如底部工作表、轮播或游戏中的手势)发生冲突。 - 使用
WindowInsets.safeContent
作为WindowInsets.safeDrawing
和WindowInsets.safeGestures
的组合,以确保内容没有视觉重叠和手势重叠。
内边距设置
要允许您的应用完全控制其绘制内容的位置,请按照以下设置步骤操作。如果没有这些步骤,您的应用可能会在系统 UI 后面绘制黑色或纯色,或者不会与软件键盘同步动画。
- 面向 SDK 35 或更高版本以 强制执行边缘到边缘 在 Android 15 及更高版本上。您的应用将在系统 UI 后面显示。您可以通过处理内边距来调整应用的 UI。
- 可选地,在
Activity.onCreate()
中调用enableEdgeToEdge()
,这将允许您的应用在以前的 Android 版本上边缘到边缘。 在 Activity 的
AndroidManifest.xml
条目中设置android:windowSoftInputMode="adjustResize"
。此设置允许您的应用接收软件 IME 的大小作为内边距,您可以在 IME 在应用中显示和消失时使用它来适当地填充和布局内容。<!-- 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 来确保内容不会被遮挡,并且可交互元素不会与系统 UI 重叠。这些 API 还将您的应用布局与内边距更改同步。
例如,这是将内边距应用于整个应用内容的最基本方法
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { Box(Modifier.safeDrawingPadding()) { // the rest of the app } } }
此代码段将 safeDrawing
窗口内边距作为应用整个内容周围的填充。虽然这确保了可交互元素不会与系统 UI 重叠,但也意味着应用的任何部分都不会在系统 UI 后面绘制以实现边缘到边缘效果。为了充分利用整个窗口,您需要在逐屏或逐组件的基础上微调内边距的应用位置。
所有这些内边距类型都将与 IME 动画自动同步,并将这些动画移植到 API 21。因此,所有使用这些内边距的布局也会在内边距值更改时自动动画。
主要有两种方法可以使用这些内边距类型来调整您的 Composable 布局:填充修饰符和内边距大小修饰符。
填充修饰符
Modifier.windowInsetsPadding(windowInsets: WindowInsets)
将给定的窗口内边距作为填充应用,就像 Modifier.padding
一样。例如,Modifier.windowInsetsPadding(WindowInsets.safeDrawing)
将安全绘制内边距作为所有 4 面的填充应用。
还有一些针对最常见的内边距类型的内置实用程序方法。 Modifier.safeDrawingPadding()
就是其中一种方法,等同于 Modifier.windowInsetsPadding(WindowInsets.safeDrawing)
。其他内边距类型也有类似的修饰符。
内边距大小修饰符
以下修饰符通过将组件的大小设置为内边距的大小来应用窗口内边距的大小
将 windowInsets 的起始侧作为宽度应用(就像 |
|
将 windowInsets 的结束侧作为宽度应用(就像 |
|
将 windowInsets 的顶部作为高度应用(就像 |
|
|
将窗口内边距的底部应用为高度(类似于 |
这些修饰符对于调整 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
应用底部填充,以便在 IME 打开时调整 LazyColumn
的大小。当 imePadding()
修饰符开始应用底部填充时,它还会开始占用该数量的内边距。因此,Spacer
的高度开始减小,因为系统栏的一部分空间已经被 imePadding()
修饰符应用。一旦 imePadding()
修饰符应用的底部填充量大于系统栏,Spacer
的高度为零。
当 IME 关闭时,更改以相反的方式发生:Spacer
开始从高度为零扩展,一旦 imePadding()
应用的填充量小于系统栏底部,直到最终 Spacer
与系统栏底部的的高度匹配,一旦 IME 完全动画消失。
这种行为是通过所有 windowInsetsPadding
修饰符之间的通信实现的,可以通过其他几种方式影响。
Modifier.consumeWindowInsets(insets: WindowInsets)
也会像 Modifier.windowInsetsPadding
一样占用内边距,但它不会将占用的内边距应用为填充。这在与内边距大小修饰符结合使用时很有用,可以告诉兄弟组件已经占用了多少内边距。
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 使用管理内边距的底层平台 API。由于这种平台行为,内边距与 Jetpack Compose 阶段 有特殊的关系。
内边距的值在组合阶段之后但布局阶段之前更新。这意味着在组合中读取内边距的值通常使用比实际值晚一帧的内边距值。此页面上描述的内置修饰符旨在延迟使用内边距的值,直到布局阶段,这确保内边距值在与更新它们的同一帧上使用。
使用 WindowInsets
的键盘 IME 动画
可以将 Modifier.imeNestedScroll()
应用于滚动容器,以便在滚动到容器底部时自动打开和关闭 IME。
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") } } }
Material 3 组件的内边距支持
为了便于使用,许多内置的 Material 3 可组合项目(androidx.compose.material3
)根据 Material 规范中可组合项目在应用程序中的放置方式自行处理内边距。
处理内边距的可组合项目
以下是自动处理内边距的 Material 组件 的列表。
应用栏
TopAppBar
/SmallTopAppBar
/CenterAlignedTopAppBar
/MediumTopAppBar
/LargeTopAppBar
:将系统栏的顶部和水平边应用为填充,因为它用在窗口的顶部。BottomAppBar
:将系统栏的底部和水平边应用为填充。
内容容器
ModalDrawerSheet
/DismissibleDrawerSheet
/PermanentDrawerSheet
(模态导航抽屉内部的内容):将垂直和开始内边距应用于内容。ModalBottomSheet
:将底部内边距应用。NavigationBar
:将底部和水平内边距应用。NavigationRail
:将垂直和开始内边距应用。
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
参数,以配置可组合项目的行为。此参数可以是另一种类型的窗口内边距,用于替代应用,也可以通过传递一个空实例来禁用:WindowInsets(0, 0, 0, 0)
。
例如,要禁用 LargeTopAppBar
上的内边距处理,请将 windowInsets
参数设置为一个空实例
LargeTopAppBar( windowInsets = WindowInsets(0, 0, 0, 0), title = { Text("Hi") } )
与 View 系统内边距的互操作性
当您的屏幕在同一层级结构中同时包含 View 和 Compose 代码时,您可能需要覆盖默认内边距。在这种情况下,您需要明确指定哪个组件应该占用内边距,哪个组件应该忽略内边距。
例如,如果您的最外层布局是 Android View 布局,您应该在 View 系统中占用内边距,并为 Compose 忽略内边距。或者,如果您的最外层布局是一个可组合项,您应该在 Compose 中占用内边距,并相应地填充 AndroidView
可组合项。
默认情况下,每个 ComposeView
在 WindowInsetsCompat
的占用级别上占用所有内边距。要更改此默认行为,请将 ComposeView.consumeWindowInsets
设置为 false
。
资源
- Now in Android — 一个完全使用 Kotlin 和 Jetpack Compose 构建的完整功能的 Android 应用程序。
- 处理 Android 15 中的边缘到边缘强制 — 一个代码实验室,引导您完成 Android 15 边缘到边缘强制的步骤。
- 改善 Android 应用程序体验的三件事:边缘到边缘、预测后退和 Glance — 一个 YouTube 视频,讲述了 Android 15 边缘到边缘强制。
- 边缘到边缘和内边距 | Compose 提示 — 一个 YouTube 视频,展示了如何处理内边距以绘制边缘到边缘。
为您推荐
- 注意:当 JavaScript 关闭时,链接文本会显示。
- Material 组件和布局
- 将
CoordinatorLayout
迁移到 Compose - 其他注意事项