2. Android 端状态栏沉浸适配

2.1 原理概述

沉浸式状态栏的核心矛盾在于:应用希望内容延伸至系统装饰区,同时关键交互控件不被系统栏遮挡。Android 通过三个层次解决该问题:

  • 系统 UI 标志位(SYSTEM_UI_FLAGS):控制状态栏与导航栏的可见性及布局模式。
  • WindowInsets:精确描述系统窗口对应用内容的侵入区域(状态栏高度、导航栏高度、键盘高度等),是自适应布局的数据基础。
  • 软键盘交互冲突:沉浸模式下键盘弹出会重新触发 WindowInsets 分发,若未正确处理将导致布局挤压或沉浸失效。
全局兼容底线:Android 5.0(API 21)引入 WindowInsets,但直到 Android 11(API 30)才提供类型化 API。项目必须使用 WindowInsetsCompat 以及 ViewCompat 才能低代价覆盖 5.0~14 全版本。

2.2 沉浸适配完整流程

下图概括了从 Activity 启动到沉浸完成的五个关键阶段,其中软键盘冲突检测是决定走正常路径还是修复路径的分岔口。

Android 状态栏沉浸适配流程 启动沉浸适配 设置系统UI标志位 处理WindowInsets 软键盘冲突检测 调整布局适配键盘 保持当前布局 沉浸适配完成 有冲突 无冲突
流程要点:菱形节点是运行时决策——软键盘弹出/收回会触发 onApplyWindowInsets,应用在此处判断当前是否处于键盘可见状态,从而决定是否需要额外调整布局。

2.3 关键 API 与配置实现

2.3.1 系统 UI 标志位组合

在 Activity.onCreate 中通过 DecorView 设置,推荐使用 三件套 保证内容延伸同时不丢失布局稳定性:

View decorView = getWindow().getDecorView();
decorView.setSystemUiVisibility(
    View.SYSTEM_UI_FLAG_LAYOUT_STABLE
  | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
  | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
);
标志位作用必需性
LAYOUT_STABLE防止系统栏显示/隐藏时布局抖动★ 必需
LAYOUT_FULLSCREEN内容延伸至状态栏后方★ 必需
LAYOUT_HIDE_NAVIGATION内容延伸至导航栏后方视设计而定

2.3.2 WindowInsets 响应处理

使用 ViewCompat.setOnApplyWindowInsetsListener 统一接收侵入区域,兼容 5.0~14+:

ViewCompat.setOnApplyWindowInsetsListener(decorView, (v, insets) -> {
    int statusBarTop = insets.getInsets(
        WindowInsetsCompat.Type.statusBars()
    ).top;
    int navBarBottom = insets.getInsets(
        WindowInsetsCompat.Type.navigationBars()
    ).bottom;
    int keyboardHeight = insets.getInsets(
        WindowInsetsCompat.Type.ime()
    ).bottom;

    // 将状态栏高度应用到 Toolbar/TopView 的 paddingTop
    toolbar.setPadding(
        toolbar.getPaddingLeft(),
        statusBarTop,
        toolbar.getPaddingRight(),
        toolbar.getPaddingBottom()
    );

    // 导航栏高度应用到 BottomNavigation 的 paddingBottom
    if (bottomNav != null) {
        bottomNav.setPadding(
            bottomNav.getPaddingLeft(),
            bottomNav.getPaddingTop(),
            bottomNav.getPaddingRight(),
            navBarBottom
        );
    }

    // 键盘高度用于调整列表/输入框位置
    if (keyboardHeight > 0) {
        onKeyboardShown(keyboardHeight);
    } else {
        onKeyboardHidden();
    }

    return WindowInsetsCompat.CONSUMED;  // 防止重复消费
});
⚠ 常见陷阱:不可在 onApplyWindowInsets 中直接修改 DecorView 的 margin 或触发 requestLayout 循环;应只更新目标子 View 的 padding,并在返回时选择 CONSUMED 或 insets 取决于是否需要继续传递。

2.3.3 软键盘冲突专用处理

