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。
- 如果子 View 实现了
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 实现。