移动/游戏
Jetpack Compose 应用迁移到自适应…
当 Jetpack Compose 应用需要支持 edge-to-edge、修复按钮或列表被导航栏/状态栏遮挡、处理 IME insets 或系统栏图标可读性时使用。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 38/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 55/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 60/100 ×0.15
适用场景
Jetpack Compose 应用迁移特定
解决思路
- 若项目 target SDK 低于 35,将其提升到 35。
- 计划阶段:定位并分析所有 Activity 类,检测哪些已有 edge-to-edge 支持;对每个没有 edge-to-edge 的 Activity,规划使其支持 edge-to-edge。
- 在每个 Activity 中定位并分析所有列表和 FAB 组件,检测哪些已有 edge-to-edge 支持;对每个不支持 edge-to-edge 的组件,规划使其支持 edge-to-edge。
- 在每个 Activity 中扫描 TextField、OutlinedTextField 或 BasicTextField;若发现,必须按本技能的 IME 部分验证 IME 不会遮挡输入框。
- 在每个尚未调用 enableEdgeToEdge 的 Activity 的 onCreate 中,在 setContent 之前添加 enableEdgeToEdge。
- 在 AndroidManifest.xml 中为所有使用软键盘的 Activity 添加 android:windowSoftInputMode="adjustResize"。
- 应用系统 insets 或将内容对齐到 rulers,确保关键 UI 可点击;只能选择一种方法以避免双倍 padding。
- 优先:可用时使用 Scaffold,并将 PaddingValues 传入 content lambda。例如 `Scaffold { innerPadding -> LazyColumn(modifier = Modifier.fillMaxSize().consumeWindowInsets(innerPadding), contentPadding = innerPadding) { ... } }`。
- 优先:可用时使用 Material 组件的自动 inset 处理或 padding modifier。
- Material 3 组件会管理自身组件的安全区域,包括 TopAppBar、SmallTopAppBar、CenterAlignedTopAppBar、MediumTopAppBar、LargeTopAppBar、BottomAppBar、ModalDrawerSheet、DismissibleDrawerSheet、PermanentDrawerSheet、ModalBottomSheet、NavigationBar、NavigationRail。
- Material 2 组件:对 BottomAppBar、TopAppBar 和 BottomNavigation 使用 windowInsets 参数手动应用 insets;不要给父容器加 padding,而应直接把 insets 传给 App Bar 组件。给父容器加 padding 会阻止 App Bar 背景绘制到系统栏区域。
- 对于 TopAppBar,按以下选项之一设置(仅选一个):1) 优先 `TopAppBar(windowInsets = AppBarDefaults.topAppBarWindowInsets)`;2) `TopAppBar(windowInsets = WindowInsets.systemBars.exclude(WindowInsets.navigationBars))`;3) `TopAppBar(windowInsets = WindowInsets.systemBars.add(WindowInsets.captionBar))`。
- 对于 Scaffold 之外的组件,使用 padding modifier,例如 `Modifier.safeDrawingPadding()` 或 `Modifier.windowInsetsPadding(WindowInsets.safeDrawing)`。例如 Box 使用 safeDrawingPadding 后,底部对齐的 Button 不会被系统栏遮挡。
- 对于嵌套过深且 padding 过多的组件,使用 WindowInsetsRulers,例如 `Modifier.fitInside(WindowInsetsRulers.SafeDrawing.current)`;可参考 IME 部分的代码示例。
- 当需要元素(例如自定义 header 或装饰性 scrim)等于系统栏尺寸时,使用 inset 尺寸 modifier,例如 `Modifier.windowInsetsTopHeight(WindowInsets.systemBars)`;可参考 Lists 部分的代码示例。
- Adaptive Scaffolds:NavigationSuiteScaffold 会管理自身组件(如 NavigationRail 或 NavigationBar)的安全区域,但 adaptive scaffolds(例如 NavigationSuiteScaffold、ListDetailPaneScaffold)不会向内部内容传播 PaddingValues。
- 对于 adaptive scaffolds,必须按 Step 3 将 insets 应用到各个屏幕或组件(例如列表 contentPadding 或 FAB padding);不要对 NavigationSuiteScaffold 父级应用 safeDrawingPadding 或类似 modifier,否则会裁剪并阻止 edge-to-edge 屏幕。
- IME:对每个使用软键盘的 Activity,检查 AndroidManifest.xml 中是否设置 android:windowSoftInputMode="adjustResize";不要使用 SOFT_INPUT_ADJUST_RESIZE,因为它已废弃。
- IME:保持焦点在输入框上,并选择以下之一:1) 优先给内容容器添加 `Modifier.fitInside(WindowInsetsRulers.Ime.current)`;相比 imePadding(),它减少 jank 以及因上游未消费 insets 导致的额外 padding。
- IME:选择以下之一(续):2) 给内容容器添加 `imePadding`;该 padding modifier 必须放在 `Modifier.verticalScroll()` 之前。
- IME:如果父级已经通过 `contentWindowInsets`(例如 `contentWindowInsets = WindowInsets.safeDrawing`)处理 IME,则不要使用 `Modifier.imePadding()`,否则会造成双倍 padding。
- IME 与 Scaffold 正确模式:`contentWindowInsets` 包含 IME insets,并作为 `innerPadding` 传给 content lambda;在 Column 上使用 `padding(innerPadding)`、`consumeWindowInsets(innerPadding)`、`verticalScroll(rememberScrollState())`。
- IME 与 Scaffold 正确模式:`fitInside` 无论 `contentWindowInsets` 如何,都会将内容适配到 IME insets;在 Column 上使用 `padding(innerPadding)`、`consumeWindowInsets(innerPadding)`、`fitInside(WindowInsetsRulers.Ime.current)`、`verticalScroll(rememberScrollState())`。
- IME 与 Scaffold 正确模式:默认 `contentWindowInsets` 不包含 IME insets,使用 `imePadding()` 应用 IME insets;在 Column 上使用 `padding(innerPadding)`、`consumeWindowInsets(innerPadding)`、`imePadding()`、`verticalScroll(rememberScrollState())`。
- IME 与 Scaffold 错误模式:当 `contentWindowInsets = WindowInsets.safeDrawing` 时,同时使用 `padding(innerPadding)` 和 `imePadding()` 会在 IME 打开时造成多余 padding,因为 IME insets 被应用了两次。
- IME 与 Scaffold 错误模式:Scaffold 默认 `contentWindowInsets` 不包含 IME insets;如果只使用 `padding(innerPadding)` 和 `verticalScroll(...)`,IME 会覆盖内容。
- IME 无 Scaffold 正确模式:外层 Box 使用 `Modifier.safeDrawingPadding()`(或 `imePadding()`、`safeContentPadding()`、`safeGesturesPadding()`)消费 insets,内层 Column 使用 `Modifier.imePadding()`。
- IME 无 Scaffold 正确模式:外层 Box 使用 `Modifier.windowInsetsPadding(WindowInsets.safeDrawing)`(或 `WindowInsets.ime`、`WindowInsets.safeContent`、`WindowInsets.safeGestures`)消费 insets,内层 Column 使用 `Modifier.imePadding()`。
- IME 无 Scaffold 正确模式:外层 Box 使用 `Modifier.padding(WindowInsets.safeDrawing.asPaddingValues())`(或 `WindowInsets.ime.asPaddingValues()` 等),即使未消费 insets 也因内层 Column 使用 `Modifier.fillMaxSize().fitInside(WindowInsetsRulers.Ime.current)` 而不出问题。
- IME 无 Scaffold 错误模式:外层 Box 使用 `Modifier.padding(WindowInsets.safeDrawing.asPaddingValues())`(或类似 asPaddingValues)且未消费 insets,内层 Column 又使用 `Modifier.imePadding()`,会造成双倍 IME padding。
- Navigation Bar Contrast & System Bar Icons:如果 Activity 使用来自 `WindowCompat` 的 `enableEdgeToEdge`,对于支持亮/暗主题的应用,必须将 `isAppearanceLightNavigationBars` 和 `isAppearanceLightStatusBars` 设置为设备主题的反值,以保证系统栏图标可读;建议在主题文件中完成。
- 不要在使用来自 `ComponentActivity` 的 `enableEdgeToEdge` 时设置上述图标颜色,因为它会自动处理图标颜色。
- 主题文件代码做法:@Composable MyTheme 中通过 LocalView.current 获取 view,并用 SideEffect 获取 window 和 WindowCompat.getInsetsController(window, view),设置 `controller.isAppearanceLightStatusBars = !darkTheme` 与 `controller.isAppearanceLightNavigationBars = !darkTheme`。
- 如果任何屏幕使用带底部栏(如 BottomAppBar、NavigationBar)的 Scaffold 或 NavigationSuiteScaffold,则在对应 Activity 中为 SDK 29+ 设置 `window.isNavigationBarContrastEnforced = false`,防止系统给导航栏添加半透明背景,确保底部栏颜色延伸到屏幕底部。
- Lists:将 inset padding(如 Scaffold 的 innerPadding)应用到可滚动组件(如 LazyColumn、LazyRow)的 contentPadding 参数;不要将它作为 Modifier.padding() 应用到列表父容器,否则会裁剪内容并阻止内容滚动到系统栏后面。
- Lists:创建一个半透明 composable 覆盖系统栏,使图标仍可读。
- Lists 代码做法:在 ComponentActivity 的 onCreate 中调用 enableEdgeToEdge() 和 setContent;在内容绘制后绘制 StatusBarProtection,例如使用 Spacer 搭配 `Modifier.fillMaxWidth().height(...)` 和垂直渐变背景,高度基于 `WindowInsets.statusBars.getTop(LocalDensity.current) * 1.2f`。
- Dialogs:如果 DialogProperties 包含 `usePlatformDefaultWidth = false`,且 Dialog 调用 `Modifier.fillMaxSize()`,则该 Dialog 是全屏 Dialog,必须使其 edge-to-edge。
- Dialogs:要使全屏 Dialog edge-to-edge,在 DialogProperties 中设置 `decorFitsSystemWindows = false`;示例同时设置 `usePlatformDefaultWidth = false` 和 `decorFitsSystemWindows = false`。
- Checklist:确认每个 Activity 都调用 `enableEdgeToEdge()`。
- Checklist:确认 AndroidManifest.xml 中设置了 `adjustResize`。
- Checklist:确认每个 TextField、OutlinedTextField 或 BasicTextField 的父级具有 `imePadding()`、`fitInside`、`Modifier.safeDrawingPadding()`、`Modifier.safeContentPadding()`、`Modifier.safeGesturesPadding()`,或 contentWindowInsets 设置为 `WindowInsets.safeDrawing` 或 `WindowInsets.ime`。
- Checklist:确认第一个和最后一个列表项通过向 contentPadding 传入 insets,从而绘制时避开系统栏。
- Checklist:确认 FAB 通过位于 Scaffold 内或应用 `Modifier.safeDrawingPadding()` 绘制在导航栏之上。
- Checklist:确认项目可构建;运行 `./gradlew build`。
适用边界 · 注意事项
- 项目不使用 Android Jetpack Compose 时。
- 无法将 target SDK 提升到 35 或更高时。
- 只需要传统 Android View 体系或非 Compose UI 的适配时。