当沉浸布局遇到键盘弹出,系统默认 SOFT_INPUT_ADJUST_RESIZE 会导致整个 Activity 重新布局,破坏沉浸效果。推荐以下分层策略:

  • 顶部区域:使用 fitSystemWindows="true" 或手动 padding 保持状态栏底色。
  • 滚动区域:监听键盘高度,动态增加 RecyclerView/ScrollView 的 bottom padding。
  • 输入框:使用 addOnGlobalLayoutListener 获取键盘可见性,滚动到焦点位置。
// 在 Activity.onCreate 中设置
getWindow().setSoftInputMode(
    WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING
);

// 通过 OnGlobalLayoutListener 监听键盘
final View content = findViewById(android.R.id.content);
content.getViewTreeObserver().addOnGlobalLayoutListener(() -> {
    Rect r = new Rect();
    content.getWindowVisibleDisplayFrame(r);
    int screenHeight = content.getRootView().getHeight();
    int keypadHeight = screenHeight - r.bottom;
    boolean isKeyboardVisible = keypadHeight > screenHeight * 0.15;

    if (isKeyboardVisible) {
        // 为底部留出键盘空间
        content.setPadding(0, 0, 0, keypadHeight);
    } else {
        content.setPadding(0, 0, 0, 0);
    }
});

2.4 调试方法

调试手段命令 / 操作验证目标
查看当前窗口标志位adb shell dumpsys window policy确认 sysUiFlags 是否包含 LAYOUT_STABLE 等
布局边界可视化开发者选项 → 显示布局边界检查 padding 是否准确覆盖状态栏/导航栏区域
WindowInsets 日志在 onApplyWindowInsets 中打印 insets.toString()确认 statusBar、navBar、ime 的 top/bottom 值
键盘高度实测使用 WindowVisibleDisplayFrame 对比全屏高度验证键盘弹出/收回时布局调整是否正确触发
高效调试技巧:在 onApplyWindowInsets 中插入 Log.d("Insets", insets.toString()),配合 adb logcat -s Insets 过滤,可以秒级定位是状态栏高度异常还是键盘通知缺失。

2.5 常见错误与避坑

  1. 未加 LAYOUT_STABLE 导致布局抖动
    表现:状态栏/导航栏显示隐藏时,整个页面忽上忽下。
    解决:始终组合 LAYOUT_STABLE + LAYOUT_FULLSCREEN。
  2. 只设标志位,不处理 WindowInsets
    表现:内容确实延伸到状态栏后方,但页面顶部/底部被系统栏遮挡,按钮无法点击。
    解决:必须在 onApplyWindowInsets 中为目标 View 添加对应 padding。
  3. 键盘弹出时沉浸失效
    表现:键盘弹出后状态栏和导航栏突然出现,背景颜色断裂。
    解决:使用 SOFT_INPUT_ADJUST_NOTHING + 手动 OnGlobalLayoutListener 替代系统 resize。
  4. fitsSystemWindows="true" 与沉浸冲突
    表现:明明设置了 LAYOUT_FULLSCREEN,顶部却仍然有状态栏高度空白。
    解决:沉浸模式下根布局不应使用 fitsSystemWindows,改为在子 View 上手动设置 padding。

2.6 性能与稳定性优化

  • 防重复触发:onApplyWindowInsets 在键盘弹出/收回期间可能高频调用,内部应做 值比较,仅在 height 变化超过阈值(如 10px)时才更新 UI。
  • 兼容库优先:始终使用 WindowInsetsCompat 和 ViewCompat,避免直接调用 View.onApplyWindowInsets 等平台 API,防止低版本崩溃。
  • 键盘监听去抖:OnGlobalLayoutListener 可能 1 秒内回调数十次,建议加入 Debounce 机制(例如 150ms 内只处理最后一次回调)。
  • 测量与布局分离:不要在 onApplyWindowInsets 中调用 requestLayout 或 invalidate,应仅修改 padding 并标记为 CONSUMED,由系统统一调度布局。
稳定性底线检查清单
✓ 是否在 onCreate 中一次性设置 systemUiVisibility,而非在 onResume 中重复设置?
✓ WindowInsets 回调中是否对每个 Type 做了空安全取值?
✓ 键盘监听是否已注册 removeOnGlobalLayoutListener 避免内存泄漏?