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

相关推荐
小牛itbull9 分钟前
初始化electron项目运行后报错 electron uninstall 解决方法
前端·javascript·electron
闲蛋小超人笑嘻嘻31 分钟前
前端面试十四之webpack和vite有什么区别
前端·webpack·node.js
rggrgerj1 小时前
Vue3 组件完全指南代码
前端·javascript·vue.js
golang学习记3 小时前
从0死磕全栈之Next.js App Router动态路由详解:从入门到实战
前端
huangql5203 小时前
基于前端+Node.js 的 Markdown 笔记 PDF 导出系统完整实战
前端·笔记·node.js
在逃的吗喽3 小时前
Vue3新变化
前端·javascript·vue.js
yqwang_cn3 小时前
打造优雅的用户体验:自定义jQuery工具提示插件开发全解析
前端·jquery·ux
小Tomkk3 小时前
AI 提效:利用 AI 从前端 快速转型为UI/UX设计师和产品
前端·人工智能·ui
Demoncode_y4 小时前
Vue3中基于路由的动态递归菜单组件实现
前端·javascript·vue.js·学习·递归·菜单组件
杨超越luckly4 小时前
HTML应用指南:利用POST请求获取全国中国工商农业银行网点位置信息
大数据·前端·html·数据可视化·银行网点