关于 edge-to-edge
面向 Android Jetpack Compose 项目的技能,指导将 Activity、列表和输入界面改造为 edge-to-edge 布局,并正确处理系统栏与 IME 内边距。要求 target SDK 35 或更高。
主要功能
- 01检查 Activity、列表和 FAB 的 edge-to-edge 支持
- 02配置 enableEdgeToEdge 与 adjustResize
- 03使用 Scaffold 和系统 Insets 管理布局
- 04处理 IME 遮挡与重复内边距
- 05适配对话框及系统栏图标对比度
适用场景
- 01为 Compose 应用补充 edge-to-edge 支持
- 02修复软键盘遮挡输入框的问题
- 03确保列表和 FAB 避开系统栏
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ android/ skills --skill edge-to-edge安装后在 Claude Code 中输入/edge-to-edge确认已加载。
详细介绍
前提条件
- 项目 必须 使用 Android Jetpack Compose。
- 项目 必须 以 SDK 35 或更高版本为目标。如果 SDK 低于 35,请将 SDK 提升至 35。
步骤 1:规划
- 定位并分析所有 Activity 类,检查哪些类已支持 edge-to-edge。对于尚未支持 edge-to-edge 的 Activity,规划为每个 Activity 添加该支持。
- 在每个 Activity 中,定位并分析所有列表和 FAB 组件,检查哪些组件已支持 edge-to-edge。对于尚未支持 edge-to-edge 的组件,规划为每个组件添加该支持。
- 在每个 Activity 中,扫描
TextField、OutlinedTextField或BasicTextField。如果找到,则 必须 按照此技能中的 IME 部分进行验证,确保 IME 不会遮挡输入框。
步骤 2:添加 edge-to-edge 支持
- 对于尚未调用
enableEdgeToEdge的每个 Activity,在其onCreate中的setContent之前添加enableEdgeToEdge。 - 在所有使用软键盘的 Activity 的 AndroidManifest.xml 中添加
android:windowSoftInputMode="adjustResize"。
步骤 3:应用 insets
-
应用 必须 应用系统 insets,或使内容与标尺对齐,以确保关键 UI 仍可点击。只选择一种方法,避免重复添加内边距:
- 首选: 在可用时,使用
Scaffold并将PaddingValues传递给内容 lambda。
Scaffold { innerPadding -> // innerPadding accounts for system bars and any Scaffold components LazyColumn( modifier = Modifier .fillMaxSize() .consumeWindowInsets(innerPadding), contentPadding = innerPadding ) { /* Content */ } }
-
首选: 在可用时,使用 Material 组件中的自动 inset 处理或内边距修饰符。
- Material 3 组件会为自身组件管理安全区域,包括:
TopAppBarSmallTopAppBarCenterAlignedTopAppBarMediumTopAppBarLargeTopAppBarBottomAppBarModalDrawerSheetDismissibleDrawerSheetPermanentDrawerSheetModalBottomSheetNavigationBarNavigationRail
- 对于 Material 2 组件,使用
windowInsetsparameter 为BottomAppBar、TopAppBar和BottomNavigation手动应用 insets。不要 为父容器添加内边距,而应直接将 insets 传递给 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("Login") } }
-
对于内边距过多的深层嵌套组件,请使用
WindowInsetsRulers(例如Modifier.fitInside(WindowInsetsRulers.SafeDrawing.current))。代码示例请参阅 IME 部分。 -
如果需要让某个元素(例如自定义标题栏或装饰性遮罩层)的尺寸 与系统栏相同,请使用 inset 尺寸修饰符(例如
Modifier.windowInsetsTopHeight(WindowInsets.systemBars))。 代码示例请参阅 列表 部分。
- 首选: 在可用时,使用
自适应脚手架
NavigationSuiteScaffold会为其自身组件(如NavigationRail或NavigationBar)处理安全区域。不过,自适应脚手架(例如NavigationSuiteScaffold、ListDetailPaneScaffold)不会将 PaddingValues 传递给其内部内容。你必须按照步骤 3中的说明,为各个屏幕或组件应用内边距(例如列表的contentPadding或 FAB 内边距)。不要对NavigationSuiteScaffold父级应用safeDrawingPadding或类似修饰符。这会裁剪内容,并阻止屏幕实现 edge-to-edge。
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。
// RIGHT
Scaffold(contentWindowInsets = WindowInsets.safeDrawing) { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.consumeWindowInsets(innerPadding)
.verticalScroll(rememberScrollState())
) { /* Content */ }
}
正确,因为无论 contentWindowInsets 如何设置,fitInside 都会使内容适配 IME 内边距。
// RIGHT
Scaffold() { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.consumeWindowInsets(innerPadding)
.fitInside(WindowInsetsRulers.Ime.current)
.verticalScroll(rememberScrollState())
) { /* Content */ }
}
正确,因为默认的 contentWindowInsets 不包含 IME 内边距,而 imePadding() 会应用 IME 内边距:
// RIGHT
Scaffold() { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.consumeWindowInsets(innerPadding)
.imePadding()
.verticalScroll(rememberScrollState())
) { /* Content */ }
}
错误
错误,因为 IME 打开时会产生多余的内边距。IME 内边距被应用了两次:一次通过 innerPadding,其中包含由传入的 contentWindowInsets 值提供的 IME 内边距;另一次通过 imePadding:
// WRONG
Scaffold(contentWindowInsets = WindowInsets.safeDrawing) { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.imePadding()
.verticalScroll(rememberScrollState())
) { /* Content */ }
}
错误,因为 IME 会遮挡内容。Scaffold 的默认 contentWindowInsets 不包含 IME 内边距。
// WRONG
Scaffold() { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.verticalScroll(rememberScrollState())
) { /* Content */ }
}
不使用 Scaffold 的 IME 代码模式
正确
以下代码示例不会导致内边距过大。
// RIGHT
Box(
// Insets consumed
modifier = Modifier.safeDrawingPadding() // or imePadding(), safeContentPadding(), safeGesturesPadding()
) {
Column(
modifier = Modifier.imePadding()
) { /* Content */ }
}
// RIGHT
Box(
// Insets consumed
modifier = Modifier.windowInsetsPadding(WindowInsets.safeDrawing) // or WindowInsets.ime, WindowInsets.safeContent, WindowInsets.safeGestures
) {
Column(
modifier = Modifier.imePadding()
) { /* Content */ }
}
// RIGHT
Box(
// Insets not consumed, but irrelevant due to fitInside
modifier = Modifier.padding(WindowInsets.safeDrawing.asPaddingValues()) // or WindowInsets.ime.asPaddingValues(), WindowInsets.safeContent.asPaddingValues(), WindowInsets.safeGestures.asPaddingValues()
) {
Column(
modifier = Modifier
.fillMaxSize()
.fitInside(WindowInsetsRulers.Ime.current)
) { /* Content */ }
}
错误
以下代码示例会导致内边距过大,因为 IME 内边距被应用了两次:
// WRONG
Box(
// Insets not consumed
modifier = Modifier.padding(WindowInsets.safeDrawing.asPaddingValues()) // or WindowInsets.ime.asPaddingValues(), WindowInsets.safeContent.asPaddingValues(), WindowInsets.safeGestures.asPaddingValues()
) {
Column(
modifier = Modifier.imePadding()
) { /* Content */ }
}
导航栏对比度 & 系统栏图标
-
如果 Activity 使用
WindowCompat中的enableEdgeToEdge,对于支持浅色和深色主题的应用,你必须将isAppearanceLightNavigationBars和isAppearanceLightStatusBars设置为与设备主题相反的值,以确保系统栏图标清晰可见。建议在主题文件中进行此设置。如果 Activity 使用ComponentActivity中的enableEdgeToEdge,不要进行此设置,因为它会自动处理图标颜色。// Only use if calling `enableEdgeToEdge` from `WindowCompat`. // Apply to your theme file. @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) // Dark icons for Light Mode (!darkTheme), Light icons for Dark Mode controller.isAppearanceLightStatusBars = !darkTheme controller.isAppearanceLightNavigationBars = !darkTheme } } MaterialTheme(content = content) }
-
如果任何屏幕使用带底部栏(例如
BottomAppBar、NavigationBar)的Scaffold或NavigationSuiteScaffold,请在对应的 Activity 中为 SDK 29 及更高版本设置window.isNavigationBarContrastEnforced = false。这样可以防止系统为导航栏添加半透明背景,并确保底部栏的颜色延伸至屏幕底部。
列表
- 将内边距(例如
Scaffold的innerPadding)应用于可滚动组件(例如LazyColumn、LazyRow)的contentPadding参数。不要将其作为Modifier.padding()应用于列表的父容器,因为这会裁剪内容,并阻止内容在系统栏后方滚动。 - 创建一个覆盖系统栏的半透明可组合项,以确保图标仍清晰可见。
class SystemBarProtectionSnippets : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// enableEdgeToEdge sets window.isNavigationBarContrastEnforced = true
// which is used to add a translucent scrim to three-button navigation
enableEdgeToEdge()
setContent {
MyTheme {
// Main content
MyContent()
// After drawing main content, draw status bar protection
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 = { /* Handle dismiss */ },
properties = DialogProperties(
// 1. Allows the dialog to span the full width of the screen
usePlatformDefaultWidth = false,
// 2. Allows the dialog to draw behind status and navigation bars
decorFitsSystemWindows = false
)
) { /* Content */ }
检查清单
- [ ] 每个
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进行确认。