前置条件
- 项目必须使用 Android Jetpack Compose。
- 项目必须以 SDK 35 或更高版本为目标。如果 SDK 低于 35,请将 SDK 提升至 35。
第一步:规划
- 定位并分析所有 Activity 类,检测哪些已有边到边支持。对于每个没有边到边的 Activity,计划使其支持边到边。
- 在每个 Activity 中,定位并分析所有列表和 FAB 组件,检测哪些已有边到边支持。对于每个没有边到边支持的组件,计划使其支持边到边。
- 在每个 Activity 中,扫描
TextField、OutlinedTextField或BasicTextField。如果找到,你必须按照本技能的 IME 部分验证 IME 不会隐藏输入字段。
第二步:添加边到边支持
- 在每个尚未调用
enableEdgeToEdge的 Activity 的onCreate中,在setContent之前添加enableEdgeToEdge。 - 在 AndroidManifest.xml 中为所有使用软键盘的 Activity 添加
android:windowSoftInputMode="adjustResize"。
第三步:应用插入区域
-
应用必须应用系统插入区域,或将内容与标尺对齐,以确保关键 UI 保持可点击状态。仅选择一种方法以避免双重内边距:
- 首选: 如果可用,使用
Scaffold并将PaddingValues传递给内容 lambda。
Scaffold { innerPadding -> // innerPadding 考虑了系统栏和任何 Scaffold 组件 LazyColumn( modifier = Modifier .fillMaxSize() .consumeWindowInsets(innerPadding), contentPadding = innerPadding ) { /* 内容 */ } }-
首选: 如果可用,使用 Material 组件的自动插入区域处理或内边距修饰符。
- Material 3 组件为其自身组件管理安全区域,包括:
TopAppBarSmallTopAppBarCenterAlignedTopAppBarMediumTopAppBarLargeTopAppBarBottomAppBarModalDrawerSheetDismissibleDrawerSheetPermanentDrawerSheetModalBottomSheetNavigationBarNavigationRail
- 对于 Material 2 组件,使用
windowInsets参数手动为BottomAppBar、TopAppBar和BottomNavigation应用插入区域。不要将内边距应用于父容器;而是直接将插入区域传递给 App Bar 组件。将内边距应用于父容器会阻止 App Bar 背景绘制到系统栏区域。例如,对于TopAppBar,仅选择以下选项之一:- 首选:
TopAppBar(windowInsets = AppBarDefaults.topAppBarWindowInsets) TopAppBar(windowInsets = WindowInsets.systemBars.exclude(WindowInsets.navigationBars))TopAppBar(windowInsets = WindowInsets.systemBars.add(WindowInsets.captionBar))
- 首选:
- Material 3 组件为其自身组件管理安全区域,包括:
-
对于 Scaffold 之外的组件,使用内边距修饰符,例如
Modifier.safeDrawingPadding()或Modifier.windowInsetsPadding(WindowInsets.safeDrawing)。Box( modifier = Modifier .fillMaxSize() .safeDrawingPadding() ) { Button( onClick = {}, modifier = Modifier.align(Alignment.BottomCenter) ) { Text("登录") } } -
对于嵌套过深且内边距过大的组件,使用
WindowInsetsRulers(例如Modifier.fitInside(WindowInsetsRulers.SafeDrawing.current))。请参阅 IME 部分的代码示例。 -
当需要一个元素(例如自定义标题或装饰性遮罩)与系统栏尺寸相等时,使用插入区域大小修饰符(例如
Modifier.windowInsetsTopHeight(WindowInsets.systemBars))。请参阅 列表 部分的代码示例。
- 首选: 如果可用,使用
自适应脚手架
NavigationSuiteScaffold为其自身组件管理安全区域,例如NavigationRail或NavigationBar。但是,自适应脚手架(例如NavigationSuiteScaffold、ListDetailPaneScaffold)不会将 PaddingValues 传播到其内部内容。你必须按照 第三步 中的描述,将插入区域应用于每个独立的屏幕或组件(例如列表的contentPadding或 FAB 内边距)。不要将safeDrawingPadding或类似修饰符应用于NavigationSuiteScaffold的父级。这会裁剪并阻止边到边屏幕。
IME
- 对于每个使用软键盘的 Activity,检查 AndroidManifest.xml 中是否设置了
android:windowSoftInputMode="adjustResize"。不要使用SOFT_INPUT_ADJUST_RESIZE,因为它已被弃用。然后,保持输入字段的焦点。选择以下一种:-
- 首选: 向内容容器添加
Modifier.fitInside(WindowInsetsRulers.Ime.current)。这比imePadding()更优,因为它减少了因忘记在层次结构上游消耗插入区域而导致的卡顿和额外内边距。
- 首选: 向内容容器添加
-
- 向内容容器添加
imePadding。内边距修饰符必须放置在Modifier.verticalScroll()之前。如果父容器已通过contentWindowInsets(例如contentWindowInsets = WindowInsets.safeDrawing)考虑了 IME,则不要使用Modifier.imePadding()。这样做会导致双重内边距。
- 向内容容器添加
-
使用 Scaffold 的 IME 代码模式
正确
正确,因为 contentWindowInsets 包含 IME 插入区域,这些插入区域作为 innerPadding 传递给内容 lambda。
// 正确
Scaffold(contentWindowInsets = WindowInsets.safeDrawing) { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.consumeWindowInsets(innerPadding)
.verticalScroll(rememberScrollState())
) { /* 内容 */ }
}
正确,因为 fitInside 使内容适应 IME 插入区域,无论 contentWindowInsets 如何。
// 正确
Scaffold() { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.consumeWindowInsets(innerPadding)
.fitInside(WindowInsetsRulers.Ime.current)
.verticalScroll(rememberScrollState())
) { /* 内容 */ }
}
正确,因为默认的 contentWindowInsets 不包含 IME 插入区域,并且 imePadding() 应用了 IME 插入区域:
// 正确
Scaffold() { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.consumeWindowInsets(innerPadding)
.imePadding()
.verticalScroll(rememberScrollState())
) { /* 内容 */ }
}
错误
错误,因为当 IME 打开时会有过多的内边距。IME 插入区域被应用了两次,一次是通过 innerPadding(它包含从传递的 contentWindowInsets 值中获得的 IME 插入区域),另一次是通过 imePadding:
// 错误
Scaffold(contentWindowInsets = WindowInsets.safeDrawing) { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.imePadding()
.verticalScroll(rememberScrollState())
) { /* 内容 */ }
}
错误,因为 IME 会覆盖内容。Scaffold 的默认 contentWindowInsets 不包含 IME 插入区域。
// 错误
Scaffold() { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.verticalScroll(rememberScrollState())
) { /* 内容 */ }
}
不使用 Scaffold 的 IME 代码模式
正确
以下代码示例不会导致过多的内边距。
// 正确
Box(
// 插入区域已消耗
modifier = Modifier.safeDrawingPadding() // 或 imePadding(), safeContentPadding(), safeGesturesPadding()
) {
Column(
modifier = Modifier.imePadding()
) { /* 内容 */ }
}
// 正确
Box(
// 插入区域已消耗
modifier = Modifier.windowInsetsPadding(WindowInsets.safeDrawing) // 或 WindowInsets.ime, WindowInsets.safeContent, WindowInsets.safeGestures
) {
Column(
modifier = Modifier.imePadding()
) { /* 内容 */ }
}
// 正确
Box(
// 插入区域未消耗,但由于 fitInside 而无关紧要
modifier = Modifier.padding(WindowInsets.safeDrawing.asPaddingValues()) // 或 WindowInsets.ime.asPaddingValues(), WindowInsets.safeContent.asPaddingValues(), WindowInsets.safeGestures.asPaddingValues()
) {
Column(
modifier = Modifier
.fillMaxSize()
.fitInside(WindowInsetsRulers.Ime.current)
) { /* 内容 */ }
}
错误
以下代码示例会导致过多的内边距,因为 IME 插入区域被应用了两次:
// 错误
Box(
// 插入区域未消耗
modifier = Modifier.padding(WindowInsets.safeDrawing.asPaddingValues()) // 或 WindowInsets.ime.asPaddingValues(), WindowInsets.safeContent.asPaddingValues(), WindowInsets.safeGestures.asPaddingValues()
) {
Column(
modifier = Modifier.imePadding()
) { /* 内容 */ }
}
导航栏对比度和系统栏图标
-
如果 Activity 使用
WindowCompat的enableEdgeToEdge,你必须将isAppearanceLightNavigationBars和isAppearanceLightStatusBars设置为与设备主题相反的值(对于支持亮暗主题的应用),以使系统栏图标清晰可读。建议在你的主题文件中执行此操作。如果 Activity 使用ComponentActivity的enableEdgeToEdge,则不要执行此操作,因为后者会自动处理图标颜色。// 仅在从 `WindowCompat` 调用 `enableEdgeToEdge` 时使用。 // 应用于你的主题文件。 @Composable fun MyTheme( darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit ) { val view = LocalView.current if (!view.isInEditMode) { SideEffect { val window = (view.context as? Activity)?.window ?: return@SideEffect val controller = WindowCompat.getInsetsController(window, view) // 亮色模式下使用深色图标 (!darkTheme),暗色模式下使用浅色图标 controller.isAppearanceLightStatusBars = !darkTheme controller.isAppearanceLightNavigationBars = !darkTheme } } MaterialTheme(content = content) } -
如果任何屏幕使用带有底部栏(例如
BottomAppBar、NavigationBar)的Scaffold或NavigationSuiteScaffold,请在相应的 Activity 中为 SDK 29+ 设置window.isNavigationBarContrastEnforced = false。这可以防止系统向导航栏添加半透明背景,从而确保你的底部栏颜色延伸到屏幕底部。
列表
- 将插入内边距(如
Scaffold的innerPadding)应用于可滚动组件的contentPadding参数(例如LazyColumn、LazyRow)。不要将其作为Modifier.padding()应用于列表的父容器,因为这会裁剪内容并阻止其滚动到系统栏后面。 - 创建一个覆盖系统栏的半透明可组合项,以确保图标仍然清晰可读。
class SystemBarProtectionSnippets : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// enableEdgeToEdge 设置 window.isNavigationBarContrastEnforced = true
// 用于为三按钮导航添加半透明遮罩
enableEdgeToEdge()
setContent {
MyTheme {
// 主内容
MyContent()
// 绘制主内容后,绘制状态栏保护
StatusBarProtection()
}
}
}
}
@Composable
private fun StatusBarProtection(
color: Color = MaterialTheme.colorScheme.surfaceContainer,
) {
Spacer(
modifier = Modifier
.fillMaxWidth()
.height(
with(LocalDensity.current) {
(WindowInsets.statusBars.getTop(this) * 1.2f).toDp()
}
)
.background(
brush = Brush.verticalGradient(
colors = listOf(
color.copy(alpha = 1f),
color.copy(alpha = 0.8f),
Color.Transparent
)
)
)
)
}
对话框
如果以下两个条件都为真,则对话框为全屏,并且必须支持边到边:
DialogProperties包含usePlatformDefaultWidth = false。- 对话框调用
Modifier.fillMaxSize()。
要使全屏对话框支持边到边,请在 DialogProperties 中设置 decorFitsSystemWindows = false。
Dialog(
onDismissRequest = { /* 处理关闭 */ },
properties = DialogProperties(
// 1. 允许对话框跨越屏幕完整宽度
usePlatformDefaultWidth = false,
// 2. 允许对话框绘制到状态栏和导航栏后面
decorFitsSystemWindows = false
)
) { /* 内容 */ }
检查清单
- [ ] 每个
Activity是否都调用了enableEdgeToEdge()? - [ ] 是否在
AndroidManifest.xml中设置了adjustResize? - [ ] 每个
TextField、OutlinedTextField或BasicTextField是否都有一个包含imePadding()、fitInside、Modifier.safeDrawingPadding()、Modifier.safeContentPadding()、Modifier.safeGesturesPadding()或将contentWindowInsets设置为WindowInsets.safeDrawing或WindowInsets.ime的父级? - [] 第一个和最后一个列表项是否通过将插入区域传递给
contentPadding而远离系统栏绘制? - [] FAB 是否通过位于 Scaffold 内部或应用
Modifier.safeDrawingPadding()绘制在导航栏上方? - [] 项目是否构建成功?运行
./gradlew build以确认。