26、多窗口模式下的 Window 管理:分屏、自由窗口模式下的窗口调整逻辑
多窗口模式,说白了就是让手机屏幕能同时干几件事。Android 从 7.0 开始引入分屏,到后来自由窗口模式,这中间踩过的坑可不少。我当年做平板适配时,就被分屏下的窗口调整逻辑折磨过好几回。
分屏模式的核心机制
分屏模式下,屏幕被一分为二。系统会创建两个 Task,每个 Task 管理一个应用的窗口栈。这里的关键是 DockedStack——它专门管理被固定在屏幕一侧的窗口。
分屏窗口的层级关系
- DockedStack:固定侧窗口(比如上半屏)
- FullscreenStack:另一侧窗口(下半屏)
- 每个 Stack 内部有自己的 Activity 栈
我记得第一次调试分屏时,发现窗口尺寸变化后,应用居然没收到 onConfigurationChanged。后来才明白,分屏模式下窗口的 Bounds 是由 WindowManagerService 直接计算的,不是应用自己说了算。
窗口调整的触发流程
当用户拖动分屏分割线时,系统会走一套完整的流程:
- 触摸事件捕获:SystemUI 的 DividerView 捕获拖动事件
- WMS 计算新边界:调用
resizeDockedStack方法 - 窗口重布局:WMS 重新计算每个窗口的
Frame - Surface 更新:通过 SurfaceFlinger 刷新显示
// WMS 中的核心调整方法
void resizeDockedStack(Rect dockedBounds, Rect tempBounds) {
synchronized (mWindowMap) {
// 1. 计算有效边界
Rect validBounds = getValidDockedBounds(dockedBounds);
// 2. 调整 DockedStack 的窗口
mDockedStack.resize(validBounds);
// 3. 调整另一侧的窗口
mFullscreenStack.resize(getRemainingBounds(validBounds));
// 4. 通知所有窗口更新
for (WindowState ws : mWindowMap.values()) {
ws.computeFrameLw();
ws.updateSurfacePosition();
}
}
}
避坑指南:我曾经在自定义 Launcher 时,直接修改了 DockedStack 的边界,结果导致分屏窗口闪烁。后来发现必须通过 resizeDockedStack 方法,它会自动处理动画和过渡效果。
自由窗口模式的调整逻辑
自由窗口模式比分屏更灵活。窗口可以任意拖动、缩放,甚至悬浮在其他应用之上。这背后是 FreeformStack 在起作用。
自由窗口的调整逻辑主要分三步:
- 拖动:修改窗口的
mFrame位置,不改变大小 - 缩放:通过四个角和四条边的手势,动态调整
mFrame的宽高 - 最大化/最小化:在自由窗口和全屏之间切换
你想想看,如果窗口被拖到屏幕边缘,系统会怎么处理?嗯,这里有个 SnapTarget 机制——当窗口靠近边缘时,会自动吸附到预设位置。
// 自由窗口的边界约束
private Rect getConstrainedFrame(Rect proposed, int minWidth, int minHeight) {
Rect result = new Rect(proposed);
// 不能小于最小尺寸
if (result.width() < minWidth) {
result.right = result.left + minWidth;
}
if (result.height() < minHeight) {
result.bottom = result.top + minHeight;
}
// 不能超出屏幕
result.left = Math.max(0, result.left);
result.top = Math.max(0, result.top);
result.right = Math.min(mDisplayWidth, result.right);
result.bottom = Math.min(mDisplayHeight, result.bottom);
return result;
}
窗口调整的动画与过渡
直接调整窗口位置会显得很生硬。Android 使用 WindowAnimationSpec 来做平滑过渡。我记得在 Android 10 上,分屏分割线的拖动动画改用了 SpringAnimation,手感好了很多。
| 动画类型 | 触发场景 | 实现方式 |
|---|---|---|
| 分屏分割线拖动 | 用户拖动分割线 | SpringAnimation + 实时计算 Bounds |
| 自由窗口缩放 | 拖拽窗口边缘 | ValueAnimator + 逐帧更新 Frame |
| 窗口进入分屏 | 从最近任务拖入分屏 | Transition 动画 + 窗口重布局 |
多窗口下的焦点管理
多窗口模式下,焦点管理变得复杂。两个窗口同时可见,但只有一个能获得焦点。系统通过 mFocusedApp 和 mFocusedWindow 来追踪。
我曾经遇到过一个 bug:分屏模式下,点击非焦点窗口,焦点切换后键盘弹起,结果把另一个窗口挤没了。后来发现是 ImePolicy 在处理输入法时,没有考虑分屏场景下的窗口边界。
注意:自由窗口模式下,如果窗口获得焦点,它会自动提升 Z-order。这意味着它可能遮挡其他窗口。如果你的应用有悬浮窗功能,一定要处理好 Z-order 的变化。
SVG 流程图:多窗口调整的核心逻辑
实际项目中的坑
我在做车机系统时,遇到过自由窗口模式下窗口无法拖出屏幕的问题。原因是车机的屏幕是圆形的,而 getConstrainedFrame 方法只考虑了矩形边界。后来我们重写了边界检测逻辑,把圆形屏幕的半径考虑进去。
另一个常见问题是分屏模式下 SurfaceView 的渲染异常。因为 SurfaceView 有自己的独立 Surface,分屏调整时它的位置更新不及时,会导致黑边。解决方案是在 onLayout 中强制更新 Surface 位置。
个人建议:如果你要适配多窗口,一定要在 AndroidManifest.xml 中声明 android:resizeableActivity="true"。否则系统会强制你的应用进入兼容模式,窗口调整时会有黑边。
多窗口的窗口调整逻辑,说白了就是 WMS 在管理一堆窗口的边界和层级。理解了这个核心,你就能应对大部分多窗口场景下的问题了。