/ Android  Edge-to-Edge  WindowInsets  Jetpack Compose  全面屏适配  Android16  沉浸式  系统栏 

Android 16 边到边适配全攻略:WindowInsetsCompat 实战指南


封面

前言:Android 16 强制边到边

Google 在 Android 15 中引入了边到边(Edge-to-Edge)显示的强制要求,并在 Android 16 中进一步收紧——所有 targetSdk 36 的应用将无法再通过 setDecorFitsSystemWindows(true) 恢复传统布局模式。这意味着每一款 Android 应用都必须正视全面屏适配问题。

边到边显示让应用内容可以延伸到状态栏和导航栏后面,带来更沉浸的视觉体验。然而,如果不正确处理系统栏遮挡,按钮、输入框等关键 UI 元素就会被系统栏覆盖,导致用户无法正常操作。

核心 API:WindowInsetsCompat 完全解析

WindowInsetsCompat 是 Jetpack 提供的 Insets 兼容层,向下兼容到 API 21。它将各种系统 UI 元素(状态栏、导航栏、IME 键盘等)的占据空间以 Insets 对象的形式提供给开发者。

  • systemBars():状态栏 + 导航栏的合集,最常用
  • statusBars():仅状态栏区域
  • navigationBars():仅导航栏区域(手势导航时 bottom 可能为 0)
  • ime():软键盘区域,处理键盘弹出遮挡问题
  • displayCutout():刘海屏/打孔屏的安全区域
  • systemGestures():手势导航触发区域,避免内容与手势冲突
// 在 Activity.onCreate 中开启边到边
WindowCompat.setDecorFitsSystemWindows(window, false)

// 监听并应用 insets
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, insets ->
    val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
    view.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
    insets
}

View 体系下的适配方案

对于仍使用传统 View 体系的项目,推荐通过 ViewCompat.setOnApplyWindowInsetsListener 精确控制每个视图的内边距,而不是粗暴地给根布局加 padding。

最佳实践:只给需要避开系统栏的视图设置 insets

// 底部导航栏:只需处理 bottom inset
ViewCompat.setOnApplyWindowInsetsListener(binding.bottomNavigation) { view, insets ->
    val navBars = insets.getInsets(WindowInsetsCompat.Type.navigationBars())
    view.updatePadding(bottom = navBars.bottom)
    insets
}

// 顶部 Toolbar:只需处理 top inset
ViewCompat.setOnApplyWindowInsetsListener(binding.toolbar) { view, insets ->
    val statusBar = insets.getInsets(WindowInsetsCompat.Type.statusBars())
    view.updateLayoutParams {
        topMargin = statusBar.top
    }
    insets
}

// 滚动列表:同时处理 top 和 bottom,避免内容被遮挡
ViewCompat.setOnApplyWindowInsetsListener(binding.recyclerView) { view, insets ->
    val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
    view.updatePadding(top = systemBars.top, bottom = systemBars.bottom)
    view.clipToPadding = false // 允许内容在 padding 区域滚动
    insets
}

注意 clipToPadding = false 对于 RecyclerView 非常重要,它允许列表项在滚动到底部时能完整显示在导航栏上方,同时在列表末端保留足够的 padding 空间。

Jetpack Compose 适配方案

Compose 从 1.2 版本开始提供了 WindowInsets API,配合 Compose BOM 1.6+ 使用体验更佳。

// build.gradle.kts
implementation(platform("androidx.compose:compose-bom:2024.09.00"))
implementation("androidx.compose.material3:material3")
// 使用 Scaffold 自动处理 insets(推荐)
Scaffold(
    modifier = Modifier.fillMaxSize(),
    topBar = { TopAppBar(title = { Text("My App") }) },
    bottomBar = { NavigationBar { /* ... */ } }
) { innerPadding ->
    // innerPadding 已包含系统栏 insets
    LazyColumn(
        modifier = Modifier.padding(innerPadding)
    ) {
        // 列表内容
    }
}

// 手动处理 insets
val insets = WindowInsets.systemBars
Box(
    modifier = Modifier
        .fillMaxSize()
        .windowInsetsPadding(insets)
) {
    // 内容
}

Material3 的 Scaffold 已经内置了 insets 处理逻辑,在大多数情况下使用 Scaffold 即可自动完成适配,无需手动处理。

键盘弹出适配与 IME 动画

键盘弹出导致内容被遮挡是另一大痛点。Android 11+ 提供了 WindowInsetsAnimation 机制,可以实现键盘弹出时内容平滑上移的动画效果。

// 在 Activity 中配置
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING)

// 使用 WindowInsetsAnimationCompat 监听键盘动画
ViewCompat.setWindowInsetsAnimationCallback(
    binding.root,
    object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_STOP) {
        override fun onProgress(
            insets: WindowInsetsCompat,
            runningAnimations: List
        ): WindowInsetsCompat {
            val imeHeight = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
            val navBarHeight = insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom
            binding.root.translationY = -(imeHeight - navBarHeight).coerceAtLeast(0).toFloat()
            return insets
        }
    }
)

Compose 中可以使用 imePadding() 修饰符,配合 animateContentSize() 实现平滑的键盘适配动画,整体代码更加简洁直观。

常见问题排查

  • 状态栏图标看不见:检查状态栏背景色与图标颜色对比度,使用 WindowInsetsControllerCompat.isAppearanceLightStatusBars 切换深色/浅色模式
  • 底部按钮被手势条遮挡:确认给底部视图添加了 navigationBars inset 的 padding
  • 横屏时左右出现黑边:处理 displayCutout() insets,给内容区域添加左右 padding
  • Dialog/BottomSheet 未适配:需要在 Dialog 的 window 上单独调用 WindowCompat.setDecorFitsSystemWindows(false)
  • WebView 中内容被遮挡:通过 WebView.setOnApplyWindowInsetsListener 或 CSS env(safe-area-inset-*) 处理

发布评论

热门评论区: