[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 实现。

相关推荐
Hyyy6 小时前
普通前端续命周报——第1周
前端·javascript
KaMeidebaby7 小时前
卡梅德生物技术快报|抗独特型抗体开发:半抗原检测技术瓶颈拆解,抗独特型抗体开发工程化实践
前端·数据库·人工智能·其他·百度·新浪微博
2501_940041747 小时前
纯前端创意交互:五款全新实用工具与视觉应用生成指南
前端·交互
刀法如飞7 小时前
《道德经》简单解说版-第 2 章:天下皆知美之为美
前端·后端·面试
发现一只大呆瓜9 小时前
超全 Vite 性能优化指南:网络、资源、预渲染三维落地方案
前端·面试·vite
IT_陈寒9 小时前
Vue的computed属性怎么突然不更新了?
前端·人工智能·后端
智商不够_熬夜来凑10 小时前
【Picker】单选多选
前端·javascript·vue.js
米饭不加菜10 小时前
Typora 原生流程图语法完全指南(Flowchart.js)
前端·javascript·流程图
scan72410 小时前
langgraphy条件边
前端·javascript·html
冰小忆11 小时前
类变量在继承场景下的初始化规则是怎样的?
java·前端·数据库