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

相关推荐
Zoey的笔记本1 分钟前
「软件开发缺陷管理工具」的闭环追踪设计与自动化集成架构
java·服务器·前端
Sapphire~5 分钟前
【前端基础】04-XSS(跨站脚本攻击,Cross-Site Scripting)
前端·xss
奔跑的web.6 分钟前
Vue 3.6 重磅新特性:Vapor Mode 深度解析
前端·javascript·vue.js
MediaTea6 分钟前
Python OOP 设计思想 13:封装服务于演化
linux·服务器·前端·数据库·python
爱敲代码的婷婷婷.8 分钟前
patch-package 修改 node_modules流程以及注意点
前端·react native·前端框架·node.js
这是个栗子12 分钟前
【API封装参数传递】params 与 API 封装
开发语言·前端·javascript·data·params
凌栀茗20 分钟前
html第二天
前端·javascript·html
你脸上有BUG20 分钟前
CSS实现透明内层+渐变边框的优雅方案
前端·css·毛玻璃
Amumu1213829 分钟前
Redux介绍(一)
前端·javascript·react.js
麷飞花32 分钟前
TypeScript问题
前端·javascript·vscode·typescript·ts