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

相关推荐
Aphasia3113 分钟前
react常用hook
前端·react.js·面试
hfd199027 分钟前
Chrome 插件开发实战:从入门到进阶
前端·chrome
菠萝+冰30 分钟前
CSS 定位的核心属性:position
前端·css
文艺理科生44 分钟前
Nuxt 状态管理权威指南:从 useState 到 Pinia
前端·javascript·vue.js
Cache技术分享1 小时前
169. Java Lambda 表达式 - 使用自然顺序比较对象
前端·后端
心宽路阔走天下1 小时前
html页面打水印效果
前端·css·html
piaoyunlive2 小时前
Base64 编码优化 Web 图片加载:异步响应式架构(Java 后端 + 前端全流程实现)
java·前端·架构
wow_DG2 小时前
【React ✨】从零搭建 React 项目:脚手架与工程化实战(2025 版)
前端·react.js·前端框架
秋秋小事2 小时前
React Hooks UseRef的用法
前端·javascript·react.js
yinuo2 小时前
uniapp换肤最佳实践
前端