[Launcher3拆解系列1] Insets 适配机制详解

1. 背景与目标

现代 Android 设备有状态栏、导航栏、手势区域、刘海屏等多种系统栏。为了保证内容不被这些系统栏遮挡,Launcher3 设计了一套 insets(安全区域)适配机制。

2. 核心接口设计

Insettable 接口

java 复制代码
public interface Insettable {
    void setInsets(Rect insets);
}
  • 作用:让实现它的 View 能够感知系统栏占用区域(insets),并据此调整布局。
  • 典型场景:顶部状态栏、底部导航栏、手势区等区域的适配。

3. 适配分发链路

3.1 根布局 LauncherRootView

  • 继承自 InsettableFrameLayout,实现了 insets 分发的核心逻辑。
  • 重写 onApplyWindowInsets,在系统栏变化时自动被 Android 框架调用。
java 复制代码
@Override
public WindowInsets onApplyWindowInsets(WindowInsets insets) {
    insets = WindowManagerProxy.INSTANCE.get(getContext())
            .normalizeWindowInsets(getContext(), insets, mTempRect);
    handleSystemWindowInsets(mTempRect);
    return insets;
}

private void handleSystemWindowInsets(Rect insets) {
    mActivity.getDeviceProfile().updateInsets(insets);
    boolean resetState = !insets.equals(mInsets);
    setInsets(insets); // 关键:分发 insets
    if (resetState) {
        mActivity.getStateManager().reapplyState(true);
    }
}

3.2 InsettableFrameLayout 的分发实现

  • 遍历所有子 View,如果子 View 也实现了 Insettable,则递归调用其 setInsets,否则直接调整 margin。
java 复制代码
@Override
public void setInsets(Rect insets) {
    final int n = getChildCount();
    for (int i = 0; i < n; i++) {
        final View child = getChildAt(i);
        setFrameLayoutChildInsets(child, insets, mInsets);
    }
    mInsets.set(insets);
}

4. 典型实现类

  • AllApps、Hotseat、ScrimView、WorkModeSwitch 等

    • 这些 View 都实现了 Insettable,在 setInsets 方法里根据 insets 调整自己的 margin、padding、可见性等。

    • 例如 AllApps:

      java 复制代码
      @Override
      public void setInsets(Rect insets) {
          mInsets.set(insets);
          // ... 省略部分代码
          MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams();
          if (grid.isTablet) {
              mlp.leftMargin = mlp.rightMargin = 0;
          } else {
              mlp.leftMargin = insets.left;
              mlp.rightMargin = insets.right;
          }
          setLayoutParams(mlp);
          // ...
          InsettableFrameLayout.dispatchInsets(this, insets);
      }
  • 递归分发:每一层 ViewGroup 都会把 insets 继续分发给自己的子 View,直到所有实现 Insettable 的 View 都能收到 insets。

5. 触发流程详解

5.1 触发入口:系统 insets 变化

  • 当系统栏(如状态栏、导航栏、手势区等)发生变化时,Android 框架会自动调用根 View(LauncherRootView)的 onApplyWindowInsets(WindowInsets insets) 方法。

5.2 处理与分发

  • LauncherRootView.onApplyWindowInsets 内部会调用 handleSystemWindowInsets(Rect insets),并最终调用自身的 setInsets(insets)
  • LauncherRootView.setInsets 继承自 InsettableFrameLayout,会遍历所有子 View:
    • 如果子 View 实现了 Insettable,则递归调用其 setInsets 方法。
    • 否则直接调整 margin。

5.3 递归适配

  • 这样,insets 会一层层传递下去,所有实现了 Insettable 的 View 都能收到 insets 并调整自身布局。
  • 例如 AllApps、Hotseat、ScrimView 等会在 setInsets 里根据 insets 设置 margin、padding、可见性等。

5.4 总结流程图

graph TD A[系统栏变化] --> B[LauncherRootView.onApplyWindowInsets] B --> C[handleSystemWindowInsets] C --> D[LauncherRootView.setInsets] D --> E[InsettableFrameLayout.setInsets] E --> F{遍历子View} F -- 实现Insettable --> G[递归调用setInsets] F -- 未实现Insettable --> H[调整margin] G --> F

6. 适配效果

  • 内容区域始终避开系统栏、手势区、刘海区等,保证显示安全
  • 支持全面屏、刘海屏、手势导航等新形态设备
  • 适配逻辑集中、易于维护和扩展

7. 总结

Launcher3 的 insets 适配机制通过 Insettable 接口和分发链路,实现了全局、递归、自动的安全区域适配。只要实现了 Insettable 的 View,都能优雅适配各种系统栏变化,极大提升了兼容性和用户体验。


如需了解具体 View 的 insets 适配细节,可查阅相关类的 setInsets 实现。

相关推荐
qq_5896660512 小时前
TypeScript 完整入门教程
前端·javascript·typescript
用户0595401744616 小时前
LLM对话记忆测试踩坑实录:手工回归30分钟,自动化后2分钟发现3个隐藏Bug
前端·css
Ashley的成长之路17 小时前
前端性能优化实战手册·第2篇:资源加载策略全解
前端·性能优化·资源加载·http/3·性能优化实战·资源加载优化
浅水壁虎17 小时前
vue基础(第二章 )
前端·javascript·vue.js
界面开发小八哥17 小时前
界面控件DevExtreme v26.1新版亮点——支持Angular 22
前端·javascript·angular.js·devexpress·ui开发·devextreme
Getflare17 小时前
前端 + UI 设计 + AI:这不是三个工种,是一个新三角能力模型(附自检清单)
前端·人工智能·ui
oil欧哟17 小时前
我做了一个 Vibe Coding 术语学习站:VibeHub
前端·ai·agent·独立开发·vibe coding
审小匠OpenCPAi18 小时前
银行流水核查怎么自动化?单边匹配、双向勾稽与图聚类异常检测的工程对比
java·前端·人工智能·python·审计
Revolution6118 小时前
一个公共表格组件,是怎么一步步失控的
前端·前端工程化