Android 平台负责绘制系统界面,如状态栏和导航栏。无论用户使用哪个应用,系统都会显示此系统界面。WindowInsets
提供有关系统界面的信息,以确保您的应用在正确的区域绘制,并且界面不会被系统界面遮挡。
默认情况下,应用的界面只能在系统界面中布局,例如状态栏和导航栏。这样可确保应用的内容不会被系统界面元素遮挡。
不过,我们建议应用选择在这些同样显示系统界面的区域展示广告,这样可以带来更顺畅的用户体验,并让应用能够充分利用可用的窗口空间。这还允许应用与系统界面一起添加动画效果,尤其是在显示和隐藏软件键盘时。
选择在这些区域显示内容并在系统界面后面显示内容称为“无边框”。在本页中,您将了解不同类型的边衬区、如何选择启用无边框效果,以及如何使用边衬区 API 为界面添加动画效果并避免遮挡应用的某些部分。
边衬区基础设置
当应用进入全屏时,您需要确保重要的内容和互动不会被系统界面遮挡。例如,如果某个按钮位于导航栏后面,用户可能无法点击该按钮。
系统界面的大小及其放置位置的相关信息通过边衬区指定。
系统界面的每个部分都有相应的边衬区类型,用于说明其大小和放置位置。例如,状态栏边衬区提供状态栏的大小和位置,而导航栏边衬区提供导航栏的大小和位置。每种边衬区都包含四个像素尺寸:顶部、左侧、右侧和底部。这些尺寸指定系统界面从应用窗口的相应侧边延伸多远。因此,为了避免与此类系统界面重叠,应用界面必须沿着该距离添加边衬区。
这些内置 Android 边衬区类型可通过 WindowInsets
获取:
描述状态栏的边衬区。这些是包含通知图标和其他指示器的顶部系统界面栏。 |
|
状态栏边衬区在可见时使用。如果状态栏当前处于隐藏状态(由于进入沉浸模式),则主状态栏边衬区将为空,但这些边衬区将不为空。 |
|
描述导航栏的边衬区。它们是位于设备左侧、右侧或底部的系统界面栏,用于描述任务栏或导航图标。这些控件可能会在运行时根据用户的首选导航方法以及与任务栏的互动发生变化。 |
|
导航栏边衬区可见时的边衬区。如果导航栏当前处于隐藏状态(由于进入沉浸模式),则主导航栏边衬区将为空,但这些边衬区为非空。 |
|
说明系统界面窗口装饰(如果位于自由窗口窗口(例如顶部标题栏)中的边衬区)。 |
|
字幕栏边衬区在可见时使用。如果字幕栏当前处于隐藏状态,则主字幕栏边衬区将为空,但这些边衬区将不为空。 |
|
系统栏边衬区的集合,包括状态栏、导航栏和字幕栏。 |
|
系统栏边衬区可见时的边衬区。如果系统栏当前处于隐藏状态(由于进入沉浸模式),则主系统栏边衬区将为空,但这些边衬区将不为空。 |
|
边衬区,用于说明软件键盘在底部所占空间的大小。 |
|
用于描述软件键盘在当前键盘动画之前所占空间的边衬区。 |
|
边衬区,用于说明软件键盘在当前键盘动画之后将占用的空间。 |
|
一种边衬区,用于描述有关导航界面的更多详细信息,提供了由系统(而非应用)处理“点按”的空间量。对于带有手势导航的透明导航栏,某些应用元素可通过系统导航界面点按。 |
|
可点按的元素边衬区(当它们可见时)。如果可点按元素当前处于隐藏状态(由于进入沉浸模式),则主要的可点按元素边衬区将为空,但这些边衬区将是非空的。 |
|
这些边衬区,用于表示系统将从此处截获用于导航的手势的边衬区数量。应用可以通过 |
|
部分系统手势,将始终由系统处理,且无法通过 |
|
这些边衬区表示所需的间距量,以免与刘海屏(凹口或针孔)重叠。 |
|
表示瀑布显示的曲线区域的边衬区。在瀑布屏中,屏幕边缘有弧形区域,此时屏幕开始沿着设备侧边进行环绕。 |
这些类型由三种“安全”边衬区类型汇总,可确保内容不被遮挡:
这些“安全”边衬区类型会根据底层平台边衬区以不同的方式保护内容:
- 使用
WindowInsets.safeDrawing
可保护不应在任何系统界面下方绘制的内容。这是边衬区的最常见用法:防止绘制被系统界面(部分或全部)遮挡的内容。 - 使用
WindowInsets.safeGestures
可通过手势保护内容。这样可以避免系统手势与应用手势(例如底部动作条、轮播界面或游戏中的手势)冲突。 - 使用
WindowInsets.safeContent
作为WindowInsets.safeDrawing
和WindowInsets.safeGestures
的组合,以确保内容没有视觉重叠和手势重叠。
边衬区设置
如需让应用完全控制其绘制内容的位置,请按以下设置步骤操作。如果不执行这些步骤,您的应用可能会在系统界面后方绘制黑色或纯色,或者无法与软件键盘同步动画效果。
- 在
Activity.onCreate
中调用enableEdgeToEdge()
。此调用会请求您的应用显示在系统界面后面。然后,您的应用将控制如何使用这些边衬区来调整界面。 在您的 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 来确保内容不会被遮挡,并且可交互元素不会与系统界面重叠。这些 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)
。其他边衬区类型有类似的修饰符。
边衬区尺寸修饰符
以下修饰符会将组件的尺寸设置为边衬区的大小,从而应用一定数量的窗口边衬区:
将 windowInsets 的起始侧用作宽度(例如 |
|
将 windowInsets 的末端用作宽度(例如 |
|
将 windowInsets 的顶部用作高度(例如 |
|
|
将 windowInsets 的底部用作高度(例如 |
这些修饰符特别适用于调整占据边衬区空间的 Spacer
的大小:
LazyColumn( Modifier.imePadding() ) { // Other content item { Spacer( Modifier.windowInsetsBottomHeight( WindowInsets.systemBars ) ) } }
边衬区用量
边衬区内边距修饰符(windowInsetsPadding
以及 safeDrawingPadding
等辅助程序)会自动使用作为内边距应用的边衬区部分。在深入了解组合树时,嵌套的边衬区内边距修饰符和边衬区尺寸修饰符会知道部分边衬区已被外部边衬区内边距修饰符使用,因此应避免多次使用相同部分边衬区,因为这会导致过多的额外空间。
如果边衬区已被使用,那么边衬区大小修饰符还会避免多次使用边衬区的同一部分。不过,由于它们会直接更改其大小,因此它们本身不会使用边衬区。
因此,嵌套内边距修饰符会自动更改应用于每个可组合项的内边距大小。
还是以之前一样的 LazyColumn
示例为例,imePadding
修饰符会调整 LazyColumn
的大小。在 LazyColumn
内,最后一项的大小调整为系统栏底部的高度:
LazyColumn( Modifier.imePadding() ) { // Other content item { Spacer( Modifier.windowInsetsBottomHeight( WindowInsets.systemBars ) ) } }
当 IME 关闭时,imePadding()
修饰符不会应用任何内边距,因为 IME 没有高度。由于 imePadding()
修饰符未应用任何内边距,因此不会使用任何边衬区,并且 Spacer
的高度将为系统栏底部的大小。
当 IME 打开时,IME 边衬区会以动画方式显示以匹配 IME 的大小,并且 imePadding()
修饰符会在 IME 打开时开始应用底部内边距以调整 LazyColumn
的大小。当 imePadding()
修饰符开始应用底部内边距时,它也会开始使用相应数量的边衬区。因此,Spacer
的高度开始减小,因为 imePadding()
修饰符已应用系统栏间距的一部分。在 imePadding()
修饰符应用的底部内边距大于系统栏后,Spacer
的高度为零。
当 IME 关闭时,变化会反过来:当 imePadding()
应用的区域小于系统栏的底边时,Spacer
会从零开始从零开始扩展,直到 IME 完全呈现动画效果后,Spacer
的高度与系统栏底部的高度一致。
这种行为是通过所有 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。由于这种平台行为,边衬区与 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") } } }
图 1. IME 动画
对 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 应用。
为您推荐
- 注意:当 JavaScript 处于关闭状态时,系统会显示链接文字
- Material 组件和布局
- 将
CoordinatorLayout
迁移到 Compose - 其他注意事项