Android分屏功能实现机制深度解析:Task组织与管理

副标题:从WindowContainer到Task Reparenting的完整技术剖析

日期:2025年10月
目标读者:Android Framework开发者
预计阅读时间:25-30分钟


第1章:引言

想象这样一个场景:用户正在使用微信聊天,突然需要参考邮件中的信息,于是长按最近任务中的Gmail图标,拖拽到屏幕顶部,屏幕瞬间一分为二——微信占据下半屏继续显示对话,Gmail在上半屏展开邮件列表。整个过程行云流水,仿佛两个应用天生就该这样并存。

但在这个看似简单的交互背后,Android Framework经历了一系列复杂而精密的内部操作:

  • Task是如何从全屏模式重新组织到分屏模式的?
  • 两个独立的应用如何在同一个屏幕上各自运行,互不干扰?
  • 系统如何确保每个应用都能获得正确的显示边界和配置?
  • 当用户拖动分隔线调整比例时,Task的边界是如何实时更新的?

这些问题的答案,都隐藏在Android分屏功能的核心实现机制中——Task组织与管理系统

为什么关注Task组织机制?

在Android的多窗口体系中,Task是应用运行的基本单元。理解Task的组织机制,就是理解分屏功能的本质:

  1. 架构层面:Task组织反映了WindowManager Shell的设计哲学
  2. 实现层面:Reparenting、Bounds管理等机制是分屏的技术基石
  3. 扩展层面:掌握Task管理对开发自定义多窗口功能至关重要
  4. 调试层面:Task层次结构是排查分屏问题的第一入口

本文价值

本文将深入Android Framework源码,从Task的视角完整剖析分屏功能的实现机制。你将获得:

  • 完整的Task层次结构知识:从DisplayArea到RootTask的组织逻辑
  • 核心的Reparenting机制理解:Task如何在不同容器间迁移
  • 详尽的边界管理分析:Bounds、AppBounds、MaxBounds的作用
  • 实用的调试技巧:如何使用dumpsys追踪Task状态
  • 关键的设计原则:WindowContainerTransaction的原子性保证

让我们开始这段深入Framework核心的技术之旅。

【图1-1:分屏界面示意图】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
用户视角:
┌─────────────────────┐
│ Gmail App │ ← 上半屏
│ (邮件列表界面) │
├─────────────────────┤ ← 分隔线(可拖动)
│ WeChat App │ ← 下半屏
│ (聊天界面) │
└─────────────────────┘

系统视角(Task组织):
TaskDisplayArea
└── SplitScreen RootTask
├── MainStage RootTask (上半屏区域)
│ └── Gmail Task (taskId=123)
└── SideStage RootTask (下半屏区域)
└── WeChat Task (taskId=456)

第2章:核心概念铺垫

在深入分屏的Task组织机制之前,我们需要建立一些核心概念的认知。这些概念构成了理解分屏实现的知识基础。

2.1 WindowContainer体系:Android窗口管理的骨架

Android的窗口管理系统建立在一个树形的容器体系之上,称为WindowContainer体系。这个体系的核心思想是:一切皆容器

WindowContainer的层次关系

1
2
3
4
5
6
DisplayContent (物理显示设备)
└── TaskDisplayArea (Task显示区域,可以有多个)
└── RootTask (根任务,也叫TaskFragment)
└── Task (应用任务)
└── ActivityRecord (Activity实例)
└── WindowState (窗口)

每个层级都是一个WindowContainer,既可以作为容器包含子节点,也可以作为被管理的对象存在于父容器中。这种递归的结构设计带来了极大的灵活性。

WindowContainer的核心接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 简化的WindowContainer核心结构
abstract class WindowContainer<E extends WindowContainer> extends ConfigurationContainer {
// 子容器列表
protected final WindowList<E> mChildren = new WindowList<>();

// 父容器引用
private WindowContainer mParent;

// 窗口配置(边界、窗口模式等)
private final WindowConfiguration mWindowConfiguration = new WindowConfiguration();

/**
* 添加子容器
* @param child 要添加的子容器
* @param index 插入位置(影响Z-order)
*/
void addChild(E child, int index) {
// 添加到子列表
mChildren.add(index, child);
// 设置父引用
child.setParent(this);
// 触发配置继承
child.onParentChanged(mParent, this);
}

/**
* 移除子容器
*/
void removeChild(E child) {
mChildren.remove(child);
child.setParent(null);
}

/**
* 配置变化时的回调
* 配置会从父容器向下传播
*/
@Override
public void onConfigurationChanged(Configuration newParentConfig) {
super.onConfigurationChanged(newParentConfig);
// 通知所有子容器
for (int i = mChildren.size() - 1; i >= 0; i--) {
mChildren.get(i).onConfigurationChanged(mMergedOverrideConfiguration);
}
}
}

关键特性

  • 配置继承:子容器默认继承父容器的WindowConfiguration
  • 配置覆盖:子容器可以覆盖特定配置项(如边界、窗口模式)
  • 变化传播:父容器的配置变化会自动传播到所有子容器

2.2 Task与RootTask:应用运行的载体

Task的定义

Task是Android中应用运行的基本单元,它代表了一组相关Activity的集合。从用户角度看,一个Task对应最近任务列表中的一个卡片;从系统角度看,Task是WindowContainer树中的一个重要节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// Task的核心属性
public class Task extends WindowContainer<WindowContainer> {
// 唯一标识
final int mTaskId;

// 用户ID
final int mUserId;

// 根Activity信息
Intent baseIntent;
ComponentName baseActivity;

// 当前顶部Activity
ActivityRecord topRunningActivity;

// 窗口配置
private final WindowConfiguration mWindowConfiguration;

// Task边界
private Rect mBounds = new Rect();

// 是否可见
boolean mVisible;

// 所属的RootTask
Task getRootTask() {
WindowContainer parent = getParent();
if (parent == null) return this;
Task parentTask = parent.asTask();
return parentTask == null ? this : parentTask.getRootTask();
}
}

RootTask的特殊性

RootTask(根任务)是一种特殊的Task,它不包含Activity,而是作为其他Task的容器存在。可以将RootTask理解为”Task的Task”。

RootTask的作用

  • 作为Task的直接父容器,组织Task的层次结构
  • 定义Task的显示区域和窗口模式
  • 在分屏模式中,每个Stage都有自己的RootTask

普通Task vs RootTask

1
2
3
4
5
6
RootTask (只作为容器)
├── Task A (包含Activity)
│ ├── MainActivity
│ └── DetailActivity
└── Task B (包含Activity)
└── SettingsActivity

2.3 分屏相关的关键组件

Stage:分屏的舞台概念

Stage是分屏模式引入的抽象概念,代表屏幕上的一个独立区域。Android分屏有两个Stage:

  • MainStage(主舞台):通常是用户先打开的应用所在区域
  • SideStage(侧舞台):通常是用户后拖入的应用所在区域

每个Stage内部都有一个RootTask,用于容纳分配到该Stage的所有Task。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Stage的核心结构(简化)
class StageTaskListener implements ShellTaskOrganizer.TaskListener {
// Stage的RootTask信息
protected RunningTaskInfo mRootTaskInfo;

// Stage中的所有子Task
private final SparseArray<RunningTaskInfo> mChildrenTaskInfo = new SparseArray<>();

// Stage的回调监听
private final StageListenerCallbacks mCallbacks;

/**
* 获取Stage中的Task数量
*/
int getChildCount() {
return mChildrenTaskInfo.size();
}

/**
* 判断是否包含指定Task
*/
boolean containsTask(int taskId) {
return mChildrenTaskInfo.contains(taskId);
}
}

StageCoordinator:分屏的总指挥

StageCoordinator是分屏模式的核心协调者,负责管理MainStage和SideStage的生命周期、Task分配、边界计算等所有分屏相关的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// StageCoordinator的核心职责
class StageCoordinator implements SplitScreen {
private final MainStage mMainStage; // 主舞台
private final SideStage mSideStage; // 侧舞台
private final SplitLayout mSplitLayout; // 分屏布局计算器

// 分屏是否激活
// 规则:只有当SideStage包含至少一个Task时,才认为分屏激活
boolean isSplitScreenActive() {
return mSideStage.getChildCount() > 0;
}

// 启动Task到指定Stage
void startTask(int taskId, @SplitPosition int position, ...) {
// position决定Task进入MainStage还是SideStage
}
}

TaskOrganizer:Task的组织者

TaskOrganizer是WindowManager Shell提供的接口,允许系统UI进程(SystemUI)组织和管理Task。它是连接Framework层和Shell层的桥梁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// ShellTaskOrganizer的核心能力
public class ShellTaskOrganizer extends TaskOrganizer {
/**
* 创建RootTask
* @param displayId 显示设备ID
* @param windowingMode 窗口模式
* @param listener Task事件监听器
*/
public RunningTaskInfo createRootTask(int displayId, int windowingMode,
TaskListener listener) {
// 通过ITaskOrganizerController创建RootTask
// 返回新创建的RootTask信息
}

/**
* 应用WindowContainerTransaction事务
*/
public void applyTransaction(WindowContainerTransaction wct) {
// 将事务发送到WindowManagerService执行
}
}

WindowContainerTransaction:原子操作的保证

WindowContainerTransaction(简称WCT)是一个事务容器,用于批量执行多个WindowContainer操作,保证原子性。

1
2
3
4
5
6
7
8
9
10
11
// WindowContainerTransaction的使用示例
WindowContainerTransaction wct = new WindowContainerTransaction();

// 批量操作
wct.reparent(taskToken, newParentToken, true) // 重新父级化
.setBounds(taskToken, newBounds) // 设置边界
.setWindowingMode(taskToken, WINDOWING_MODE_MULTI_WINDOW) // 设置窗口模式
.reorder(taskToken, true); // 调整Z序

// 原子性提交
mTaskOrganizer.applyTransaction(wct);

WCT的核心价值

  • 原子性:要么全部成功,要么全部失败,不会出现中间状态
  • 批量优化:减少Binder调用次数,提高性能
  • 一致性:所有操作在同一个事务中完成,避免状态不一致

2.4 核心概念关系总览

【图2-1:WindowContainer继承体系图】

classDiagram
    ConfigurationContainer <|-- WindowContainer
    WindowContainer <|-- DisplayContent
    WindowContainer <|-- TaskDisplayArea
    WindowContainer <|-- Task
    WindowContainer <|-- ActivityRecord
    
    class ConfigurationContainer {
        +Configuration mConfiguration
        +onConfigurationChanged()
    }
    
    class WindowContainer~E~ {
        +E mParent
        +WindowList~E~ mChildren
        +addChild()
        +removeChild()
        +reparent()
    }
    
    class DisplayContent {
        +int mDisplayId
        +DisplayArea
    }
    
    class TaskDisplayArea {
        +List~Task~ mRootTasks
        +创建和管理RootTask
    }
    
    class Task {
        +TaskInfo
        +WindowConfiguration
        +Task是RootTask的基类
    }
    
    class ActivityRecord {
        +ActivityInfo
        +Intent
        +表示单个Activity
    }
    
    TaskDisplayArea --> Task : 包含
    Task --> ActivityRecord : 包含

【图2-2:分屏模式关键组件关系图】

graph TB
    subgraph SC["StageCoordinator (协调者)"]
        subgraph MS["MainStage"]
            MSR["RootTask"]
            MST1["Task A"]
            MST2["Task B"]
            MSR --> MST1
            MSR --> MST2
        end
        
        subgraph SS["SideStage"]
            SSR["RootTask"]
            SST1["Task C"]
            SST2["Task D"]
            SSR --> SST1
            SSR --> SST2
        end
        
        SL["SplitLayout
(计算两个Stage的边界)"] MS --> SL SS --> SL end STO["ShellTaskOrganizer
(执行Task操作)"] WCT["WindowContainerTransaction
(事务机制)"] WMS["WindowManagerService
(Framework层)"] SC --> STO STO --> WCT WCT --> WMS style SC fill:#e1f5ff style MS fill:#fff4e1 style SS fill:#e8f5e9 style SL fill:#f3e5f5

小结

通过本章,我们建立了理解分屏Task组织机制所需的核心概念:

  1. WindowContainer体系提供了树形的容器架构
  2. Task和RootTask是应用运行和组织的基本单元
  3. Stage将屏幕划分为独立的应用区域
  4. StageCoordinator统筹分屏的所有逻辑
  5. WindowContainerTransaction保证操作的原子性

有了这些基础,我们可以深入分析Task在分屏模式下的具体组织方式了。


第3章:Task层次结构详解

理解了核心概念后,让我们深入探究Task在不同窗口模式下的层次结构,以及这种结构在进入分屏模式时如何发生变化。

3.1 正常模式下的Task组织

在全屏模式下,Task的组织相对简单扁平:

【图3-1:全屏模式下的Task层次结构】

graph TB
    DC["DisplayContent
(displayId=0)"] TDA["TaskDisplayArea (TDA)"] RT1["RootTask
(Home, ACTIVITY_TYPE_HOME)"] LT["Launcher Task"] RT2["RootTask
(App1, ACTIVITY_TYPE_STANDARD)"] WT["WeChat Task
(全屏运行)"] CA["ChatActivity"] CoA["ContactActivity"] RT3["RootTask
(App2, ACTIVITY_TYPE_STANDARD)"] GT["Gmail Task
(后台,不可见)"] MA["MailListActivity"] DC --> TDA TDA --> RT1 TDA --> RT2 TDA --> RT3 RT1 --> LT RT2 --> WT WT --> CA WT --> CoA RT3 --> GT GT --> MA style DC fill:#e1f5ff style TDA fill:#fff4e1 style RT1 fill:#e8f5e9 style RT2 fill:#e8f5e9 style RT3 fill:#e8f5e9 style WT fill:#fff9c4

关键特征

  • 每个应用Task都有自己独立的RootTask作为父容器
  • Task的windowingModeWINDOWING_MODE_FULLSCREEN
  • Task的bounds等于屏幕的bounds
  • 只有前台Task可见,其他Task被隐藏

3.2 分屏模式下的Task层次变化

当进入分屏模式后,Task的组织结构发生了显著变化:

【图3-2:分屏模式下的Task层次结构】

graph TB
    DC["DisplayContent
(displayId=0)"] TDA["TaskDisplayArea (TDA)"] SSR["RootTask
(SplitScreen,
WINDOWING_MODE_FULLSCREEN)
← 分屏根容器"] MSR["MainStage RootTask
(WINDOWING_MODE_MULTI_WINDOW)"] WT["WeChat Task
(bounds=[0,0,1080,1080])"] CA["ChatActivity"] CoA["ContactActivity"] BT["Browser Task
(后台,bounds继承)"] BA["BrowserActivity"] SSTR["SideStage RootTask
(WINDOWING_MODE_MULTI_WINDOW)"] GT["Gmail Task
(bounds=[0,1080,1080,2160])"] MA["MailListActivity"] HRT["RootTask
(Home, ACTIVITY_TYPE_HOME)"] LT["Launcher Task
(被遮挡,不可见)"] DC --> TDA TDA --> SSR TDA --> HRT SSR --> MSR SSR --> SSTR MSR --> WT MSR --> BT WT --> CA WT --> CoA BT --> BA SSTR --> GT GT --> MA HRT --> LT style DC fill:#e1f5ff style TDA fill:#fff4e1 style SSR fill:#ffccbc style MSR fill:#c8e6c9 style SSTR fill:#c8e6c9 style WT fill:#fff9c4 style GT fill:#fff9c4

关键变化

  1. 新增分屏根容器:创建一个single-top的RootTask作为分屏的根容器
  2. 引入Stage层级:MainStage和SideStage成为子容器
  3. Task重新组织:原本独立的Task被reparent到相应的Stage下
  4. 边界分割:每个Stage有自己的bounds,其下的Task继承这些bounds
  5. 窗口模式变化:Stage的windowingMode变为MULTI_WINDOW

3.3 Task的关键属性详解

3.3.1 WindowConfiguration:Task的窗口配置核心

WindowConfiguration是Task最重要的配置类,它决定了Task的显示方式和行为模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
// WindowConfiguration的完整定义
public class WindowConfiguration implements Parcelable, Comparable<WindowConfiguration> {

// ============ 窗口模式定义 ============
/** 未定义,继承父容器 */
public static final int WINDOWING_MODE_UNDEFINED = 0;
/** 全屏模式 */
public static final int WINDOWING_MODE_FULLSCREEN = 1;
/** 画中画模式 */
public static final int WINDOWING_MODE_PINNED = 2;
/** 自由形式窗口模式(如PC的浮动窗口) */
public static final int WINDOWING_MODE_FREEFORM = 5;
/** 多窗口模式(分屏使用) */
public static final int WINDOWING_MODE_MULTI_WINDOW = 6;

// ============ 活动类型定义 ============
/** 未定义活动类型 */
public static final int ACTIVITY_TYPE_UNDEFINED = 0;
/** 标准应用活动 */
public static final int ACTIVITY_TYPE_STANDARD = 1;
/** 桌面活动(Launcher) */
public static final int ACTIVITY_TYPE_HOME = 2;
/** 最近任务活动(Recents) */
public static final int ACTIVITY_TYPE_RECENTS = 3;
/** 助手活动(Google Assistant等) */
public static final int ACTIVITY_TYPE_ASSISTANT = 4;
/** 梦境活动(DreamService) */
public static final int ACTIVITY_TYPE_DREAM = 5;

// ============ 边界配置 ============
/** Task的显示边界(在父容器坐标系中) */
private Rect mBounds = new Rect();

/** 应用可用的边界(扣除系统UI后的区域) */
private Rect mAppBounds;

/** Task可以达到的最大边界 */
private Rect mMaxBounds = new Rect();

// ============ 其他配置 ============
/** 窗口模式 */
private int mWindowingMode = WINDOWING_MODE_UNDEFINED;

/** 活动类型 */
private int mActivityType = ACTIVITY_TYPE_UNDEFINED;

/** 屏幕方向 */
private int mRotation = ROTATION_UNDEFINED;

/** 显示窗口的Token */
private WindowContainerToken mDisplayWindowingMode;

// ============ 核心方法 ============

/**
* 设置边界
* @param rect 新的边界,null表示清除
*/
public void setBounds(Rect rect) {
if (rect == null) {
mBounds.setEmpty();
return;
}
mBounds.set(rect);
}

/**
* 设置应用边界
*/
public void setAppBounds(Rect rect) {
if (rect == null) {
mAppBounds = null;
return;
}
if (mAppBounds == null) {
mAppBounds = new Rect();
}
mAppBounds.set(rect);
}

/**
* 设置窗口模式
*/
public void setWindowingMode(int windowingMode) {
mWindowingMode = windowingMode;
}

/**
* 比较两个配置的差异
* @return 差异掩码
*/
public long diff(WindowConfiguration other, boolean compareUndefined) {
long changes = 0;

if (!mBounds.equals(other.mBounds)) {
changes |= WINDOW_CONFIG_BOUNDS;
}

if (mWindowingMode != other.mWindowingMode) {
changes |= WINDOW_CONFIG_WINDOWING_MODE;
}

// ... 其他字段比较

return changes;
}
}

三种边界的区别

边界类型 含义 使用场景
mBounds Task在屏幕上的显示区域 分屏时,主舞台的bounds可能是[0,0,1080,1080]
mAppBounds 应用实际可用的绘制区域(扣除系统栏) 如果有状态栏,appBounds可能是[0,75,1080,1080]
mMaxBounds Task可以扩展到的最大区域 旋转屏幕、折叠屏展开时使用

3.3.2 TaskInfo:Task的完整信息描述

TaskInfo是Task信息的对外表示,通过Binder传递给SystemUI进程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
// ActivityManager.RunningTaskInfo的关键字段
public class RunningTaskInfo extends TaskInfo {
/**
* Task的唯一标识符
*/
public int taskId;

/**
* Task是否正在运行
*/
public boolean isRunning;

/**
* Task是否可见
*/
public boolean isVisible;

/**
* Task的根Activity信息
*/
public Intent baseIntent;
public ComponentName baseActivity;

/**
* 当前顶部的Activity
*/
public ComponentName topActivity;

/**
* 顶部Activity是否可见
*/
public boolean topActivityVisible;

/**
* Task的窗口配置(最重要!)
*/
public final WindowConfiguration configuration = new WindowConfiguration();

/**
* Task的WindowContainerToken(用于事务操作)
*/
public WindowContainerToken token;

/**
* Task的显示边界(快捷访问,实际值在configuration中)
*/
public Rect bounds = new Rect();

/**
* Task所属的显示设备ID
*/
public int displayId;

/**
* Task所属的用户ID
*/
public int userId;

/**
* 父Task的ID(如果有)
*/
public int parentTaskId = INVALID_TASK_ID;

/**
* Task是否支持分屏
*/
public boolean supportsMultiWindow;

/**
* Task的窗口模式(快捷访问)
*/
public int getWindowingMode() {
return configuration.windowingMode;
}

/**
* Task的活动类型(快捷访问)
*/
public int getActivityType() {
return configuration.activityType;
}
}

TaskInfo在分屏中的应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// StageTaskListener接收Task信息变化
@Override
public void onTaskInfoChanged(RunningTaskInfo taskInfo) {
// 检查Task是否可见
if (!taskInfo.isVisible) {
// 不可见的Task可能需要被移除
}

// 检查Task边界是否变化
if (!mChildrenTaskInfo.get(taskInfo.taskId).bounds.equals(taskInfo.bounds)) {
// 边界变化,可能需要通知应用重新布局
}

// 更新缓存
mChildrenTaskInfo.put(taskInfo.taskId, taskInfo);
}

3.4 层次结构的设计原则

Android分屏的Task层次结构设计遵循几个重要原则:

原则1:配置继承与覆盖

子Task默认继承父容器的WindowConfiguration,但可以选择性覆盖某些属性:

1
2
3
4
5
6
7
// Stage的RootTask设置边界和窗口模式
wct.setBounds(stageRootToken, new Rect(0, 0, 1080, 1080))
.setWindowingMode(stageRootToken, WINDOWING_MODE_MULTI_WINDOW);

// 子Task清除自己的配置,完全继承父容器
wct.setBounds(childTaskToken, null) // null表示继承
.setWindowingMode(childTaskToken, WINDOWING_MODE_UNDEFINED); // UNDEFINED表示继承

为什么这样设计?

  • 简化管理:只需设置Stage的配置,所有子Task自动生效
  • 动态调整:拖动分隔线时,只需更新Stage边界,子Task自动更新
  • 避免冲突:子Task不保存自己的边界,不会与父容器冲突

原则2:Single-Top根容器

分屏的根RootTask使用single-top模式,保证MainStage和SideStage在同一个父容器下:

1
2
3
4
5
/**
* StageCoordinator注释中的设计规则:
* - Both stages are put under a single-top root task.
* - This ensures they share the same parent and can be managed together.
*/

好处

  • 统一管理:两个Stage作为兄弟节点,方便协调
  • 事件分发:触摸事件可以正确分发到两个Stage
  • 动画同步:进入/退出分屏的动画可以同步执行

原则3:激活条件控制

分屏的激活遵循明确的规则:

1
2
3
4
5
6
7
8
9
10
11
12
// StageCoordinator的激活条件
/**
* Some high-level rules:
* - The StageCoordinator is only considered active if the SideStage
* contains at least one child task.
* - The MainStage should only have children if the coordinator is active.
* - The SplitLayout divider is only visible if both the MainStage and
* SideStage are visible.
*/
boolean isSplitScreenActive() {
return mSideStage.getChildCount() > 0;
}

为什么SideStage优先?

  • 用户意图明确:拖入SideStage是主动进入分屏的信号
  • 避免误触发:防止MainStage自动吸收Task导致意外进入分屏
  • 退出简洁:当SideStage清空时,自动退出分屏,逻辑清晰

小结

本章深入分析了Task的层次结构:

  1. 全屏到分屏的结构演变:从扁平到分层
  2. WindowConfiguration:Task配置的核心数据结构
  3. TaskInfo:Task信息的对外表示
  4. 设计原则:配置继承、single-top容器、激活条件控制

下一章,我们将探究Task如何通过Reparenting机制在不同容器间迁移,这是分屏组织的核心技术。


第4章:Task重新组织机制

Task在分屏中的重新组织是通过**Reparenting(重新父级化)**机制实现的。这是Android多窗口体系中最核心的技术之一。

4.1 Reparenting机制的核心思想

什么是Task Reparenting?

Reparenting字面意思是”重新指定父级”,在WindowContainer体系中,指的是将一个Task从当前的父容器移动到另一个父容器的操作。

类比理解

1
2
3
4
就像孩子从一个班级转到另一个班级:
- 孩子本身(Task)没有变化
- 但所属的班级(Parent Container)变了
- 孩子需要遵守新班级的规则(继承新父容器的配置)

技术层面的变化

1
2
3
4
5
6
7
8
9
10
// Reparenting前
TDA
└── RootTask A (全屏)
└── Task X (WeChat)

// Reparenting后
TDA
└── SplitScreen RootTask
└── MainStage RootTask
└── Task X (WeChat) ← Task X的父容器变了

为什么需要Reparenting?

场景1:进入分屏

  • 用户拖动Task进入分屏区域
  • Task需要从独立的RootTask移动到Stage的RootTask下
  • 移动后,Task自动继承Stage的边界和窗口模式

场景2:退出分屏

  • 用户关闭分屏模式
  • Task需要从Stage的RootTask移回独立的RootTask
  • 恢复全屏显示

场景3:Stage间切换

  • 用户将Task从MainStage拖到SideStage
  • Task需要在两个Stage的RootTask间移动
  • 边界随之改变

核心价值

  • 无需重启应用:Task迁移过程中,应用保持运行状态
  • 配置自动继承:Task自动适应新父容器的配置
  • 状态完整保留:Activity栈、返回栈等状态完整保留

4.2 WindowContainerTransaction事务机制

在深入Reparenting实现前,我们需要理解WindowContainerTransaction(WCT)这个关键的事务机制。

为什么需要事务机制?

设想一个场景:进入分屏需要执行以下操作:

  1. 创建Stage的RootTask
  2. 将Task A reparent到MainStage
  3. 设置Task A的边界为上半屏
  4. 设置Task A的窗口模式为MULTI_WINDOW
  5. 将Task B reparent到SideStage
  6. 设置Task B的边界为下半屏
  7. 显示分隔线

如果这些操作逐个执行,用户会看到:

  • Task A先移动,边界还是全屏(闪烁)
  • 然后Task A边界变化(跳变)
  • Task B出现(延迟)
  • 整个过程不流畅,甚至可能出现中间状态错误

WindowContainerTransaction解决了这个问题:将多个操作打包成一个原子事务,要么全部成功,要么全部失败。

WindowContainerTransaction的核心实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
// WindowContainerTransaction的完整结构
public final class WindowContainerTransaction implements Parcelable {

// 存储所有容器变更操作
private final ArrayMap<IBinder, Change> mChanges = new ArrayMap<>();

// 存储所有层次结构操作
private final ArrayList<HierarchyOp> mHierarchyOps = new ArrayList<>();

/**
* Change: 单个容器的配置变更
*/
public static class Change {
public static final int CHANGE_BOUNDS = 1;
public static final int CHANGE_WINDOWING_MODE = 1 << 1;
public static final int CHANGE_FOCUSABLE = 1 << 2;
public static final int CHANGE_HIDDEN = 1 << 3;
// ... 更多变更类型

private int mChangeMask = 0; // 标记哪些属性被修改
private Rect mBounds; // 新的边界
private int mWindowingMode; // 新的窗口模式
private boolean mHidden; // 是否隐藏
// ... 更多属性
}

/**
* HierarchyOp: 层次结构操作
*/
public static class HierarchyOp {
public static final int HIERARCHY_OP_TYPE_REPARENT = 0;
public static final int HIERARCHY_OP_TYPE_REORDER = 1;
public static final int HIERARCHY_OP_TYPE_CHILDREN_TASKS_REPARENT = 2;
// ... 更多操作类型

private int mType; // 操作类型
private IBinder mContainer; // 目标容器
private IBinder mReparent; // reparent的新父容器
private boolean mToTop; // 是否移到顶部
private int[] mWindowingModes; // 筛选条件:窗口模式
private int[] mActivityTypes; // 筛选条件:活动类型
// ... 更多参数
}

// ============ 核心API ============

/**
* 重新父级化单个容器
* @param child 要移动的子容器
* @param newParent 新的父容器,null表示移到TDA根部
* @param onTop 是否放在新父容器的顶部
*/
@NonNull
public WindowContainerTransaction reparent(
@NonNull WindowContainerToken child,
@Nullable WindowContainerToken newParent,
boolean onTop) {

HierarchyOp hop = new HierarchyOp();
hop.mType = HierarchyOp.HIERARCHY_OP_TYPE_REPARENT;
hop.mContainer = child.asBinder();
hop.mReparent = newParent != null ? newParent.asBinder() : null;
hop.mToTop = onTop;
mHierarchyOps.add(hop);

return this; // 链式调用
}

/**
* 批量重新父级化符合条件的所有Task
* @param currentParent 当前父容器,null表示所有
* @param newParent 新的父容器
* @param windowingModes 筛选:只reparent这些窗口模式的Task
* @param activityTypes 筛选:只reparent这些活动类型的Task
* @param onTop 是否放在顶部
* @param reparentTopOnly 是否只reparent最顶部的Task
*/
@NonNull
public WindowContainerTransaction reparentTasks(
@Nullable WindowContainerToken currentParent,
@NonNull WindowContainerToken newParent,
@Nullable int[] windowingModes,
@Nullable int[] activityTypes,
boolean onTop,
boolean reparentTopOnly) {

HierarchyOp hop = new HierarchyOp();
hop.mType = HierarchyOp.HIERARCHY_OP_TYPE_CHILDREN_TASKS_REPARENT;
hop.mContainer = currentParent != null ? currentParent.asBinder() : null;
hop.mReparent = newParent.asBinder();
hop.mWindowingModes = windowingModes != null ? windowingModes.clone() : null;
hop.mActivityTypes = activityTypes != null ? activityTypes.clone() : null;
hop.mToTop = onTop;
hop.mReparentTopOnly = reparentTopOnly;
mHierarchyOps.add(hop);

return this;
}

/**
* 设置容器边界
* @param container 目标容器
* @param bounds 新的边界,null表示清除(继承父容器)
*/
@NonNull
public WindowContainerTransaction setBounds(
@NonNull WindowContainerToken container,
@Nullable Rect bounds) {

Change chg = getOrCreateChange(container.asBinder());
if (bounds != null) {
chg.mBounds = new Rect(bounds);
} else {
chg.mBounds = null; // null表示继承
}
chg.mChangeMask |= Change.CHANGE_BOUNDS;

return this;
}

/**
* 设置应用边界
*/
@NonNull
public WindowContainerTransaction setAppBounds(
@NonNull WindowContainerToken container,
@Nullable Rect appBounds) {

Change chg = getOrCreateChange(container.asBinder());
if (appBounds != null) {
chg.mAppBounds = new Rect(appBounds);
} else {
chg.mAppBounds = null;
}
chg.mChangeMask |= Change.CHANGE_APP_BOUNDS;

return this;
}

/**
* 设置窗口模式
*/
@NonNull
public WindowContainerTransaction setWindowingMode(
@NonNull WindowContainerToken container,
@WindowingMode int windowingMode) {

Change chg = getOrCreateChange(container.asBinder());
chg.mWindowingMode = windowingMode;
chg.mChangeMask |= Change.CHANGE_WINDOWING_MODE;

return this;
}

/**
* 重新排序容器(调整Z-order)
*/
@NonNull
public WindowContainerTransaction reorder(
@NonNull WindowContainerToken container,
boolean onTop) {

HierarchyOp hop = new HierarchyOp();
hop.mType = HierarchyOp.HIERARCHY_OP_TYPE_REORDER;
hop.mContainer = container.asBinder();
hop.mToTop = onTop;
mHierarchyOps.add(hop);

return this;
}

/**
* 获取或创建容器的Change对象
*/
private Change getOrCreateChange(IBinder token) {
Change chg = mChanges.get(token);
if (chg == null) {
chg = new Change();
mChanges.put(token, chg);
}
return chg;
}
}

WindowContainerTransaction的使用模式

模式1:链式构建事务

1
2
3
4
5
6
7
8
9
10
11
// 一次性构建复杂的事务
WindowContainerTransaction wct = new WindowContainerTransaction();

wct.reparent(taskToken, stageRootToken, true) // 移动Task
.setBounds(taskToken, null) // 清除Task边界(继承)
.setAppBounds(taskToken, null) // 清除应用边界
.setWindowingMode(taskToken, WINDOWING_MODE_UNDEFINED) // 清除窗口模式
.reorder(taskToken, true); // 移到顶部

// 原子提交
mTaskOrganizer.applyTransaction(wct);

模式2:批量操作

1
2
3
4
5
6
7
8
9
10
11
12
13
// 一次reparent多个Task
WindowContainerTransaction wct = new WindowContainerTransaction();

wct.reparentTasks(
null, // 从所有父容器中筛选
stageRootToken, // 移动到Stage的RootTask
new int[]{WINDOWING_MODE_FULLSCREEN, WINDOWING_MODE_MULTI_WINDOW}, // 只选这些模式
new int[]{ACTIVITY_TYPE_STANDARD}, // 只选标准应用
true, // 放在顶部
false // reparent所有符合条件的Task,不仅仅是顶部
);

mTaskOrganizer.applyTransaction(wct);

模式3:事务复用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 同一个事务对象可以操作多个容器
WindowContainerTransaction wct = new WindowContainerTransaction();

// 操作MainStage
wct.setBounds(mainStageToken, bounds1)
.setAppBounds(mainStageToken, bounds1);

// 操作SideStage
wct.setBounds(sideStageToken, bounds2)
.setAppBounds(sideStageToken, bounds2);

// 操作分隔线
wct.setBounds(dividerToken, dividerBounds);

// 一次性提交所有变更
mTaskOrganizer.applyTransaction(wct);

【图4-1:WindowContainerTransaction执行流程】

sequenceDiagram
    participant UI as SystemUI (Shell进程)
    participant TO as TaskOrganizer
    participant IPC as Binder IPC
    participant WMS as WindowManagerService (System进程)
    participant WC as WindowContainer
    
    Note over UI: 构建WCT事务
    UI->>UI: reparent(Task A → MainStage)
    UI->>UI: setBounds(MainStage, [0,0,1080,1080])
    UI->>UI: setBounds(SideStage, [0,1080,1080,2160])
    
    UI->>TO: applyTransaction(wct)
    TO->>IPC: Binder调用
    IPC->>WMS: 传输WCT数据
    
    Note over WMS: 验证事务合法性
    WMS->>WMS: 检查权限
    WMS->>WMS: 检查容器存在性
    
    Note over WMS: 开始事务
    WMS->>WMS: beginTransaction()
    
    Note over WMS: 执行所有HierarchyOps(按顺序)
    WMS->>WC: Task.reparent()
    Note right of WC: 修改容器树结构
    
    Note over WMS: 应用所有Changes(批量)
    WMS->>WC: WindowContainer.onConfigurationChanged()
    Note right of WC: 更新配置
    
    Note over WMS: 提交事务
    WMS->>WMS: commitTransaction()
    WMS->>WMS: 触发Layout
    WMS->>WMS: 触发Surface更新
    WMS->>WMS: 通知应用配置变化
    
    WMS->>TO: 回调TaskOrganizer通知结果
    TO->>UI: 操作完成

4.3 Task Reparenting核心实现

有了WCT的基础,让我们深入Task.reparent()的实现。

Framework层的reparent()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
// Task.java (Framework层)
/**
* 将Task重新父级化到新的RootTask
*
* @param preferredRootTask 首选的目标RootTask
* @param position 在目标RootTask中的位置(MAX_VALUE表示顶部)
* @param moveRootTaskMode 是否移动RootTask到前台
* @param animate 是否播放动画
* @param deferResume 是否延迟恢复
* @param schedulePictureInPictureModeChange 是否调度PIP模式变化
* @param reason reparent的原因(用于日志)
* @return 是否成功reparent
*/
boolean reparent(Task preferredRootTask, int position,
@ReparentMoveRootTaskMode int moveRootTaskMode,
boolean animate, boolean deferResume,
boolean schedulePictureInPictureModeChange,
String reason) {

// 1. 获取当前所属的RootTask
final Task sourceRootTask = getRootTask();

// 2. 获取目标RootTask
// 可能会根据Task的属性和系统状态选择最合适的RootTask
final ActivityTaskSupervisor supervisor = mAtmService.mTaskSupervisor;
final Task toRootTask = supervisor.getReparentTargetRootTask(
this, preferredRootTask, position == MAX_VALUE);

// 3. 检查是否已经在目标RootTask中
if (toRootTask == sourceRootTask) {
return false; // 无需移动
}

// 4. 记录日志
ProtoLog.i(WM_DEBUG_TASKS, "reparent: moving taskId=%d from rootTask=%d to rootTask=%d "
+ "at position=%d reason=%s",
mTaskId, sourceRootTask.mTaskId, toRootTask.mTaskId, position, reason);

// 5. 确定是否需要将目标RootTask移到前台
final boolean moveRootTaskToFront = moveRootTaskMode == REPARENT_MOVE_ROOT_TASK_TO_FRONT
|| (moveRootTaskMode == REPARENT_KEEP_ROOT_TASK_AT_FRONT
&& (sourceRootTask.isTopRootTaskInDisplayArea()
|| toRootTask == preferredRootTask));

// 6. 执行核心的reparent操作
// 这会调用WindowContainer的reparent方法,修改容器树结构
reparent(toRootTask, position, moveRootTaskToFront, reason);

// 7. 如果需要,将目标RootTask移到前台
if (moveRootTaskToFront) {
toRootTask.moveToFront(reason);

// 如果有Activity处于resumed状态,通知它可能需要重新resume
if (getTopResumedActivity() != null) {
supervisor.scheduleUpdatePictureInPictureModeIfNeeded(this, sourceRootTask);
}
} else if (animate) {
// 播放动画
toRootTask.mTransitionController.requestTransitionIfNeeded(...);
}

// 8. 处理延迟恢复
if (!deferResume) {
supervisor.ensureActivitiesVisible(null, 0, PRESERVE_WINDOWS);
supervisor.resumeFocusedTasksTopActivities();
}

return true;
}

/**
* 核心的reparent实现(WindowContainer层)
*/
@Override
void reparent(WindowContainer newParent, int position, boolean moveParents, String reason) {
if (newParent == null) {
throw new IllegalArgumentException("reparent: can't reparent to null");
}

if (getParent() == newParent) {
throw new IllegalArgumentException("Task=" + this + " already child of newParent=" + newParent);
}

// 同步块保证线程安全
synchronized (mWmService.mGlobalLock) {
// 从当前父容器移除
final WindowContainer oldParent = getParent();
if (oldParent != null) {
oldParent.removeChild(this);
}

// 添加到新父容器
newParent.addChild(this, position);

// 触发父容器变化回调
// 这会导致配置重新计算和传播
onParentChanged(oldParent, newParent);

// 通知显示内容变化
getDisplayContent().setLayoutNeeded();
getDisplayContent().getDisplayPolicy().adjustWindowsForEnsureVisible();

// 请求执行Layout
mWmService.scheduleAnimationLocked();
}
}

/**
* 父容器变化时的回调
* 这里会触发配置的重新计算
*/
@Override
void onParentChanged(ConfigurationContainer oldParent, ConfigurationContainer newParent) {
super.onParentChanged(oldParent, newParent);

// 更新RootTask引用
mRootTask = null;

if (newParent != null) {
// 从新父容器继承配置
// 如果Task自己设置了bounds,会被清除
// 然后自动继承新父容器的bounds
onConfigurationChanged(newParent.getConfiguration());

// 更新显示设备引用
updateDisplayInfo(newParent.getDisplayContent());
}

// 通知所有子Activity配置变化
for (int i = mChildren.size() - 1; i >= 0; --i) {
final ActivityRecord activity = (ActivityRecord) mChildren.get(i);
activity.onParentChanged(oldParent, newParent);
}
}

Reparent的核心步骤总结

  1. 验证目标:确定实际的目标RootTask
  2. 检查必要性:如果已在目标中,直接返回
  3. 修改树结构:从旧父容器移除,添加到新父容器
  4. 配置重算:触发onParentChanged,重新计算WindowConfiguration
  5. 配置传播:新配置向下传播到所有子Activity
  6. 通知应用:发送配置变化回调给应用进程
  7. 更新显示:请求重新layout和绘制

4.4 Shell层的Task组织操作

在WindowManager Shell中,StageTaskListener使用WCT来组织Task。

添加Task到Stage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// StageTaskListener.java
/**
* 将Task添加到Stage中
*
* @param task 要添加的Task信息
* @param wct 事务对象
*/
void addTask(ActivityManager.RunningTaskInfo task, WindowContainerTransaction wct) {
ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "StageTaskListener.addTask: taskId=%d", task.taskId);

// 关键操作:清除Task自身的配置覆盖,让它继承父容器(Stage RootTask)的配置
wct.setWindowingMode(task.token, WINDOWING_MODE_UNDEFINED) // 窗口模式继承
.setBounds(task.token, null); // 边界继承

// 将Task reparent到Stage的RootTask下,放在顶部
wct.reparent(task.token, mRootTaskInfo.token, true /* onTop*/);
}

为什么要清除Task的配置?

  • Task之前可能是全屏模式,有自己的bounds和windowingMode
  • 清除后,Task自动继承Stage的配置
  • 这样当拖动分隔线调整Stage边界时,Task自动跟随变化

批量重组所有符合条件的Task

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// StageTaskListener.java
/**
* 重新父级化所有符合条件的Task到当前Stage
* 通常在Stage激活时调用,吸收所有可以进入分屏的Task
*/
void reparentAllEligibleTasks(WindowContainerTransaction wct) {
ProtoLog.d(WM_SHELL_SPLIT_SCREEN,
"StageTaskListener.reparentAllEligibleTasks: starting reparent operation");

// 使用批量reparent操作
wct.reparentTasks(
null, // currentParent为null,表示从整个系统中筛选
mRootTaskInfo.token, // 目标:当前Stage的RootTask
CONTROLLED_WINDOWING_MODES, // 只reparent全屏和多窗口模式的Task
CONTROLLED_ACTIVITY_TYPES, // 只reparent标准应用类型的Task
true, // onTop:放在顶部
false // reparentTopOnly:false表示reparent所有符合条件的,不仅仅是顶部Task
);
}

// SplitScreenConstants.java
/** 受控的窗口模式:只有这些模式的Task才能进入分屏 */
public static final int[] CONTROLLED_WINDOWING_MODES = {
WINDOWING_MODE_FULLSCREEN, // 全屏模式
WINDOWING_MODE_MULTI_WINDOW // 多窗口模式
};

/** 受控的活动类型:只有这些类型的Task才能进入分屏 */
public static final int[] CONTROLLED_ACTIVITY_TYPES = {
ACTIVITY_TYPE_STANDARD, // 标准应用
ACTIVITY_TYPE_UNDEFINED // 未定义类型(继承父容器)
};

为什么需要筛选条件?

  • 排除系统UI:Home、Recents等不应该进入分屏
  • 排除特殊模式:PIP模式的Task不能进入分屏
  • 确保兼容性:只有明确支持多窗口的Task才能进入

【图4-2:reparentAllEligibleTasks的筛选逻辑】

flowchart TB
    Start([系统中的所有Task])
    
    HT["Home Task
(ACTIVITY_TYPE_HOME)"] RT["Recents Task
(ACTIVITY_TYPE_RECENTS)"] PT["PIP Task
(WINDOWING_MODE_PINNED)"] WT["WeChat Task
(STANDARD, FULLSCREEN)"] BT["Browser Task
(STANDARD, FULLSCREEN)"] GT["Gmail Task
(STANDARD, MULTI_WINDOW)"] Filter{"筛选条件
windowingMode in
[FULLSCREEN, MULTI_WINDOW]
AND
activityType in
[STANDARD, UNDEFINED]"} Excluded([✗ 被排除]) ToStage([✓ reparent到Stage]) Start --> HT Start --> RT Start --> PT Start --> WT Start --> BT Start --> GT HT --> Filter RT --> Filter PT --> Filter WT --> Filter BT --> Filter GT --> Filter Filter -- "不符合" --> HT Filter -- "不符合" --> RT Filter -- "不符合" --> PT HT --> Excluded RT --> Excluded PT --> Excluded Filter -- "符合条件" --> WT Filter -- "符合条件" --> BT Filter -- "符合条件" --> GT WT --> ToStage BT --> ToStage GT --> ToStage style HT fill:#ffcdd2 style RT fill:#ffcdd2 style PT fill:#ffcdd2 style WT fill:#c8e6c9 style BT fill:#c8e6c9 style GT fill:#c8e6c9 style Excluded fill:#ffcdd2 style ToStage fill:#c8e6c9

4.5 实际分屏场景的Reparenting流程

让我们通过一个完整场景,串联Reparenting的全过程。

场景:用户将Gmail从最近任务拖入分屏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
// 步骤1:用户开始拖动(在RecentsTasks中)
// Launcher检测到拖动目标是分屏区域,调用StageCoordinator

// 步骤2:StageCoordinator准备进入分屏
void startTask(int taskId, @SplitPosition int position, Bundle options) {
WindowContainerTransaction wct = new WindowContainerTransaction();

// 2.1 激活分屏布局
prepareEnterSplitScreen(wct);

// 2.2 确定目标Stage
final StageTaskListener targetStage = position == SPLIT_POSITION_TOP_OR_LEFT
? mMainStage : mSideStage;

// 2.3 准备Task启动选项
options.putInt(KEY_LAUNCH_ROOT_TASK_TOKEN, targetStage.mRootTaskInfo.token);

// 2.4 启动Task
mTaskOrganizer.startTask(taskId, options);

// 提交事务
mSyncQueue.queue(wct);
mSyncQueue.runInSync(t -> {
// 同步Surface变换(动画)
updateSurfaceBounds(null, t, false);
});
}

// 步骤3:prepareEnterSplitScreen准备环境
void prepareEnterSplitScreen(WindowContainerTransaction wct) {
// 3.1 如果已经激活,无需准备
if (isSplitScreenActive()) {
return;
}

// 3.2 设置MainStage为激活状态
mMainStage.activate(wct, false);

// 3.3 设置SideStage为激活状态
mSideStage.activate(wct, false);

// 3.4 更新分屏边界
updateWindowBounds(mSplitLayout, wct);

// 3.5 显示分隔线
mSplitLayout.update(wct);
}

// 步骤4:Stage.activate激活舞台
// MainStage.java / SideStage.java
void activate(WindowContainerTransaction wct, boolean reparentAllTasks) {
// 4.1 如果需要,批量reparent已有的Task
if (reparentAllTasks) {
reparentAllEligibleTasks(wct);
}

// 4.2 设置RootTask可见
wct.setHidden(mRootTaskInfo.token, false);

// 4.3 设置窗口模式为MULTI_WINDOW
wct.setWindowingMode(mRootTaskInfo.token, WINDOWING_MODE_MULTI_WINDOW);

// 4.4 重新排序到顶部
wct.reorder(mRootTaskInfo.token, true /* onTop */);
}

// 步骤5:Task被启动到Stage中
// Framework层会处理Task的创建或恢复,并自动reparent到指定的RootTask

// 步骤6:Task继承Stage的配置
// Task.onParentChanged被调用
// → Task.onConfigurationChanged被调用
// → 继承Stage的bounds和windowingMode
// → 通知应用进程配置变化(通过ActivityThread)

// 步骤7:应用响应配置变化
// Application进程:
// → ActivityThread.handleConfigurationChanged()
// → Activity.onConfigurationChanged()
// → 重新计算布局、重新绘制

【图4-3:完整的分屏启动流程时序图(含动画与Surface管理)】

sequenceDiagram
    participant UI as SystemUI
    participant SC as StageCoordinator
    participant MS as MainStage
    participant SS as SideStage
    participant WMS as WindowManagerService
    participant SF as SurfaceFlinger
    participant App1 as EZWRITE
    participant App2 as Maps

    Note over UI,App2: 阶段1: 启动准备 (Preparation Phase)
    
    UI->>SC: handleRequest(enter split)
    SC->>SC: prepareEnterSplitScreen()
    
    SC->>MS: activate(includeTopTask=true)
    MS->>MS: reparentTopTask()
    MS-->>SC: 主舞台激活完成
    
    SC->>SC: updateWindowBounds()
    Note right of SC: 计算Stage边界
Main: [0,0,1080,1200]
Side: [0,1200,1080,2400] Note over UI,App2: 阶段2: 过渡动画 (Transition Animation Phase) WMS->>SC: startPendingAnimation() SC->>SC: 分析过渡变化 Note right of SC: • 识别主舞台任务
• 识别侧舞台任务 SC->>SF: 创建分割线Surface SC->>SC: setDividerVisibility(true) SC->>SC: playInternalAnimation() Note right of SC: • Task位置动画
• 边界缩放动画
• 透明度渐变 Note over UI,App2: 阶段3: 任务同步 (Task Synchronization Phase) App1->>SC: onTaskAppeared(taskId=69) Note right of SC: 缓存Task信息 App2->>SC: onTaskAppeared(taskId=70) Note right of SC: 缓存Task信息 SC->>SC: updateRecentTasksSplitPair() Note right of SC: 记录分屏组合
[69 <-> 70] SC->>SC: onTransitionAnimationComplete() Note right of SC: • 清理过渡状态
• 确认分屏激活
• 通知监听器 Note over UI,App2: 阶段4: 分割线调整 (Divider Adjustment Phase) SC->>SC: onLayoutSizeChanged() SC->>SC: updateWindowBounds() SC->>WMS: 创建调整事务 WMS->>SC: playResizeAnimation() SC->>SC: onFinish(resize) SC->>SF: updateSurfaceBounds() Note right of SF: • MainStage Surface
• SideStage Surface
• Divider Surface Note over UI,App2: 分屏启动完成 - 用户可正常交互

时序图关键阶段说明

阶段1:启动准备(Preparation)

  • SystemUI接收用户操作,触发分屏请求
  • StageCoordinator调用prepareEnterSplitScreen()准备环境
  • MainStage激活并重新组织顶部Task(reparentTopTask)
  • 计算并设置初始的窗口边界

阶段2:过渡动画(Transition Animation)

  • WMS触发startPendingAnimation()开始过渡
  • StageCoordinator分析需要动画的变化:识别主舞台和侧舞台的Task
  • 在SurfaceFlinger中创建分割线Surface
  • 设置分割线可见性并播放内部动画(playInternalAnimation)

阶段3:任务同步(Task Synchronization)

  • 两个应用的Task通过onTaskAppeared回调通知StageCoordinator
  • 更新最近任务列表的分屏配对信息(updateRecentTasksSplitPair)
  • 过渡动画完成回调(onTransitionAnimationComplete)

阶段4:分割线调整(Divider Adjustment)

  • 监听布局尺寸变化(onLayoutSizeChanged)
  • 重新计算窗口边界(updateWindowBounds)
  • 创建调整事务并播放调整动画(playResizeAnimation)
  • 完成后更新Surface边界(updateSurfaceBounds)

小结

本章深入剖析了Task重新组织的核心机制:

  1. Reparenting概念:Task在容器间迁移,保持运行状态
  2. WindowContainerTransaction:原子事务机制,保证操作一致性
  3. Framework实现:Task.reparent()修改容器树,触发配置重算
  4. Shell层应用:StageTaskListener使用WCT组织Task
  5. 完整流程:从用户操作到应用响应的全链路

下一章,我们将分析分屏Task组织的完整生命周期,从创建到销毁的全过程。


第5章:分屏Task组织完整流程

理解了Reparenting机制后,让我们从头到尾追踪一个完整的分屏流程,看看Task组织的各个环节是如何协作的。

5.1 分屏环境初始化

StageCoordinator的创建

分屏功能的初始化发生在SystemUI启动时,SplitScreenController创建Stage Coordinator。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// StageCoordinator.java
/**
* StageCoordinator构造函数
* 这是分屏功能的核心协调者
*/
protected StageCoordinator(Context context, int displayId,
SyncTransactionQueue syncQueue,
ShellTaskOrganizer taskOrganizer,
DisplayController displayController,
DisplayImeController imeController,
DisplayInsetsController insetsController,
SplitLayout splitLayout,
Transitions transitions,
TransactionPool transactionPool,
IconProvider iconProvider,
ShellExecutor mainExecutor,
Optional<RecentTasksController> recentTasksOptional) {

mContext = context;
mDisplayId = displayId;
mSyncQueue = syncQueue;
mTaskOrganizer = taskOrganizer;

// 1. 创建分屏根任务
// windowingMode为FULLSCREEN,因为它是整个分屏容器
// 其子容器(Stage)才是MULTI_WINDOW模式
taskOrganizer.createRootTask(displayId, WINDOWING_MODE_FULLSCREEN, this /* listener */);

// 2. 创建主舞台(MainStage)
mMainStage = new MainStage(
context,
mTaskOrganizer,
mDisplayId,
mMainStageListener, // 监听MainStage的Task变化
mSyncQueue,
mSurfaceSession,
iconProvider,
mWindowDecorViewModel
);

// 3. 创建侧舞台(SideStage)
mSideStage = new SideStage(
context,
mTaskOrganizer,
mDisplayId,
mSideStageListener, // 监听SideStage的Task变化
mSyncQueue,
mSurfaceSession,
iconProvider,
mWindowDecorViewModel
);

// 4. 创建分屏布局管理器
mSplitLayout = splitLayout;
mSplitLayout.init();

// 5. 初始化时,分屏是非激活状态
// 只有当SideStage包含Task时才激活
ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "StageCoordinator initialized for display=%d", displayId);
}

Stage的创建

每个Stage(MainStage和SideStage)在创建时会向WMS请求创建自己的RootTask。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// StageTaskListener.java(MainStage和SideStage的基类)
/**
* Stage初始化
*/
protected StageTaskListener(Context context,
ShellTaskOrganizer taskOrganizer,
int displayId,
StageListenerCallbacks callbacks,
SyncTransactionQueue syncQueue,
SurfaceSession surfaceSession,
IconProvider iconProvider) {
mContext = context;
mCallbacks = callbacks;
mSyncQueue = syncQueue;
mSurfaceSession = surfaceSession;

// 向TaskOrganizer注册,表示这个Listener可以管理Task
taskOrganizer.registerListener(this);

// 请求创建RootTask
// displayId: 指定显示设备
// windowingMode: UNDEFINED,在激活时才设置为MULTI_WINDOW
mRootTaskInfo = taskOrganizer.createRootTask(
displayId,
WINDOWING_MODE_UNDEFINED,
this // TaskListener
);

ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "Stage RootTask created: taskId=%d", mRootTaskInfo.taskId);
}

【图5-1:分屏环境初始化结构】

flowchart TB
    Boot([系统启动])
    SUI[SystemUI启动]
    SSC[SplitScreenController初始化]
    SC[StageCoordinator创建]
    
    MS[MainStage创建]
    SS[SideStage创建]
    
    CRMS[createRootTask
WINDOWING_MODE_UNDEFINED] CRSS[createRootTask
WINDOWING_MODE_UNDEFINED] MSRT[mMainStageRootTask] SSRT[mSideStageRootTask] subgraph Initial["初始状态:TaskDisplayArea"] HRT[Home RootTask
前台可见] IMSRT[MainStage RootTask
隐藏,无Task] ISSRT[SideStage RootTask
隐藏,无Task] end Boot --> SUI SUI --> SSC SSC --> SC SC --> MS SC --> SS MS --> CRMS SS --> CRSS CRMS --> MSRT CRSS --> SSRT MSRT -.-> IMSRT SSRT -.-> ISSRT style Boot fill:#e1f5ff style SUI fill:#fff4e1 style SSC fill:#e8f5e9 style SC fill:#f3e5f5 style MS fill:#c8e6c9 style SS fill:#c8e6c9 style HRT fill:#fff9c4 style IMSRT fill:#e0e0e0 style ISSRT fill:#e0e0e0

5.2 进入分屏模式

触发进入分屏

用户通过多种方式触发进入分屏:

  1. 从最近任务拖拽应用到分屏区域
  2. 长按应用多任务按钮选择”分屏”
  3. 通过Launcher的分屏快捷操作

这些操作最终都会调用StageCoordinator.startTask()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// StageCoordinator.java
/**
* 启动Task到指定的分屏位置
*
* @param taskId 要启动的Task ID
* @param position 分屏位置(SPLIT_POSITION_TOP_OR_LEFT / SPLIT_POSITION_BOTTOM_OR_RIGHT)
* @param options 启动选项
*/
void startTask(int taskId, @SplitPosition int position, @Nullable Bundle options) {
// 创建事务
final WindowContainerTransaction wct = new WindowContainerTransaction();

// 如果options为null,创建默认选项
if (options == null) {
options = new Bundle();
}

// 1. 准备进入分屏(如果尚未进入)
prepareEnterSplitScreen(wct, null /* taskInfo */, position);

// 2. 确定目标Stage
final StageTaskListener targetStage = position == SPLIT_POSITION_TOP_OR_LEFT
? mMainStage : mSideStage;

// 3. 将目标Stage的RootTask token放入启动选项
// Framework在启动Task时会将其放入指定的RootTask
options.putBinder(KEY_LAUNCH_ROOT_TASK_TOKEN,
targetStage.mRootTaskInfo.token.asBinder());

// 4. 启动Task
wct.startTask(taskId, options);

// 5. 提交事务并同步Surface变换
mSyncQueue.queue(wct);
mSyncQueue.runInSync(t -> {
// 更新Surface边界(带动画)
updateSurfaceBounds(mSplitLayout, t, false /* applyResizingOffset */);
// 设置分隔线可交互
setDividerVisibility(true, t);
});

ProtoLog.d(WM_SHELL_SPLIT_SCREEN,
"startTask: taskId=%d position=%d", taskId, position);
}

准备分屏环境

prepareEnterSplitScreen()负责激活Stage并设置初始布局。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// StageCoordinator.java
/**
* 准备进入分屏模式
* 激活MainStage和SideStage,设置它们的边界
*/
void prepareEnterSplitScreen(WindowContainerTransaction wct,
@Nullable ActivityManager.RunningTaskInfo taskInfo,
@SplitPosition int startPosition) {

// 1. 如果已经在分屏模式,无需再次准备
if (isSplitScreenActive()) {
return;
}

ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "prepareEnterSplitScreen: preparing split screen");

// 2. 确定哪个Task应该在哪个Stage
// 如果有指定taskInfo,需要确保它不会进入错误的Stage
if (taskInfo != null) {
// 将当前前台Task移到MainStage
// (这通常是用户之前正在使用的应用)
final WindowContainerToken targetStageToken =
startPosition == SPLIT_POSITION_TOP_OR_LEFT
? mMainStage.mRootTaskInfo.token
: mSideStage.mRootTaskInfo.token;

wct.reparent(taskInfo.token, targetStageToken, true /* onTop */);
wct.setBounds(taskInfo.token, null); // 清除边界,继承Stage
wct.setWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED);
}

// 3. 激活MainStage
// reparentAllTasks=true表示将所有符合条件的Task移入MainStage
mMainStage.activate(wct, false /* reparentAllTasks */);

// 4. 激活SideStage
mSideStage.activate(wct, false /* reparentAllTasks */);

// 5. 设置初始分屏边界
// 根据屏幕方向和配置计算MainStage和SideStage的边界
updateWindowBounds(mSplitLayout, wct);

// 6. 更新分隔线
// 创建分隔线Surface,设置其边界
mSplitLayout.update(wct);

ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "prepareEnterSplitScreen: stages activated");
}

Stage激活

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// StageTaskListener.java
/**
* 激活Stage
* 设置为可见,设置窗口模式为MULTI_WINDOW
*/
void activate(WindowContainerTransaction wct, boolean reparentAllTasks) {
ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "Stage.activate: reparentAllTasks=%b", reparentAllTasks);

// 1. 如果需要,批量reparent所有符合条件的Task
// 这个选项通常在"快速进入分屏"时使用
// 会将当前所有可见的应用Task都移入Stage
if (reparentAllTasks) {
reparentAllEligibleTasks(wct);
}

// 2. 设置RootTask可见
wct.setHidden(mRootTaskInfo.token, false);

// 3. 设置窗口模式为MULTI_WINDOW
// 这是分屏的关键标志
wct.setWindowingMode(mRootTaskInfo.token, WINDOWING_MODE_MULTI_WINDOW);

// 4. 重新排序到顶部
// 确保Stage在Z-order上位于其他RootTask之上
wct.reorder(mRootTaskInfo.token, true /* onTop */);

ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "Stage.activate: stage is now active");
}

过渡动画的启动

在激活Stage后,系统会启动过渡动画以提供流畅的用户体验。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
// StageCoordinator.java
/**
* 启动分屏过渡动画
* 由WindowManagerService的Transition系统触发
*/
void startPendingAnimation(IBinder transition, TransitionInfo info,
SurfaceControl.Transaction startT,
SurfaceControl.Transaction finishT) {

ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "startPendingAnimation: analyzing transition");

// 1. 分析过渡中的变化
// 识别哪些Task需要动画到MainStage
// 识别哪些Task需要动画到SideStage
final TransitionInfo.Change mainStageChange = identifyMainStageChange(info);
final TransitionInfo.Change sideStageChange = identifySideStageChange(info);

// 2. 创建分割线Surface(如果还未创建)
if (mDividerLeash == null) {
mDividerLeash = createDividerSurface(startT);
ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "startPendingAnimation: divider surface created");
}

// 3. 设置分割线可见性
setDividerVisibility(true, startT);

// 4. 播放内部动画
// 使用ValueAnimator或TransitionAnimation播放进入动画
playInternalAnimation(mainStageChange, sideStageChange, startT, finishT);
}

/**
* 播放内部进入动画
* 包括Task位置、大小、透明度的变化
*/
private void playInternalAnimation(TransitionInfo.Change mainChange,
TransitionInfo.Change sideChange,
SurfaceControl.Transaction startT,
SurfaceControl.Transaction finishT) {

// 动画参数
final long ANIMATION_DURATION = 336; // ms
final Interpolator INTERPOLATOR = new PathInterpolator(0.2f, 0f, 0f, 1f);

// 获取Task的Surface Leash
final SurfaceControl mainLeash = mainChange.getLeash();
final SurfaceControl sideLeash = sideChange.getLeash();

// 创建动画
ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
animator.setDuration(ANIMATION_DURATION);
animator.setInterpolator(INTERPOLATOR);

animator.addUpdateListener(animation -> {
float progress = (float) animation.getAnimatedValue();

SurfaceControl.Transaction t = new SurfaceControl.Transaction();

// MainStage动画:从全屏位置缩放到上半屏
animateTaskToStage(t, mainLeash, mSplitLayout.getBounds1(), progress);

// SideStage动画:从屏幕外滑入到下半屏
animateTaskToStage(t, sideLeash, mSplitLayout.getBounds2(), progress);

// 分割线动画:透明度从0到1
t.setAlpha(mDividerLeash, progress);

t.apply();
});

animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
// 动画结束,应用最终状态
finishT.apply();
onTransitionAnimationComplete();
}
});

animator.start();

ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "playInternalAnimation: animation started");
}

/**
* 动画单个Task到目标Stage
*/
private void animateTaskToStage(SurfaceControl.Transaction t,
SurfaceControl leash,
Rect targetBounds,
float progress) {
// 计算当前帧的位置和大小
float x = lerp(startBounds.left, targetBounds.left, progress);
float y = lerp(startBounds.top, targetBounds.top, progress);
float width = lerp(startBounds.width(), targetBounds.width(), progress);
float height = lerp(startBounds.height(), targetBounds.height(), progress);

// 应用到Surface
t.setPosition(leash, x, y);
t.setWindowCrop(leash, (int) width, (int) height);
}

/**
* 过渡动画完成回调
*/
private void onTransitionAnimationComplete() {
ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onTransitionAnimationComplete: cleaning up");

// 1. 清理过渡状态
mTransitionState = TRANSITION_STATE_IDLE;

// 2. 确认分屏已激活
if (isSplitScreenActive()) {
ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "Split screen is now fully active");
}

// 3. 通知监听器
for (SplitScreen.SplitScreenListener listener : mListeners) {
listener.onSplitBoundsChanged(mSplitLayout.getBounds1(), mSplitLayout.getBounds2());
}
}

动画的关键点

  1. 平滑过渡:使用PathInterpolator提供流畅的缓动效果
  2. 多元素同步:MainStage、SideStage、Divider的动画同步进行
  3. 硬件加速:直接操作SurfaceControl,充分利用GPU加速
  4. 状态管理:通过onTransitionAnimationComplete()确保动画完成后的状态一致

5.3 Task加入Stage

当Task被启动或reparent到Stage后,StageTaskListener会收到回调。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
// StageTaskListener.java
/**
* Task出现时的回调
* 这是TaskOrganizer的回调方法,当新Task加入Stage时被调用
*/
@Override
public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) {
ProtoLog.d(WM_SHELL_SPLIT_SCREEN,
"onTaskAppeared: taskId=%d isVisible=%b", taskInfo.taskId, taskInfo.isVisible);

// 1. 将Task添加到内部缓存
// mChildrenTaskInfo维护了Stage中所有Task的信息
mChildrenTaskInfo.put(taskInfo.taskId, taskInfo);

// 2. 缓存Task的Surface Leash
// Leash是Surface的控制句柄,用于应用变换(位置、缩放、透明度等)
mChildrenLeashes.put(taskInfo.taskId, leash);

// 3. 更新Task计数
updateChildTaskSurface(taskInfo, leash, true /* firstAppeared */);

// 4. 回调给StageCoordinator
// 通知有新Task加入,可能需要调整布局或状态
mCallbacks.onTaskAppeared(taskInfo);

// 5. 如果这是Stage的第一个Task,触发分屏激活
if (mChildrenTaskInfo.size() == 1) {
// 对于SideStage,第一个Task加入意味着分屏正式激活
mCallbacks.onStageHasChildrenChanged(this);
}
}

/**
* Task信息变化时的回调
* 当Task的边界、可见性、配置等发生变化时被调用
*/
@Override
public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
ProtoLog.d(WM_SHELL_SPLIT_SCREEN,
"onTaskInfoChanged: taskId=%d isVisible=%b bounds=%s",
taskInfo.taskId, taskInfo.isVisible, taskInfo.configuration.windowConfiguration.getBounds());

// 1. 更新缓存的Task信息
mChildrenTaskInfo.put(taskInfo.taskId, taskInfo);

// 2. 更新Surface(如果边界或可见性变化)
SurfaceControl leash = mChildrenLeashes.get(taskInfo.taskId);
updateChildTaskSurface(taskInfo, leash, false /* firstAppeared */);

// 3. 回调给StageCoordinator
mCallbacks.onTaskInfoChanged(taskInfo);
}

/**
* Task消失时的回调
* 当Task被关闭或移出Stage时被调用
*/
@Override
public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) {
ProtoLog.d(WM_SHELL_SPLIT_SCREEN,
"onTaskVanished: taskId=%d", taskInfo.taskId);

// 1. 从缓存中移除
mChildrenTaskInfo.remove(taskInfo.taskId);
SurfaceControl leash = mChildrenLeashes.remove(taskInfo.taskId);

// 2. 释放Surface Leash
if (leash != null) {
SurfaceControl.Transaction t = new SurfaceControl.Transaction();
t.remove(leash);
t.apply();
}

// 3. 回调给StageCoordinator
mCallbacks.onTaskVanished(taskInfo);

// 4. 如果Stage变空,可能需要退出分屏
if (mChildrenTaskInfo.size() == 0) {
mCallbacks.onStageHasChildrenChanged(this);
}
}

StageCoordinator响应Task变化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
// StageCoordinator.java

// MainStage的监听器
private final StageListenerImpl mMainStageListener = new StageListenerImpl() {
@Override
public void onTaskAppeared(RunningTaskInfo taskInfo) {
// MainStage有Task加入
// 如果之前分屏未激活,现在可能需要激活
if (!isSplitScreenActive() && mSideStage.getChildCount() > 0) {
// SideStage已有Task,MainStage也有了,分屏正式启动
onSplitScreenEnter();
}
}

@Override
public void onStageHasChildrenChanged(StageTaskListener stage) {
// MainStage的Task数量变化
// 如果变为0,可能需要退出分屏
if (stage.getChildCount() == 0 && mSideStage.getChildCount() == 0) {
onSplitScreenExit();
}
}
};

// SideStage的监听器
private final StageListenerImpl mSideStageListener = new StageListenerImpl() {
@Override
public void onTaskAppeared(RunningTaskInfo taskInfo) {
// SideStage有Task加入,这是分屏激活的关键信号
if (mSideStage.getChildCount() == 1) {
// 第一个Task进入SideStage,分屏激活
onSplitScreenEnter();
}
}

@Override
public void onStageHasChildrenChanged(StageTaskListener stage) {
// SideStage的Task数量变化
if (mSideStage.getChildCount() == 0) {
// SideStage清空,退出分屏
onSplitScreenExit();
}
}
};

/**
* 分屏激活回调
*/
private void onSplitScreenEnter() {
ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onSplitScreenEnter: split screen is now active");

// 通知系统UI更新状态
for (SplitScreen.SplitScreenListener listener : mListeners) {
listener.onSplitVisibilityChanged(true);
}

// 更新最近任务的分屏状态
mRecentTasksOptional.ifPresent(recentTasks -> {
recentTasks.notifySplitScreenActiveChanged(true);
});
}

/**
* 退出分屏回调
*/
private void onSplitScreenExit() {
ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onSplitScreenExit: exiting split screen");

WindowContainerTransaction wct = new WindowContainerTransaction();

// 将Stage中的Task移回独立的RootTask
exitSplitScreen(null /* toTopTask */, EXIT_REASON_APP_FINISHED);

// 通知系统UI更新状态
for (SplitScreen.SplitScreenListener listener : mListeners) {
listener.onSplitVisibilityChanged(false);
}
}

【图5-2:分屏启动的四阶段流程详解】

flowchart TB
    Start([用户操作触发])
    
    subgraph Phase1["阶段1: 启动准备 (Preparation Phase)"]
        A1[SystemUI.handleRequest
enter split] A2[StageCoordinator.
prepareEnterSplitScreen] A3[MainStage.activate] A4[reparentTopTask
将前台Task移入MainStage] A5[设置MULTI_WINDOW模式] A6[updateWindowBounds
计算初始边界] A7["Main: [0,0,1080,1200]
Side: [0,1200,1080,2400]"] end subgraph Phase2["阶段2: 过渡动画 (Transition Animation Phase)"] B1[WMS.startPendingAnimation] B2[StageCoordinator
分析过渡变化] B3[识别主舞台/侧舞台Task] B4[SurfaceFlinger
创建分割线Surface] B5[setDividerVisibility
true] B6[playInternalAnimation] B7["• Task位置动画
• 边界缩放动画
• 透明度渐变"] end subgraph Phase3["阶段3: 任务同步 (Task Synchronization Phase)"] C1[App1.onTaskAppeared
taskId=69, EZWRITE] C2[StageCoordinator
更新缓存] C3[App2.onTaskAppeared
taskId=70, Maps] C4[updateRecentTasksSplitPair
记录分屏组合] C5["[TaskId=69 <-> TaskId=70]"] C6[onTransitionAnimationComplete] C7["• 清理过渡状态
• 确认分屏激活
• 通知监听器"] end subgraph Phase4["阶段4: 分割线调整 (Divider Adjustment Phase)"] D1[onLayoutSizeChanged
布局变化监听] D2[updateWindowBounds
重新计算边界] D3[创建WCT调整事务] D4[WMS.playResizeAnimation
播放调整动画] D5[onFinish resize] D6[updateSurfaceBounds
同步Surface] D7["• MainStage Surface
• SideStage Surface
• Divider Surface"] end Complete([分屏启动完成
用户可正常交互]) Start --> A1 A1 --> A2 --> A3 --> A4 --> A5 --> A6 --> A7 A7 --> B1 B1 --> B2 --> B3 --> B4 --> B5 --> B6 --> B7 B7 --> C1 C1 --> C2 --> C3 --> C2 C2 --> C4 --> C5 --> C6 --> C7 C7 --> D1 D1 --> D2 --> D3 --> D4 --> D5 --> D6 --> D7 D7 --> Complete style Phase1 fill:#e1f5ff style Phase2 fill:#fff4e1 style Phase3 fill:#e8f5e9 style Phase4 fill:#f3e5f5 style Complete fill:#ffebee

流程关键点说明

  1. 阶段1的核心reparentTopTask()是关键操作,它将当前前台运行的Task重新组织到MainStage,为第二个应用进入SideStage做准备。

  2. 阶段2的动画:使用playInternalAnimation()而不是直接应用配置变化,这样用户看到的是平滑的过渡,而不是突兀的跳变。动画包括:

    • Task从全屏位置移动到分屏位置
    • Task边界从全屏尺寸缩放到分屏尺寸
    • 分隔线从透明渐变到可见
  3. 阶段3的同步onTaskAppeared是异步回调,两个应用的Task可能不会同时到达。StageCoordinator需要:

    • 缓存每个Task的信息
    • 等待两个Task都出现
    • 更新最近任务的分屏配对关系
  4. 阶段4的优化updateSurfaceBounds()直接操作Surface层,比通过WCT更新配置更高效,适合频繁的边界调整场景。

5.4 动态调整分屏比例

用户拖动分隔线时,Task的边界需要实时更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// SplitLayout.java
/**
* 响应分隔线拖动
*
* @param position 分隔线的新位置
*/
void onDraggingDivider(int position) {
// 1. 计算新的边界
// 根据分隔线位置,重新计算两个Stage的bounds
mDividePosition = position;
Rect bounds1 = new Rect();
Rect bounds2 = new Rect();
calculateBounds(position, bounds1, bounds2);

// 2. 应用边界变化(通过Surface Transaction,不经过WCT)
// 拖动过程中频繁更新,使用Surface Transaction性能更好
SurfaceControl.Transaction t = mTransactionPool.acquire();
applySurfaceChanges(t, bounds1, bounds2, false /* applyResizingOffset */);
t.apply();
mTransactionPool.release(t);
}

/**
* 分隔线拖动结束
* 此时需要正式更新Task的配置
*/
void onDraggingDividerFinished(int position) {
ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onDraggingDividerFinished: position=%d", position);

// 1. 创建事务
WindowContainerTransaction wct = new WindowContainerTransaction();

// 2. 更新Stage边界配置
// 这会触发Task的配置变化回调,应用会重新布局
updateWindowBounds(mSplitLayout, wct);

// 3. 提交事务
mSyncQueue.queue(wct);

// 4. 保存当前分屏比例
// 用于下次进入分屏时恢复
mSplitLayout.setDividePosition(position, true /* applyLayoutChange */);
}

Task边界的更新传播

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// StageCoordinator.java
/**
* 更新Stage的窗口边界
*
* @param layout 分屏布局管理器
* @param wct 事务对象
*/
private boolean updateWindowBounds(SplitLayout layout, WindowContainerTransaction wct) {
// 1. 从SplitLayout获取计算好的边界
final Rect bounds1 = layout.getBounds1(); // MainStage边界
final Rect bounds2 = layout.getBounds2(); // SideStage边界

ProtoLog.d(WM_SHELL_SPLIT_SCREEN,
"updateWindowBounds: bounds1=%s bounds2=%s", bounds1, bounds2);

// 2. 更新MainStage边界
wct.setBounds(mMainStage.mRootTaskInfo.token, bounds1);
wct.setAppBounds(mMainStage.mRootTaskInfo.token, bounds1);
wct.setSmallestScreenWidthDp(mMainStage.mRootTaskInfo.token,
getSmallestWidthDpForBounds(bounds1));

// 3. 更新SideStage边界
wct.setBounds(mSideStage.mRootTaskInfo.token, bounds2);
wct.setAppBounds(mSideStage.mRootTaskInfo.token, bounds2);
wct.setSmallestScreenWidthDp(mSideStage.mRootTaskInfo.token,
getSmallestWidthDpForBounds(bounds2));

return true;
}

边界变化传播链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
拖动分隔线

SplitLayout.onDraggingDivider()

计算新的bounds1, bounds2

applySurfaceChanges()(实时Surface变换)

拖动结束:onDraggingDividerFinished()

updateWindowBounds(wct)

wct.setBounds(MainStage, bounds1)
wct.setBounds(SideStage, bounds2)

Framework: Stage.onConfigurationChanged()

Framework: Task.onConfigurationChanged()

应用进程: Activity.onConfigurationChanged()

应用重新布局和绘制

5.5 退出分屏模式

退出分屏有多种触发方式:

  1. 用户将分隔线拖到边缘
  2. 关闭某个Stage的最后一个Task
  3. 启动不支持分屏的全屏应用
  4. 系统进入其他模式(如PIP)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
// StageCoordinator.java
/**
* 退出分屏模式
*
* @param toTopTask 退出后要显示在前台的Task,null表示自动选择
* @param exitReason 退出原因(用于日志和统计)
*/
void exitSplitScreen(@Nullable StageTaskListener childrenToTop, int exitReason) {
ProtoLog.d(WM_SHELL_SPLIT_SCREEN,
"exitSplitScreen: reason=%d childrenToTop=%s", exitReason, childrenToTop);

// 创建事务
final WindowContainerTransaction wct = new WindowContainerTransaction();

// 1. 如果没有指定要保留的Stage,默认保留MainStage
if (childrenToTop == null) {
childrenToTop = mMainStage.getChildCount() > 0 ? mMainStage : mSideStage;
}

// 2. 将保留Stage中的Task移回独立的RootTask
// evictOtherChildren=true表示清空另一个Stage
childrenToTop.evictOtherChildren(wct);

// 3. 重置MainStage
mMainStage.deactivate(wct);

// 4. 重置SideStage
mSideStage.deactivate(wct);

// 5. 隐藏分隔线
wct.setHidden(mSplitLayout.mDividerLeash, true);

// 6. 提交事务
mSyncQueue.queue(wct);
mSyncQueue.runInSync(t -> {
// 移除Surface
t.hide(mMainStage.mRootLeash);
t.hide(mSideStage.mRootLeash);
t.hide(mSplitLayout.mDividerLeash);
});

// 7. 通知监听器
onSplitScreenExit();

ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "exitSplitScreen: split screen exited");
}

// StageTaskListener.java
/**
* 取消激活Stage
*/
void deactivate(WindowContainerTransaction wct) {
ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "Stage.deactivate");

// 1. 移除所有子Task(reparent回独立的RootTask)
evictAllChildren(wct);

// 2. 隐藏RootTask
wct.setHidden(mRootTaskInfo.token, true);

// 3. 重置边界
wct.setBounds(mRootTaskInfo.token, null);
wct.setAppBounds(mRootTaskInfo.token, null);

// 4. 重置窗口模式
wct.setWindowingMode(mRootTaskInfo.token, WINDOWING_MODE_UNDEFINED);
}

/**
* 移除Stage中的所有Task
*/
void evictAllChildren(WindowContainerTransaction wct) {
for (int i = mChildrenTaskInfo.size() - 1; i >= 0; i--) {
final ActivityManager.RunningTaskInfo taskInfo = mChildrenTaskInfo.valueAt(i);

// 将Task reparent到null(回到TDA根部,系统会自动为其创建独立的RootTask)
wct.reparent(taskInfo.token, null /* newParent */, false /* onTop */);

// 重置Task的配置
wct.setBounds(taskInfo.token, null);
wct.setWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED);
}
}

【图5-3:退出分屏的Task重组流程】

flowchart TB
    Start([触发退出分屏])
    Exit[exitSplitScreen]
    Decide[确定要保留的Stage]
    
    MS[MainStage.deactivate]
    SS[SideStage.deactivate]
    
    MSEC[evictAllChildren]
    SSEC[evictAllChildren]
    
    MSR[reparent task → null]
    SSR[reparent task → null]
    
    MSTDA[Task移回TDA根部]
    SSTDA[Task移回TDA根部]
    
    Create[系统为Task创建独立RootTask]
    Restore[Task恢复全屏显示]
    Notify[onSplitScreenExit]
    Complete([通知系统UI,分屏已退出])
    
    Start --> Exit
    Exit --> Decide
    
    Decide --> MS
    Decide --> SS
    
    MS --> MSEC
    SS --> SSEC
    
    MSEC --> MSR
    SSEC --> SSR
    
    MSR --> MSTDA
    SSR --> SSTDA
    
    MSTDA --> Create
    SSTDA --> Create
    
    Create --> Restore
    Restore --> Notify
    Notify --> Complete
    
    style Start fill:#ffebee
    style Exit fill:#e1f5ff
    style MS fill:#fff4e1
    style SS fill:#e8f5e9
    style Create fill:#f3e5f5
    style Restore fill:#c8e6c9
    style Complete fill:#fff9c4

小结

本章完整追踪了分屏Task组织的生命周期:

  1. 初始化:StageCoordinator创建Stage,Stage创建RootTask
  2. 进入分屏:激活Stage,reparent Task,设置边界
  3. Task加入:onTaskAppeared回调,更新状态
  4. 动态调整:拖动分隔线,更新边界,应用响应配置变化
  5. 退出分屏:deactivate Stage,Task移回独立RootTask

下一章将深入分析Task边界管理的细节,包括bounds、appBounds、maxBounds的计算和应用。


第6章:Task边界管理深度解析

Task的边界管理是分屏功能中最精细的部分之一,它直接决定了应用的显示区域和布局计算。

6.1 三种边界类型详解

在WindowConfiguration中,存在三种不同的边界概念,理解它们的区别对于掌握Task边界管理至关重要。

Bounds:任务的显示边界

1
2
3
4
5
6
7
8
9
10
11
// WindowConfiguration.java
private Rect mBounds = new Rect();

/**
* Bounds是Task在屏幕上的显示区域
*
* 特性:
* - 在父容器坐标系中的绝对位置
* - 包含所有窗口装饰(如窗口标题栏)
* - 是Surface的实际显示区域
*/

示例

1
2
3
4
屏幕分辨率:1080x2400
分屏模式下:
- MainStage bounds: [0, 0, 1080, 1200] // 上半屏
- SideStage bounds: [0, 1200, 1080, 2400] // 下半屏

AppBounds:应用可用边界

1
2
3
4
5
6
7
8
9
10
11
// WindowConfiguration.java
private Rect mAppBounds;

/**
* AppBounds是应用实际可用的绘制区域
*
* 特性:
* - 扣除了系统UI占用的空间(状态栏、导航栏等)
* - 应用的contentView在这个区域内绘制
* - 可能小于bounds
*/

示例

1
2
3
假设MainStage有状态栏(高度75px):
- MainStage bounds: [0, 0, 1080, 1200]
- MainStage appBounds: [0, 75, 1080, 1200] // 顶部扣除状态栏

MaxBounds:最大可扩展边界

1
2
3
4
5
6
7
8
9
10
11
// WindowConfiguration.java
private Rect mMaxBounds = new Rect();

/**
* MaxBounds是Task可以扩展到的最大区域
*
* 使用场景:
* - 屏幕旋转:记录旋转前的最大bounds
* - 折叠屏展开:记录展开后的最大可用空间
* - 多显示设备:记录跨显示设备的边界
*/

【图6-1:三种边界类型的关系】

%%{init: {'theme':'base', 'themeVariables': { 'fontSize':'14px'}}}%%
flowchart TB
    subgraph Screen["屏幕物理区域"]
        StatusBar["状态栏 (系统UI)
← 75px"] subgraph Upper["bounds.top = 0"] subgraph AppContent["AppBounds
appBounds.top = 75"] Content["应用内容绘制区域
(contentView)"] end end BoundsBottom["bounds.bottom = 1200"] Divider["分隔线"] subgraph Lower["下半屏Stage"] LowerContent[" "] end end Note1["bounds
(当前显示区域)"] Note2["MaxBounds
(可扩展到的最大区域)"] StatusBar --> Upper Upper --> BoundsBottom BoundsBottom --> Divider Divider --> Lower Upper -.-> Note1 Screen -.-> Note2 style StatusBar fill:#ffccbc style AppContent fill:#c8e6c9 style Content fill:#a5d6a7 style Divider fill:#90a4ae style Lower fill:#e1f5ff

6.2 边界的计算与设置

SplitLayout:分屏边界计算器

SplitLayout负责根据屏幕尺寸、方向、分隔线位置计算两个Stage的边界。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
// SplitLayout.java
/**
* 分屏布局管理器
* 负责计算MainStage和SideStage的边界
*/
public class SplitLayout {
// 屏幕显示区域
private final Rect mRootBounds = new Rect();

// 主舞台边界
private final Rect mBounds1 = new Rect();

// 侧舞台边界
private final Rect mBounds2 = new Rect();

// 分隔线边界
private final Rect mDividerBounds = new Rect();

// 分隔线位置
private int mDividePosition;

// 分屏方向(水平或垂直)
private boolean mIsHorizontalSplit;

/**
* 初始化布局
* 根据屏幕配置确定分屏方向和初始分隔线位置
*/
public void init() {
// 获取显示设备信息
final DisplayLayout displayLayout = mDisplayController.getDisplayLayout(mDisplayId);
mRootBounds.set(0, 0, displayLayout.width(), displayLayout.height());

// 确定分屏方向
// 横屏:垂直分隔(左右分屏)
// 竖屏:水平分隔(上下分屏)
mIsHorizontalSplit = mRootBounds.width() < mRootBounds.height();

// 设置初始分隔线位置(默认50:50)
mDividePosition = mIsHorizontalSplit
? mRootBounds.height() / 2
: mRootBounds.width() / 2;

// 计算初始边界
updateBounds(mDividePosition);
}

/**
* 根据分隔线位置更新边界
*
* @param dividePosition 分隔线位置
*/
public void updateBounds(int dividePosition) {
mDividePosition = dividePosition;

if (mIsHorizontalSplit) {
// 水平分屏(上下分屏)
// MainStage在上,SideStage在下
mBounds1.set(
mRootBounds.left,
mRootBounds.top,
mRootBounds.right,
dividePosition - mDividerSize / 2
);

mBounds2.set(
mRootBounds.left,
dividePosition + mDividerSize / 2,
mRootBounds.right,
mRootBounds.bottom
);

mDividerBounds.set(
mRootBounds.left,
dividePosition - mDividerSize / 2,
mRootBounds.right,
dividePosition + mDividerSize / 2
);
} else {
// 垂直分屏(左右分屏)
// MainStage在左,SideStage在右
mBounds1.set(
mRootBounds.left,
mRootBounds.top,
dividePosition - mDividerSize / 2,
mRootBounds.bottom
);

mBounds2.set(
dividePosition + mDividerSize / 2,
mRootBounds.top,
mRootBounds.right,
mRootBounds.bottom
);

mDividerBounds.set(
dividePosition - mDividerSize / 2,
mRootBounds.top,
dividePosition + mDividerSize / 2,
mRootBounds.bottom
);
}

ProtoLog.d(WM_SHELL_SPLIT_SCREEN,
"updateBounds: bounds1=%s bounds2=%s dividerBounds=%s",
mBounds1, mBounds2, mDividerBounds);
}

/**
* 获取主舞台边界
*/
public Rect getBounds1() {
return new Rect(mBounds1);
}

/**
* 获取侧舞台边界
*/
public Rect getBounds2() {
return new Rect(mBounds2);
}
}

【图6-2:分屏边界计算示意图】

flowchart TB
    subgraph Portrait["竖屏模式(水平分隔,上下分屏)"]
        direction TB
        P_Main["Main Stage Bounds1
[0,0,1080,1188]
高度 = dividePosition - dividerSize/2"] P_Divider["Divider (12px)
[0,1188,1080,1212]
← dividePosition = 1200"] P_Side["Side Stage Bounds2
[0,1212,1080,2400]
高度 = 屏幕高度 - dividePosition - dividerSize/2"] P_Main --> P_Divider P_Divider --> P_Side end subgraph Landscape["横屏模式(垂直分隔,左右分屏)"] direction LR L_Main["Main Stage
Bounds1
[0,0,1188,1080]"] L_Divider["分

线
(12px)"] L_Side["Side Stage
Bounds2
[1212,0,2400,1080]"] L_Note["dividePosition = 1200"] L_Main ---|""|L_Divider L_Divider ---|""|L_Side L_Divider -.-> L_Note end style P_Main fill:#c8e6c9 style P_Divider fill:#90a4ae style P_Side fill:#e1f5ff style L_Main fill:#c8e6c9 style L_Divider fill:#90a4ae style L_Side fill:#e1f5ff style L_Note fill:#fff9c4

边界的应用

计算好边界后,通过WindowContainerTransaction应用到Stage。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// StageCoordinator.java
/**
* 更新Stage的窗口边界
*/
private boolean updateWindowBounds(SplitLayout layout, WindowContainerTransaction wct) {
final Rect bounds1 = layout.getBounds1();
final Rect bounds2 = layout.getBounds2();

// 更新MainStage
wct.setBounds(mMainStage.mRootTaskInfo.token, bounds1)
.setAppBounds(mMainStage.mRootTaskInfo.token, bounds1)
.setSmallestScreenWidthDp(mMainStage.mRootTaskInfo.token,
getSmallestWidthDp(bounds1));

// 更新SideStage
wct.setBounds(mSideStage.mRootTaskInfo.token, bounds2)
.setAppBounds(mSideStage.mRootTaskInfo.token, bounds2)
.setSmallestScreenWidthDp(mSideStage.mRootTaskInfo.token,
getSmallestWidthDp(bounds2));

return true;
}

/**
* 根据bounds计算smallest width(用于资源选择)
*/
private int getSmallestWidthDp(Rect bounds) {
final int width = bounds.width();
final int height = bounds.height();
final int smallest = Math.min(width, height);

// 转换为dp
final float density = mContext.getResources().getDisplayMetrics().density;
return (int) (smallest / density);
}

6.3 边界继承机制

子Task默认继承父容器(Stage RootTask)的边界配置。

清除Task自身边界

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// StageTaskListener.java
/**
* 将Task添加到Stage时,清除Task自身的边界配置
* 让Task完全继承Stage的配置
*/
void addTask(RunningTaskInfo task, WindowContainerTransaction wct) {
// 清除Task的边界覆盖
wct.setBounds(task.token, null); // null表示继承
wct.setAppBounds(task.token, null); // null表示继承
wct.setWindowingMode(task.token, WINDOWING_MODE_UNDEFINED); // UNDEFINED表示继承

// Reparent到Stage
wct.reparent(task.token, mRootTaskInfo.token, true);
}

继承的好处

  1. 简化管理:只需设置Stage边界,所有Task自动生效
  2. 自动同步:拖动分隔线时,Task边界自动更新
  3. 避免冲突:Task不保存自己的边界,不会与Stage冲突

边界继承的计算过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// Framework: WindowContainer.java
/**
* 配置继承的核心逻辑
*/
void onConfigurationChanged(Configuration newParentConfig) {
// 1. 获取父容器的配置
final Configuration parentConfig = newParentConfig;

// 2. 如果自己没有覆盖bounds,使用父容器的bounds
if (mBounds.isEmpty()) {
mBounds.set(parentConfig.windowConfiguration.getBounds());
}

// 3. 如果自己没有覆盖appBounds,使用父容器的appBounds
if (mAppBounds == null) {
mAppBounds = new Rect(parentConfig.windowConfiguration.getAppBounds());
}

// 4. 如果windowingMode是UNDEFINED,继承父容器的值
if (mWindowingMode == WINDOWING_MODE_UNDEFINED) {
mWindowingMode = parentConfig.windowConfiguration.getWindowingMode();
}

// 5. 合并配置
mMergedOverrideConfiguration.setTo(parentConfig);
mMergedOverrideConfiguration.updateFrom(mOverrideConfiguration);

// 6. 向下传播给子容器
for (int i = mChildren.size() - 1; i >= 0; i--) {
mChildren.get(i).onConfigurationChanged(mMergedOverrideConfiguration);
}
}

【配置继承示意图】

1
2
3
4
5
6
7
8
9
10
11
Stage RootTask
bounds = [0, 0, 1080, 1200]
windowingMode = MULTI_WINDOW
↓ (继承)
Task A
bounds = null (继承) → 实际使用 [0, 0, 1080, 1200]
windowingMode = UNDEFINED (继承) → 实际使用 MULTI_WINDOW
↓ (继承)
Activity Record
bounds = null (继承) → 实际使用 [0, 0, 1080, 1200]
windowingMode = UNDEFINED (继承) → 实际使用 MULTI_WINDOW

6.4 边界重置时机

在特定场景下,需要重置Task的边界配置。

退出分屏时重置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// StageTaskListener.java
/**
* 重置Stage的边界
* 退出分屏时调用
*/
void resetBounds(WindowContainerTransaction wct) {
// 重置Stage RootTask的边界
wct.setBounds(mRootTaskInfo.token, null);
wct.setAppBounds(mRootTaskInfo.token, null);
wct.setSmallestScreenWidthDp(mRootTaskInfo.token,
SMALLEST_SCREEN_WIDTH_DP_UNDEFINED);

// 重置所有子Task的边界
for (int i = 0; i < mChildrenTaskInfo.size(); i++) {
final RunningTaskInfo taskInfo = mChildrenTaskInfo.valueAt(i);
wct.setBounds(taskInfo.token, null);
wct.setAppBounds(taskInfo.token, null);
wct.setWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED);
}
}

Task移出Stage时重置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// StageTaskListener.java
/**
* 将Task移出Stage
* 重置Task的配置,让它适应新的父容器
*/
void evictChild(RunningTaskInfo taskInfo, WindowContainerTransaction wct) {
// 重置Task的配置
wct.setBounds(taskInfo.token, null);
wct.setAppBounds(taskInfo.token, null);
wct.setWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED);

// Reparent到TDA根部(系统会为其创建新的RootTask)
wct.reparent(taskInfo.token, null, false);
}

6.5 屏幕旋转时的边界处理

屏幕旋转是边界管理中的复杂场景。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// StageCoordinator.java
/**
* 响应屏幕旋转
*/
void onDisplayConfigurationChanged(int displayId, Configuration newConfig) {
if (displayId != mDisplayId) {
return;
}

ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onDisplayConfigurationChanged: rotation=%d",
newConfig.windowConfiguration.getRotation());

// 1. 更新根边界
final Rect newRootBounds = newConfig.windowConfiguration.getBounds();
mSplitLayout.updateRoot Bounds(newRootBounds);

// 2. 重新计算分屏边界
// 保持分隔线的相对位置(比例)
final float ratio = (float) mSplitLayout.getDividePosition() /
(mSplitLayout.isHorizontalSplit()
? mSplitLayout.getRootBounds().height()
: mSplitLayout.getRootBounds().width());

final int newDividePosition = mSplitLayout.isHorizontalSplit()
? (int) (newRootBounds.height() * ratio)
: (int) (newRootBounds.width() * ratio);

mSplitLayout.updateBounds(newDividePosition);

// 3. 应用新边界
WindowContainerTransaction wct = new WindowContainerTransaction();
updateWindowBounds(mSplitLayout, wct);

// 4. 同步更新Surface
mSyncQueue.queue(wct);
mSyncQueue.runInSync(t -> {
updateSurfaceBounds(mSplitLayout, t, false);
});
}

小结

本章深入分析了Task边界管理:

  1. 三种边界:bounds、appBounds、maxBounds的含义和区别
  2. 边界计算:SplitLayout根据屏幕和分隔线位置计算边界
  3. 边界继承:Task继承Stage配置的机制
  4. 边界重置:退出分屏和Task迁移时的边界重置
  5. 旋转处理:屏幕旋转时保持分屏比例

下一章将分析Task状态管理,包括可见性、焦点、Z-order等状态的维护。


第7章:Task状态管理

Task在分屏模式下不仅需要管理边界,还需要管理各种运行时状态。

7.1 Task可见性管理

可见性的判断

1
2
3
4
5
6
7
8
9
10
11
12
// TaskInfo.java
/**
* Task是否可见
* 由WindowManagerService设置
*/
public boolean isVisible;

// Task的可见性受多个因素影响:
// 1. Task所在的RootTask是否可见
// 2. Task是否被其他Task遮挡
// 3. Task的Activity是否处于Resumed状态
// 4. Task的Surface是否被显示

清理不可见Task

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// StageTaskListener.java
/**
* 移除Stage中所有不可见的Task
* 优化内存和性能
*/
void evictInvisibleChildren(WindowContainerTransaction wct) {
ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "evictInvisibleChildren: checking visibility");

for (int i = mChildrenTaskInfo.size() - 1; i >= 0; i--) {
final RunningTaskInfo taskInfo = mChildrenTaskInfo.valueAt(i);

// 检查Task是否不可见
if (!taskInfo.isVisible) {
ProtoLog.d(WM_SHELL_SPLIT_SCREEN,
"evictInvisibleChildren: evicting invisible task=%d", taskInfo.taskId);

// 将不可见Task移出Stage
wct.reparent(taskInfo.token, null, false);
wct.setBounds(taskInfo.token, null);
wct.setWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED);
}
}
}

何时调用evictInvisibleChildren?

  • 用户切换到其他Task
  • 进入最近任务界面
  • 从分屏返回全屏模式

7.2 Task Z-order管理

Z-order决定了Task的叠加顺序,影响事件分发和显示优先级。

重新排序Task

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// StageTaskListener.java
/**
* 调整Task在Stage中的Z-order
*
* @param taskId 要调整的Task ID
* @param onTop true表示移到顶部,false表示移到底部
* @param wct 事务对象
*/
void reorderChild(int taskId, boolean onTop, WindowContainerTransaction wct) {
if (!containsTask(taskId)) {
ProtoLog.w(WM_SHELL_SPLIT_SCREEN,
"reorderChild: task=%d not found in stage", taskId);
return;
}

final RunningTaskInfo taskInfo = mChildrenTaskInfo.get(taskId);

ProtoLog.d(WM_SHELL_SPLIT_SCREEN,
"reorderChild: task=%d onTop=%b", taskId, onTop);

// 重新排序
wct.reorder(taskInfo.token, onTop);

// 如果移到顶部,可能需要请求焦点
if (onTop) {
wct.setFocusable(taskInfo.token, true);
}
}

用户切换Task时的Z-order调整

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// StageCoordinator.java
/**
* 用户点击Stage中的某个Task
* 将其移到前台
*/
void onTaskTapped(int taskId) {
// 确定Task所在的Stage
final StageTaskListener stage = mMainStage.containsTask(taskId)
? mMainStage : mSideStage;

if (stage == null) {
return;
}

WindowContainerTransaction wct = new WindowContainerTransaction();

// 将Task移到Stage的顶部
stage.reorderChild(taskId, true, wct);

// 应用事务
mTaskOrganizer.applyTransaction(wct);
}

7.3 Task焦点管理

在分屏模式下,两个Stage可以同时可见,但只有一个可以拥有输入焦点。

焦点切换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// StageCoordinator.java
/**
* 设置焦点Stage
*
* @param stage 要获得焦点的Stage
*/
void setFocusedStage(StageTaskListener stage) {
if (stage == null) {
return;
}

ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "setFocusedStage: stage=%s", stage);

WindowContainerTransaction wct = new WindowContainerTransaction();

// 将目标Stage移到顶部(在split root task的子容器中)
wct.reorder(stage.mRootTaskInfo.token, true /* onTop */);

// 设置另一个Stage为non-focusable
final StageTaskListener otherStage = stage == mMainStage ? mSideStage : mMainStage;
wct.setFocusable(otherStage.mRootTaskInfo.token, false);
wct.setFocusable(stage.mRootTaskInfo.token, true);

mTaskOrganizer.applyTransaction(wct);
}

焦点变化的影响

有焦点的Stage

  • 可以接收键盘输入
  • 可以接收触摸事件
  • Activity处于Resumed状态
  • 标题栏高亮显示(如果有)

无焦点的Stage

  • 不接收键盘输入
  • 可以接收触摸事件(点击后会获得焦点)
  • Activity处于Paused状态
  • 标题栏灰化显示

7.4 Task生命周期状态同步

StageTaskListener需要实时同步Task的生命周期状态。

监听Task状态变化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
// StageTaskListener.java
/**
* Task信息变化回调
* 这个方法会频繁被调用,需要高效处理
*/
@Override
public void onTaskInfoChanged(RunningTaskInfo taskInfo) {
// 更新缓存
final RunningTaskInfo oldInfo = mChildrenTaskInfo.get(taskInfo.taskId);
mChildrenTaskInfo.put(taskInfo.taskId, taskInfo);

// 检查关键状态变化
if (oldInfo != null) {
// 1. 可见性变化
if (oldInfo.isVisible != taskInfo.isVisible) {
onTaskVisibilityChanged(taskInfo);
}

// 2. 边界变化
if (!oldInfo.configuration.windowConfiguration.getBounds()
.equals(taskInfo.configuration.windowConfiguration.getBounds())) {
onTaskBoundsChanged(taskInfo);
}

// 3. 顶部Activity变化
if (!Objects.equals(oldInfo.topActivity, taskInfo.topActivity)) {
onTaskTopActivityChanged(taskInfo);
}
}

// 通知StageCoordinator
mCallbacks.onTaskInfoChanged(taskInfo);
}

/**
* Task可见性变化处理
*/
private void onTaskVisibilityChanged(RunningTaskInfo taskInfo) {
ProtoLog.d(WM_SHELL_SPLIT_SCREEN,
"onTaskVisibilityChanged: task=%d visible=%b",
taskInfo.taskId, taskInfo.isVisible);

if (!taskInfo.isVisible) {
// Task变为不可见,可能需要回收资源
// 例如:停止动画、释放Surface缓存等
} else {
// Task变为可见,可能需要恢复资源
}
}

/**
* Task边界变化处理
*/
private void onTaskBoundsChanged(RunningTaskInfo taskInfo) {
ProtoLog.d(WM_SHELL_SPLIT_SCREEN,
"onTaskBoundsChanged: task=%d bounds=%s",
taskInfo.taskId, taskInfo.configuration.windowConfiguration.getBounds());

// 更新Task Surface的位置和大小
final SurfaceControl leash = mChildrenLeashes.get(taskInfo.taskId);
if (leash != null) {
SurfaceControl.Transaction t = new SurfaceControl.Transaction();
final Rect bounds = taskInfo.configuration.windowConfiguration.getBounds();
t.setPosition(leash, bounds.left, bounds.top);
t.setWindowCrop(leash, bounds.width(), bounds.height());
t.apply();
}
}

7.5 Task多任务栈管理

在分屏模式下,每个Stage可以包含多个Task,形成Task栈。

Task栈的维护

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// StageTaskListener.java
/**
* Stage中的所有Task(按Z-order排序)
*/
private final SparseArray<RunningTaskInfo> mChildrenTaskInfo = new SparseArray<>();

/**
* 获取顶部Task
*/
RunningTaskInfo getTopChildTaskInfo() {
if (mChildrenTaskInfo.size() == 0) {
return null;
}

// 遍历找到Z-order最高的Task
RunningTaskInfo topTask = null;
for (int i = 0; i < mChildrenTaskInfo.size(); i++) {
final RunningTaskInfo taskInfo = mChildrenTaskInfo.valueAt(i);
if (topTask == null || taskInfo.isVisible) {
topTask = taskInfo;
}
}

return topTask;
}

/**
* 获取所有可见Task
*/
List<RunningTaskInfo> getVisibleChildTasks() {
final List<RunningTaskInfo> visibleTasks = new ArrayList<>();
for (int i = 0; i < mChildrenTaskInfo.size(); i++) {
final RunningTaskInfo taskInfo = mChildrenTaskInfo.valueAt(i);
if (taskInfo.isVisible) {
visibleTasks.add(taskInfo);
}
}
return visibleTasks;
}

Task栈的操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// StageCoordinator.java
/**
* 在Stage中启动新Task
* 新Task会被添加到Task栈的顶部
*/
void startTaskInStage(int taskId, StageTaskListener stage, Bundle options) {
// 设置启动选项,指定目标Stage
options.putBinder(KEY_LAUNCH_ROOT_TASK_TOKEN,
stage.mRootTaskInfo.token.asBinder());

WindowContainerTransaction wct = new WindowContainerTransaction();
wct.startTask(taskId, options);

mTaskOrganizer.applyTransaction(wct);
}

/**
* 从Stage中移除Task
* Task栈自动调整
*/
void removeTaskFromStage(int taskId, StageTaskListener stage) {
if (!stage.containsTask(taskId)) {
return;
}

WindowContainerTransaction wct = new WindowContainerTransaction();

// 获取Task信息
final RunningTaskInfo taskInfo = stage.mChildrenTaskInfo.get(taskId);

// Reparent到null,让系统为其创建独立的RootTask
wct.reparent(taskInfo.token, null, false);
wct.setBounds(taskInfo.token, null);
wct.setWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED);

mTaskOrganizer.applyTransaction(wct);
}

小结

本章分析了Task状态管理的各个方面:

  1. 可见性管理:判断和清理不可见Task
  2. Z-order管理:调整Task叠加顺序
  3. 焦点管理:控制输入焦点的Stage
  4. 状态同步:实时监听和响应Task状态变化
  5. 任务栈管理:维护Stage中的多Task栈

下一章将总结分屏Task组织的关键设计原则和约束。


第8章:关键设计原则与约束

分屏功能的Task组织机制建立在一系列精心设计的原则和约束之上,理解这些原则对于掌握分屏实现至关重要。

8.1 受控的窗口模式和活动类型

并非所有Task都适合进入分屏,系统通过窗口模式和活动类型进行筛选。

受控的窗口模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// SplitScreenConstants.java
/**
* 可以进入分屏的窗口模式
*/
public static final int[] CONTROLLED_WINDOWING_MODES = {
WINDOWING_MODE_FULLSCREEN, // 全屏模式
WINDOWING_MODE_MULTI_WINDOW // 多窗口模式
};

/**
* 为什么只有这两种模式?
*
* FULLSCREEN:正常运行的应用,是分屏的主要来源
* MULTI_WINDOW:已经在分屏中的应用,可以在Stage间移动
*
* 排除的模式:
* - PINNED (PIP):画中画不能进入分屏
* - FREEFORM:自由窗口有自己的管理机制
* - UNDEFINED:未定义状态,需等待明确的模式
*/

受控的活动类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// SplitScreenConstants.java
/**
* 可以进入分屏的活动类型
*/
public static final int[] CONTROLLED_ACTIVITY_TYPES = {
ACTIVITY_TYPE_STANDARD, // 标准应用
ACTIVITY_TYPE_UNDEFINED // 未定义类型(继承父容器)
};

/**
* 为什么只有这两种类型?
*
* STANDARD:普通应用Activity,是分屏的主要对象
* UNDEFINED:允许继承父容器类型的Task
*
* 排除的类型:
* - HOME:桌面不能进入分屏
* - RECENTS:最近任务界面不能进入分屏
* - ASSISTANT:助手界面有特殊处理逻辑
* - DREAM:屏保/梦境模式不适合分屏
*/

筛选逻辑的应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// StageTaskListener.java
/**
* 批量reparent符合条件的Task
*/
void reparentAllEligibleTasks(WindowContainerTransaction wct) {
wct.reparentTasks(
null, // 从所有父容器筛选
mRootTaskInfo.token, // 移动到当前Stage
CONTROLLED_WINDOWING_MODES, // 只选这些窗口模式
CONTROLLED_ACTIVITY_TYPES, // 只选这些活动类型
true, // onTop
false // reparent所有符合条件的,不仅仅顶部
);
}

实际效果

1
2
3
4
5
6
7
系统中的Task:
├─ Launcher (HOME) → 不进入分屏 ✗
├─ Recents (RECENTS) → 不进入分屏 ✗
├─ YouTube PIP (PINNED) → 不进入分屏 ✗
├─ WeChat (STANDARD, FULLSCREEN) → 进入分屏 ✓
├─ Gmail (STANDARD, FULLSCREEN) → 进入分屏 ✓
└─ Browser (STANDARD, MULTI_WINDOW) → 进入分屏 ✓

8.2 Stage协调器的高级规则

StageCoordinator遵循严格的规则来管理分屏状态。

核心规则文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// StageCoordinator.java
/**
* 分屏模式的高级规则:
*
* 1. 只有当SideStage包含至少一个子Task时,StageCoordinator才被认为是激活的
* - 这是分屏激活的唯一判断标准
* - SideStage为空 = 分屏未激活
*
* 2. 只有在协调器激活时,MainStage才应该有子Task
* - MainStage的Task依赖于分屏状态
* - 退出分屏时,MainStage的Task应该被移出或移回独立RootTask
*
* 3. 只有当MainStage和SideStage都可见时,分隔线才可见
* - 分隔线的可见性与两个Stage绑定
* - 任一Stage不可见,分隔线自动隐藏
*
* 4. 两个Stage都放在单个single-top的RootTask下
* - 确保Stage间的协调管理
* - 便于统一的事件分发和动画处理
*/

激活条件的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// StageCoordinator.java
/**
* 判断分屏是否激活
* 核心依据:SideStage是否有Task
*/
boolean isSplitScreenActive() {
return mSideStage.getChildCount() > 0;
}

/**
* 确保Stage状态一致性
* 在关键操作后调用,确保规则被遵守
*/
private void ensureStateConsistency() {
if (!isSplitScreenActive()) {
// 分屏未激活,确保MainStage也是空的
if (mMainStage.getChildCount() > 0) {
ProtoLog.w(WM_SHELL_SPLIT_SCREEN,
"ensureStateConsistency: split not active but MainStage has tasks, cleaning up");

WindowContainerTransaction wct = new WindowContainerTransaction();
mMainStage.evictAllChildren(wct);
mTaskOrganizer.applyTransaction(wct);
}
}
}

分隔线可见性规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// StageCoordinator.java
/**
* 设置分隔线可见性
* 必须满足:MainStage和SideStage都可见
*/
void setDividerVisibility(boolean visible, SurfaceControl.Transaction t) {
// 检查规则3:两个Stage都必须可见
final boolean mainVisible = mMainStage.isVisible();
final boolean sideVisible = mSideStage.isVisible();
final boolean shouldVisible = visible && mainVisible && sideVisible;

ProtoLog.d(WM_SHELL_SPLIT_SCREEN,
"setDividerVisibility: requested=%b main=%b side=%b actual=%b",
visible, mainVisible, sideVisible, shouldVisible);

if (mDividerLeash != null) {
if (shouldVisible) {
t.show(mDividerLeash);
t.setAlpha(mDividerLeash, 1.0f);
} else {
t.hide(mDividerLeash);
}
}
}

8.3 Single-Top RootTask的意义

分屏根RootTask采用single-top模式,这有深刻的设计考量。

Single-Top的含义

1
2
3
4
5
6
7
/**
* Single-Top模式的特性:
*
* 1. 唯一性:一个显示设备上只有一个分屏根RootTask
* 2. 顶层容器:MainStage和SideStage都是它的直接子容器
* 3. 统一管理:所有分屏相关的配置和状态在此统一管理
*/

为什么需要Single-Top?

原因1:统一的父容器

  • MainStage和SideStage作为兄弟节点,方便协调
  • 配置变化可以同时影响两个Stage
  • Z-order管理更简单

原因2:事件分发

1
2
3
4
5
// 触摸事件分发逻辑
// Input系统从顶层RootTask开始分发
Split Root Task
├─ MainStage → 如果触摸在bounds1内,分发给MainStage
└─ SideStage → 如果触摸在bounds2内,分发给SideStage

原因3:动画协调

1
2
3
4
5
6
7
// 进入分屏的动画
// 可以在Split Root Task层面协调MainStage和SideStage的动画
Transition.animate(mSplitRootTask, () -> {
// MainStage和SideStage的动画同步执行
mMainStage.animateEnter();
mSideStage.animateEnter();
});

原因4:资源隔离

  • 分屏相关的资源(Surface、Leash等)都在Split Root Task管理下
  • 退出分屏时,统一清理资源

8.4 事务原子性的重要性

WindowContainerTransaction的原子性是分屏稳定性的关键保证。

原子性的价值

问题场景(如果没有原子性):

1
2
3
4
5
6
7
8
9
10
11
// 假设逐个执行操作
reparent(taskA, mainStage); // ← 用户看到taskA移动
setBounds(taskA, bounds1); // ← 用户看到taskA调整大小
reparent(taskB, sideStage); // ← 用户看到taskB移动
setBounds(taskB, bounds2); // ← 用户看到taskB调整大小
showDivider(); // ← 用户看到分隔线出现

// 中间状态暴露给用户:
// - taskA已移动但边界错误
// - taskB还未移动,屏幕空缺
// - 分隔线还未显示,布局混乱

使用WCT后

1
2
3
4
5
6
7
8
9
10
11
WindowContainerTransaction wct = new WindowContainerTransaction();
wct.reparent(taskA, mainStage, true)
.setBounds(taskA, bounds1)
.reparent(taskB, sideStage, true)
.setBounds(taskB, bounds2)
.setHidden(divider, false);

mTaskOrganizer.applyTransaction(wct);

// 所有操作在一个VSync周期内完成
// 用户看到的是最终状态,没有中间过程

原子性的实现机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Framework: WindowContainerTransaction处理流程
void applyTransaction(WindowContainerTransaction wct) {
synchronized (mGlobalLock) {
// 1. 开始事务
beginTransaction();

try {
// 2. 执行所有HierarchyOps(reparent、reorder等)
for (HierarchyOp op : wct.getHierarchyOps()) {
applyHierarchyOp(op);
}

// 3. 应用所有Changes(bounds、windowingMode等)
for (Map.Entry<IBinder, Change> entry : wct.getChanges()) {
applyChange(entry.getKey(), entry.getValue());
}

// 4. 提交事务
// 此时才真正应用到窗口树
// 并触发Surface更新
commitTransaction();

} catch (Exception e) {
// 5. 出错时回滚
rollbackTransaction();
throw e;
}
}
}

分屏开发的最佳实践

规则1:总是使用WCT

1
2
3
4
5
6
7
8
9
// ✓ 正确
WindowContainerTransaction wct = new WindowContainerTransaction();
wct.setBounds(stage1, bounds1)
.setBounds(stage2, bounds2);
mTaskOrganizer.applyTransaction(wct);

// ✗ 错误
mTaskOrganizer.setBounds(stage1, bounds1); // 假设API存在
mTaskOrganizer.setBounds(stage2, bounds2);

规则2:批量操作使用单个WCT

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ✓ 正确:一个WCT包含所有操作
WindowContainerTransaction wct = new WindowContainerTransaction();
for (Task task : tasksToMove) {
wct.reparent(task.token, newParent, true);
wct.setBounds(task.token, null);
}
mTaskOrganizer.applyTransaction(wct);

// ✗ 错误:每个Task一个WCT
for (Task task : tasksToMove) {
WindowContainerTransaction wct = new WindowContainerTransaction();
wct.reparent(task.token, newParent, true);
wct.setBounds(task.token, null);
mTaskOrganizer.applyTransaction(wct); // 多次Binder调用
}

规则3:配置和Surface更新分离

1
2
3
4
5
6
7
8
9
10
11
// ✓ 正确:先更新配置,再同步Surface
WindowContainerTransaction wct = new WindowContainerTransaction();
wct.setBounds(stage1, bounds1)
.setBounds(stage2, bounds2);

mSyncQueue.queue(wct);
mSyncQueue.runInSync(t -> {
// Surface变换与配置变化同步
t.setPosition(stage1Leash, bounds1.left, bounds1.top);
t.setPosition(stage2Leash, bounds2.left, bounds2.top);
});

小结

本章总结了分屏Task组织的关键设计原则:

  1. 受控筛选:通过窗口模式和活动类型筛选适合分屏的Task
  2. 协调器规则:SideStage优先、Stage联动、分隔线可见性
  3. Single-Top架构:统一管理、简化事件分发、协调动画
  4. 事务原子性:保证操作一致性,避免中间状态

这些原则共同保证了分屏功能的稳定性和用户体验。下一章将提供实践建议和调试技巧。


第9章:实践建议与调试技巧

掌握了原理后,让我们看看如何在实际开发中应用这些知识,以及如何调试分屏相关问题。

9.1 通过代码启动分屏

了解如何通过代码启动分屏对于Framework开发和调试非常重要。本节介绍多种启动分屏的方法。

9.1.1 从SystemUI启动分屏

SystemUI是启动分屏的主要入口,通过SplitScreenController提供的接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// SystemUI中启动分屏的完整流程
public class SplitScreenController implements SplitScreen {

/**
* 启动Task到指定的分屏位置
*
* @param taskId 要启动的Task ID
* @param position 分屏位置(SPLIT_POSITION_TOP_OR_LEFT / SPLIT_POSITION_BOTTOM_OR_RIGHT)
*/
@Override
public void startTask(int taskId, @SplitPosition int position, @Nullable Bundle options) {
mMainExecutor.execute(() -> {
mStageCoordinator.startTask(taskId, position, options);
});
}

/**
* 启动Intent到分屏的指定位置
*
* @param intent 要启动的Intent
* @param position 分屏位置
* @param options 启动选项
*/
@Override
public void startIntent(PendingIntent intent, Intent fillInIntent,
@SplitPosition int position, @Nullable Bundle options) {
mMainExecutor.execute(() -> {
mStageCoordinator.startIntent(intent, fillInIntent, position, options);
});
}

/**
* 启动快捷方式到分屏
*
* @param packageName 应用包名
* @param shortcutId 快捷方式ID
* @param position 分屏位置
* @param options 启动选项
*/
@Override
public void startShortcut(String packageName, String shortcutId,
@SplitPosition int position, @Nullable Bundle options,
UserHandle user) {
mMainExecutor.execute(() -> {
mStageCoordinator.startShortcut(packageName, shortcutId, position, options, user);
});
}
}

实际使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 在Launcher或最近任务中拖拽应用到分屏
public class RecentTasksController {

@Inject
SplitScreenController mSplitScreenController;

/**
* 用户拖拽Task到分屏区域
*/
public void onTaskDroppedToSplitScreen(int taskId, int position) {
// 创建启动选项
Bundle options = ActivityOptions.makeBasic().toBundle();

// 启动Task到分屏
mSplitScreenController.startTask(taskId, position, options);

Log.d(TAG, "Task " + taskId + " started in split position " + position);
}

/**
* 用户长按应用图标选择分屏
*/
public void onAppLongPressSelectSplit(String packageName, int userId) {
// 创建启动Intent
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_LAUNCHER);
intent.setPackage(packageName);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

// 包装为PendingIntent
PendingIntent pendingIntent = PendingIntent.getActivity(
mContext, 0, intent, PendingIntent.FLAG_IMMUTABLE);

// 启动到分屏
mSplitScreenController.startIntent(
pendingIntent,
null,
SplitScreen.SPLIT_POSITION_TOP_OR_LEFT,
null
);
}
}

9.1.2 从应用内启动分屏

应用也可以请求以分屏模式启动另一个Activity。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// 应用内启动分屏的方法
public class MainActivity extends Activity {

/**
* 方法1:使用ActivityOptions启动相邻Activity
* 这是官方推荐的方式(Android 12+)
*/
private void launchAdjacentActivity() {
Intent intent = new Intent(this, SecondActivity.class);

// 创建分屏启动选项
ActivityOptions options = ActivityOptions.makeBasic();

// 设置启动模式为相邻(分屏)
options.setLaunchWindowingMode(WINDOWING_MODE_MULTI_WINDOW);

// 在Android 12+可以指定分屏位置
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// LAUNCH_ADJACENT_FLAG_ROOT_TASK_NOT_EMPTY 表示在另一侧启动
intent.addFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT |
Intent.FLAG_ACTIVITY_NEW_TASK);
}

startActivity(intent, options.toBundle());

Log.d(TAG, "Adjacent activity launched in split screen");
}

/**
* 方法2:使用FLAG_ACTIVITY_LAUNCH_ADJACENT标志
* 兼容老版本Android
*/
private void launchAdjacentWithFlag() {
Intent intent = new Intent(this, SecondActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT |
Intent.FLAG_ACTIVITY_NEW_TASK |
Intent.FLAG_ACTIVITY_MULTIPLE_TASK);

startActivity(intent);
}

/**
* 方法3:通过SystemUI的分屏服务启动
* 需要系统权限(仅供SystemUI或系统应用使用)
*/
@RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS)
private void launchViaSplitScreenService() {
try {
// 获取ActivityTaskManager服务
IActivityTaskManager atm = ActivityTaskManager.getService();

// 获取当前Task ID
int currentTaskId = getTaskId();

// 创建要启动的Intent
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("https://www.google.com"));
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

// 设置启动选项,指定分屏位置
ActivityOptions options = ActivityOptions.makeBasic();
options.setLaunchWindowingMode(WINDOWING_MODE_MULTI_WINDOW);

// 先将当前Task移到MainStage
// 然后启动新Task到SideStage

// 这需要调用隐藏API,实际应用无法直接使用
// 仅作为示例说明原理

} catch (Exception e) {
Log.e(TAG, "Failed to launch split screen", e);
}
}
}

应用Manifest配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- 应用需要支持分屏模式 -->
<application>
<activity
android:name=".MainActivity"
android:resizeableActivity="true"
android:supportsPictureInPicture="false"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation">

<!-- 声明支持多窗口模式 -->
<meta-data
android:name="android.supports_size_changes"
android:value="true" />
</activity>
</application>

9.1.3 通过ADB命令启动分屏

在开发和调试阶段,可以使用ADB命令快速测试分屏功能。

方法1:使用am命令启动分屏

1
2
3
4
5
6
7
8
9
10
11
12
# 1. 启动第一个应用到分屏主位置
adb shell am start -n com.example.app1/.MainActivity --windowingMode 6 --displayId 0

# 参数说明:
# --windowingMode 6 表示WINDOWING_MODE_MULTI_WINDOW(分屏模式)
# --displayId 0 表示主显示设备

# 2. 启动第二个应用到分屏副位置
adb shell am start -n com.example.app2/.MainActivity --windowingMode 6 --displayId 0

# 3. 或者使用stack命令(老版本Android)
adb shell am stack start --display 0 --windowingMode 6 com.example.app1/.MainActivity

方法2:使用TaskOrganizer Shell命令

1
2
3
4
5
6
7
8
9
10
11
12
13
# 1. 获取当前运行的Task列表
adb shell dumpsys activity activities | grep "TaskRecord"

# 输出示例:
# TaskRecord{abc123 #101 A=com.example.app1 U=0 StackId=1}
# TaskRecord{def456 #102 A=com.example.app2 U=0 StackId=2}

# 2. 通过wm命令操作分屏(如果系统支持)
adb shell wm split-screen enter

# 3. 移动Task到分屏位置
adb shell wm task move --task-id 101 --split-position top
adb shell wm task move --task-id 102 --split-position bottom

方法3:通过SystemUI的广播启动

1
2
3
4
5
6
7
8
# 发送分屏启动广播(需要SystemUI支持)
adb shell am broadcast -a com.android.systemui.action.ENTER_SPLIT_SCREEN \
--ei task_id 101 \
--ei position 0

# 参数说明:
# task_id: Task的ID(通过dumpsys获取)
# position: 0=TOP_OR_LEFT, 1=BOTTOM_OR_RIGHT

方法4:使用Shell脚本自动化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#!/bin/bash
# split_screen_launcher.sh - 自动启动分屏的脚本

# 配置
APP1_PACKAGE="com.example.app1"
APP1_ACTIVITY=".MainActivity"
APP2_PACKAGE="com.example.app2"
APP2_ACTIVITY=".MainActivity"

echo "Starting split screen mode..."

# 1. 启动第一个应用
echo "Launching $APP1_PACKAGE..."
adb shell am start -n $APP1_PACKAGE/$APP1_ACTIVITY

# 等待应用启动
sleep 2

# 2. 获取第一个应用的Task ID
TASK_ID_1=$(adb shell dumpsys activity activities | \
grep "TaskRecord.*$APP1_PACKAGE" | \
sed -n 's/.*#\([0-9]*\).*/\1/p' | \
head -1)

echo "App1 Task ID: $TASK_ID_1"

# 3. 进入分屏模式(将App1移到主位置)
adb shell am broadcast -a com.android.systemui.action.ENTER_SPLIT_SCREEN \
--ei task_id $TASK_ID_1 \
--ei position 0

sleep 1

# 4. 启动第二个应用到侧位置
echo "Launching $APP2_PACKAGE..."
adb shell am start -n $APP2_PACKAGE/$APP2_ACTIVITY

echo "Split screen launched successfully!"

9.1.4 通过Framework API启动(需要系统权限)

如果你在开发系统级应用或Framework服务,可以直接调用Framework的API。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
// Framework层启动分屏的方法
public class SplitScreenHelper {

private final Context mContext;
private final IActivityTaskManager mActivityTaskManager;

public SplitScreenHelper(Context context) {
mContext = context;
mActivityTaskManager = ActivityTaskManager.getService();
}

/**
* 启动分屏模式
* 需要权限:android.permission.MANAGE_ACTIVITY_TASKS
*/
@RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS)
public void startSplitScreen(int task1Id, int task2Id) throws RemoteException {

// 1. 创建WindowContainerTransaction
WindowContainerTransaction wct = new WindowContainerTransaction();

// 2. 获取Task信息
List<ActivityManager.RunningTaskInfo> tasks =
mActivityTaskManager.getTasks(Integer.MAX_VALUE);

ActivityManager.RunningTaskInfo task1 = findTaskById(tasks, task1Id);
ActivityManager.RunningTaskInfo task2 = findTaskById(tasks, task2Id);

if (task1 == null || task2 == null) {
Log.e(TAG, "Task not found");
return;
}

// 3. 创建分屏根Task(如果不存在)
// 这需要通过ITaskOrganizer完成
ITaskOrganizer organizer = ITaskOrganizer.Stub.asInterface(
ServiceManager.getService("task_organizer"));

if (organizer != null) {
// 创建分屏布局
// 这里简化了流程,实际需要更复杂的设置

// 4. 设置Task的窗口模式
wct.setWindowingMode(task1.token, WINDOWING_MODE_MULTI_WINDOW);
wct.setWindowingMode(task2.token, WINDOWING_MODE_MULTI_WINDOW);

// 5. 设置Task的边界
DisplayMetrics metrics = mContext.getResources().getDisplayMetrics();
int screenWidth = metrics.widthPixels;
int screenHeight = metrics.heightPixels;

Rect bounds1 = new Rect(0, 0, screenWidth, screenHeight / 2);
Rect bounds2 = new Rect(0, screenHeight / 2, screenWidth, screenHeight);

wct.setBounds(task1.token, bounds1);
wct.setBounds(task2.token, bounds2);

// 6. 应用事务
mActivityTaskManager.applyContainerTransaction(wct, null);

Log.d(TAG, "Split screen started successfully");
}
}

/**
* 根据Task ID查找Task信息
*/
private ActivityManager.RunningTaskInfo findTaskById(
List<ActivityManager.RunningTaskInfo> tasks, int taskId) {
for (ActivityManager.RunningTaskInfo task : tasks) {
if (task.taskId == taskId) {
return task;
}
}
return null;
}

/**
* 退出分屏模式
*/
@RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS)
public void exitSplitScreen() throws RemoteException {
// 通过SystemUI的SplitScreenController退出
// 或者重置Task的窗口模式

List<ActivityManager.RunningTaskInfo> tasks =
mActivityTaskManager.getTasks(Integer.MAX_VALUE);

WindowContainerTransaction wct = new WindowContainerTransaction();

for (ActivityManager.RunningTaskInfo task : tasks) {
if (task.getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW) {
// 重置为全屏模式
wct.setWindowingMode(task.token, WINDOWING_MODE_FULLSCREEN);
wct.setBounds(task.token, null);
}
}

mActivityTaskManager.applyContainerTransaction(wct, null);
}
}

9.1.5 启动分屏的完整示例

以下是一个完整的工具类,整合了多种启动分屏的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
/**
* 分屏启动工具类
* 提供多种方式启动和控制分屏
*/
public class SplitScreenLauncher {

private final Context mContext;
private final SplitScreenController mSplitScreenController;

public SplitScreenLauncher(Context context) {
mContext = context;
// 从Dependency注入获取SplitScreenController
mSplitScreenController = Dependency.get(SplitScreenController.class);
}

/**
* 启动两个应用到分屏
*
* @param package1 第一个应用的包名
* @param package2 第二个应用的包名
*/
public void launchTwoAppsInSplitScreen(String package1, String package2) {
// 1. 先启动第一个应用到主位置
launchAppInSplit(package1, SplitScreen.SPLIT_POSITION_TOP_OR_LEFT);

// 2. 延迟后启动第二个应用到侧位置
new Handler(Looper.getMainLooper()).postDelayed(() -> {
launchAppInSplit(package2, SplitScreen.SPLIT_POSITION_BOTTOM_OR_RIGHT);
}, 500); // 延迟500ms确保第一个应用已启动
}

/**
* 启动单个应用到指定的分屏位置
*/
private void launchAppInSplit(String packageName, @SplitPosition int position) {
// 创建启动Intent
Intent intent = mContext.getPackageManager()
.getLaunchIntentForPackage(packageName);

if (intent == null) {
Log.e(TAG, "Cannot find launch intent for package: " + packageName);
return;
}

intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

// 包装为PendingIntent
PendingIntent pendingIntent = PendingIntent.getActivity(
mContext, 0, intent,
PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);

// 通过SplitScreenController启动
mSplitScreenController.startIntent(pendingIntent, null, position, null);
}

/**
* 将运行中的Task移动到分屏
*/
public void moveTaskToSplitScreen(int taskId, @SplitPosition int position) {
mSplitScreenController.startTask(taskId, position, null);
}

/**
* 退出分屏模式
*/
public void exitSplitScreen() {
mSplitScreenController.exitSplitScreen(
SplitScreenController.EXIT_REASON_UNKNOWN);
}

/**
* 检查分屏是否激活
*/
public boolean isSplitScreenActive() {
return mSplitScreenController.isSplitScreenVisible();
}
}

// 使用示例
public class SplitScreenTestActivity extends Activity {

private SplitScreenLauncher mLauncher;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);

mLauncher = new SplitScreenLauncher(this);

// 点击按钮启动分屏
findViewById(R.id.btn_launch_split).setOnClickListener(v -> {
// 启动Chrome和Gmail到分屏
mLauncher.launchTwoAppsInSplitScreen(
"com.android.chrome",
"com.google.android.gm"
);
});

// 退出分屏
findViewById(R.id.btn_exit_split).setOnClickListener(v -> {
mLauncher.exitSplitScreen();
});
}
}

启动分屏的注意事项

  1. 权限要求

    • 普通应用只能使用FLAG_ACTIVITY_LAUNCH_ADJACENT
    • 系统应用需要MANAGE_ACTIVITY_TASKS权限
    • SystemUI可以直接使用SplitScreenController
  2. 应用兼容性

    • 目标应用必须声明android:resizeableActivity="true"
    • 应用需要正确处理onConfigurationChanged回调
    • 避免启动不支持分屏的应用(如固定方向的应用)
  3. 时序控制

    • 两个应用的启动需要适当的延迟
    • 等待第一个应用完全启动后再启动第二个
    • 监听分屏状态变化以确保操作成功
  4. 错误处理

    • 检查应用是否存在和可启动
    • 处理分屏启动失败的情况
    • 提供回退方案(如全屏启动)

9.2 调试工具与方法

使用dumpsys查看Task层次结构

基础命令

1
2
3
4
5
6
7
8
9
10
11
# 查看完整的WindowContainer层次结构
adb shell dumpsys activity containers

# 输出示例(分屏模式):
# DisplayContent
# TaskDisplayArea
# Task id=10 (SplitScreenRoot)
# Task id=11 (MainStage) bounds=[0,0,1080,1200]
# Task id=101 WeChat bounds=[0,0,1080,1200]
# Task id=12 (SideStage) bounds=[0,1200,1080,2400]
# Task id=102 Gmail bounds=[0,1200,1080,2400]

筛选特定Task

1
2
3
4
5
6
7
8
9
10
# 查看特定Task的详细信息
adb shell dumpsys activity containers | grep -A 20 "Task id=101"

# 输出:
# Task id=101
# type=standard
# windowingMode=multi-window
# bounds=[0,0,1080,1200]
# isVisible=true
# topActivity=com.tencent.mm/.ui.LauncherUI

【图9-1:dumpsys输出解读】

1
2
3
4
5
6
7
8
9
Task id=101 WeChat                     ← Task标识和应用名
type=standard ← 活动类型
windowingMode=multi-window ← 窗口模式(分屏的关键标志)
bounds=[0,0,1080,1200] ← Task边界(注解:上半屏)
appBounds=[0,75,1080,1200] ← 应用可用边界
isVisible=true ← 可见性
topActivity=...LauncherUI ← 顶部Activity
numActivities=2 ← Activity数量
parent=Task id=11 (MainStage) ← 父容器(注解:在MainStage中)

使用ProtoLog追踪分屏事件

启用ProtoLog

1
2
3
4
5
6
7
8
# 启用分屏相关的日志
adb shell wm logging enable-text WM_SHELL_SPLIT_SCREEN

# 启用Task组织相关的日志
adb shell wm logging enable-text WM_SHELL_TASK_ORG

# 查看日志
adb logcat | grep "WM_SHELL"

关键日志点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Task进入分屏
WM_SHELL_SPLIT_SCREEN: StageTaskListener.addTask: taskId=101
WM_SHELL_SPLIT_SCREEN: Stage.activate: reparentAllTasks=false
WM_SHELL_SPLIT_SCREEN: updateWindowBounds: bounds1=[0,0,1080,1200] bounds2=[0,1200,1080,2400]

# Task被reparent
WM_SHELL_TASK_ORG: onTaskAppeared: taskId=101 isVisible=true
WM_SHELL_SPLIT_SCREEN: onSplitScreenEnter: split screen is now active

# 边界更新
WM_SHELL_SPLIT_SCREEN: onDraggingDivider: position=1000
WM_SHELL_SPLIT_SCREEN: onDraggingDividerFinished: position=1000
WM_SHELL_SPLIT_SCREEN: updateWindowBounds: bounds1=[0,0,1080,1000] bounds2=[0,1000,1080,2400]

# 退出分屏
WM_SHELL_SPLIT_SCREEN: exitSplitScreen: reason=APP_FINISHED
WM_SHELL_SPLIT_SCREEN: Stage.deactivate
WM_SHELL_SPLIT_SCREEN: onSplitScreenExit: exiting split screen

使用WM Shell调试命令

1
2
3
4
5
6
7
8
9
10
11
12
# 查看分屏状态
adb shell dumpsys activity service SystemUIService WMShell | grep -A 50 "SplitScreen"

# 输出:
# SplitScreenController:
# isActive: true
# mainStagePosition: TOP_OR_LEFT
# sideStagePosition: BOTTOM_OR_RIGHT
# mainStageTaskCount: 1
# sideStageTaskCount: 1
# dividePosition: 1200
# splitRatio: 0.5

9.2 常见问题与解决方案

问题1:Task丢失或消失

现象
进入分屏后,某个Task不可见或消失

可能原因

  1. Task被错误地reparent到了null
  2. Task的bounds设置错误(如空Rect)
  3. Task被标记为hidden
  4. Task不符合CONTROLLED筛选条件

调试步骤

1
2
3
4
5
6
7
8
9
10
11
# 1. 确认Task是否还存在
adb shell dumpsys activity containers | grep "Task id=101"

# 2. 检查Task的父容器
# 如果parent为null或不是Stage,说明reparent出错

# 3. 检查Task的可见性和边界
# isVisible=false 或 bounds=[] 说明状态异常

# 4. 检查ProtoLog日志
adb logcat | grep "taskId=101"

解决方案

1
2
3
4
5
6
7
// 确保reparent时正确设置父容器
wct.reparent(task.token, stage.mRootTaskInfo.token, true /* onTop */);
// 而不是
wct.reparent(task.token, null, true); // ✗ 错误:null会让Task回到TDA根部

// 确保清除Task自身边界,继承Stage
wct.setBounds(task.token, null); // null表示继承,不是设置为空

问题2:边界计算错误

现象
Task显示区域不正确,或与预期的分屏比例不符

可能原因

  1. SplitLayout计算错误
  2. 分隔线大小未考虑
  3. 边界未正确传播到Task
  4. 配置变化未触发

调试步骤

1
2
3
4
5
6
# 查看Stage和Task的bounds
adb shell dumpsys activity containers | grep -E "(Stage|Task id)" -A 5

# 检查是否一致:
# Stage bounds: [0,0,1080,1200]
# Task bounds: [0,0,1080,1200] ← 应该一致

解决方案

1
2
3
4
5
6
7
8
9
10
11
// 确保边界计算考虑分隔线
mBounds1.set(
mRootBounds.left,
mRootBounds.top,
mRootBounds.right,
dividePosition - mDividerSize / 2 // 减去分隔线的一半
);

// 确保Task继承Stage边界
wct.setBounds(stage.token, stageBounds);
wct.setBounds(task.token, null); // 继承Stage边界

问题3:状态不同步

现象
Task的可见性、焦点等状态与实际不符

可能原因

  1. onTaskInfoChanged回调未正确处理
  2. 状态缓存未更新
  3. 并发问题导致状态不一致

调试步骤

1
2
3
4
5
6
7
8
9
10
# 监听Task状态变化
adb logcat | grep "onTaskInfoChanged"

# 检查状态缓存
# 在StageTaskListener中添加日志:
ProtoLog.d(WM_SHELL_SPLIT_SCREEN,
"onTaskInfoChanged: taskId=%d cached_visible=%b new_visible=%b",
taskInfo.taskId,
mChildrenTaskInfo.get(taskInfo.taskId).isVisible,
taskInfo.isVisible);

解决方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 始终更新缓存
@Override
public void onTaskInfoChanged(RunningTaskInfo taskInfo) {
// 先更新缓存
mChildrenTaskInfo.put(taskInfo.taskId, taskInfo);

// 再处理变化
handleTaskInfoChange(taskInfo);
}

// 使用同步保护并发访问
private synchronized void handleTaskInfoChange(RunningTaskInfo taskInfo) {
// 处理逻辑
}

9.3 修改分屏逻辑的注意事项

注意事项1:保持事务原子性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ✓ 正确:所有操作在一个WCT中
void enterSplitScreen() {
WindowContainerTransaction wct = new WindowContainerTransaction();
prepareStages(wct);
reparentTasks(wct);
updateBounds(wct);
mTaskOrganizer.applyTransaction(wct);
}

// ✗ 错误:多个WCT分步执行
void enterSplitScreen() {
WindowContainerTransaction wct1 = new WindowContainerTransaction();
prepareStages(wct1);
mTaskOrganizer.applyTransaction(wct1); // 第一次提交

WindowContainerTransaction wct2 = new WindowContainerTransaction();
reparentTasks(wct2);
mTaskOrganizer.applyTransaction(wct2); // 第二次提交
}

注意事项2:处理边界情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 始终检查Stage是否激活
if (!isSplitScreenActive()) {
// 分屏未激活,不应该操作Stage中的Task
return;
}

// 始终检查Task是否存在
if (!mChildrenTaskInfo.contains(taskId)) {
ProtoLog.w(WM_SHELL_SPLIT_SCREEN, "Task not found: taskId=%d", taskId);
return;
}

// 始终检查参数有效性
if (bounds == null || bounds.isEmpty()) {
ProtoLog.e(WM_SHELL_SPLIT_SCREEN, "Invalid bounds: %s", bounds);
return;
}

注意事项3:尊重Task可见性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 在操作Task前检查可见性
void moveTaskToStage(int taskId, StageTaskListener targetStage) {
final RunningTaskInfo taskInfo = getTaskInfo(taskId);

// 不可见的Task不应该被移动到前台Stage
if (!taskInfo.isVisible && targetStage == getCurrentFocusedStage()) {
ProtoLog.w(WM_SHELL_SPLIT_SCREEN,
"Attempting to move invisible task to focused stage");
return;
}

WindowContainerTransaction wct = new WindowContainerTransaction();
targetStage.addTask(taskInfo, wct);
mTaskOrganizer.applyTransaction(wct);
}

9.4 性能优化建议

优化1:减少不必要的WCT提交

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ✓ 优化:合并连续的配置变化
private WindowContainerTransaction mPendingWct;
private Handler mHandler;

void updateTaskBounds(int taskId, Rect bounds) {
if (mPendingWct == null) {
mPendingWct = new WindowContainerTransaction();
mHandler.postDelayed(this::flushPendingTransaction, 16); // 一帧后提交
}

mPendingWct.setBounds(getTaskToken(taskId), bounds);
}

private void flushPendingTransaction() {
if (mPendingWct != null) {
mTaskOrganizer.applyTransaction(mPendingWct);
mPendingWct = null;
}
}

优化2:使用Surface Transaction处理动画

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 拖动分隔线时,使用Surface Transaction而不是WCT
void onDraggingDivider(int position) {
// 计算新边界
Rect bounds1 = calculateBounds1(position);
Rect bounds2 = calculateBounds2(position);

// 直接操作Surface,不触发配置变化
SurfaceControl.Transaction t = new SurfaceControl.Transaction();
t.setPosition(mMainStageLeash, bounds1.left, bounds1.top);
t.setWindowCrop(mMainStageLeash, bounds1.width(), bounds1.height());
t.setPosition(mSideStageLeash, bounds2.left, bounds2.top);
t.setWindowCrop(mSideStageLeash, bounds2.width(), bounds2.height());
t.apply();
}

// 拖动结束后,才使用WCT更新配置
void onDraggingDividerFinished(int position) {
WindowContainerTransaction wct = new WindowContainerTransaction();
updateWindowBounds(wct, position);
mTaskOrganizer.applyTransaction(wct);
}

优化3:缓存Task信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ✓ 优化:缓存Task信息,减少查询
private final SparseArray<RunningTaskInfo> mTaskInfoCache = new SparseArray<>();

RunningTaskInfo getTaskInfo(int taskId) {
return mTaskInfoCache.get(taskId);
}

@Override
public void onTaskInfoChanged(RunningTaskInfo taskInfo) {
// 更新缓存
mTaskInfoCache.put(taskInfo.taskId, taskInfo);
}

// ✗ 未优化:每次都查询
RunningTaskInfo getTaskInfo(int taskId) {
return mTaskOrganizer.getTaskInfo(taskId); // Binder调用,性能差
}

小结

本章提供了实用的开发和调试建议:

  1. 调试工具:dumpsys、ProtoLog、WM Shell命令
  2. 常见问题:Task丢失、边界错误、状态不同步的诊断和解决
  3. 开发注意事项:原子性、边界情况、可见性检查
  4. 性能优化:减少WCT提交、使用Surface Transaction、缓存优化

下一章将对全文进行总结。


第10章:总结与展望

经过前面九章的深入分析,我们完整地剖析了Android分屏功能中Task组织与管理的实现机制。让我们回顾核心要点,并展望未来的发展方向。

10.1 核心要点回顾

WindowContainer体系:分屏的基础架构

Android的窗口管理建立在树形的WindowContainer体系之上:

  • DisplayContentTaskDisplayAreaRootTaskTaskActivityRecord
  • 每个层级都是容器,既可包含子节点,也可被父节点管理
  • 配置从父容器向下继承和传播,子容器可选择性覆盖

Task Reparenting:分屏的核心机制

Task在不同RootTask间迁移而无需重启应用:

  • Reparenting将Task从当前父容器移到新父容器
  • Task保持运行状态,Activity栈完整保留
  • 配置自动重算,应用收到onConfigurationChanged回调

WindowContainerTransaction:原子性的保证

所有Task操作都通过WCT事务机制执行:

  • 批量操作打包成原子事务
  • 要么全部成功,要么全部失败
  • 避免中间状态暴露给用户,保证流畅体验

Stage架构:分屏的组织模式

MainStage和SideStage是分屏的两个独立区域:

  • 每个Stage有自己的RootTask容器
  • Task被reparent到Stage下,继承Stage的边界和配置
  • StageCoordinator协调两个Stage的生命周期和交互

边界管理:精细的显示控制

三种边界类型服务不同目的:

  • Bounds:Task的显示区域
  • AppBounds:应用可用的绘制区域(扣除系统UI)
  • MaxBounds:可扩展到的最大区域

设计原则:稳定性的基石

严格的规则确保分屏功能的稳定:

  • 只有特定窗口模式和活动类型的Task可进入分屏
  • SideStage优先:有Task才激活分屏
  • Single-Top架构:统一管理、简化事件分发
  • 事务原子性:保证操作一致性

10.2 技术价值与应用

对Framework开发者

掌握Task组织机制后,你可以:

  • 理解多窗口体系:不仅是分屏,还有PIP、Freeform等
  • 调试复杂问题:快速定位Task层次、边界、状态问题
  • 扩展分屏功能:实现自定义的多窗口模式
  • 优化性能:减少不必要的配置变化和重绘

对应用开发者

理解分屏机制有助于:

  • 适配分屏模式:正确处理配置变化,优化布局
  • 测试多窗口场景:了解Task状态变化,覆盖边界情况
  • 利用分屏特性:如启动Activity到特定Stage

对系统架构师

分屏实现体现了Android的设计哲学:

  • 容器化架构:递归的WindowContainer设计
  • 配置继承机制:高效的属性传播
  • 事务模式:保证系统状态一致性
  • 关注点分离:Framework层、Shell层、应用层各司其职

10.3 与其他多窗口模式的对比

画中画(Picture-in-Picture)

相似点

  • 也使用RootTask组织Task
  • 也通过reparent进入PIP模式
  • 也有边界管理和配置变化

不同点

  • PIP是单Task模式,分屏是双Task模式
  • PIP窗口可拖动和调整大小,分屏的Task边界固定在Stage内
  • PIP有自己的WINDOWING_MODE_PINNED

自由形式窗口(Freeform)

相似点

  • 同样基于Task和RootTask
  • 同样需要边界管理

不同点

  • Freeform允许任意数量的窗口
  • 每个Task有独立的边界和Z-order
  • 没有Stage概念,Task直接在TDA下
  • 使用WINDOWING_MODE_FREEFORM

桌面模式(Desktop Mode)

相似点

  • 利用WindowContainer体系
  • 支持多Task并行显示

不同点

  • 更接近传统桌面操作系统
  • Task可最大化、最小化、关闭
  • 有任务栏和窗口装饰

10.4 未来演进方向

更灵活的分屏布局

当前分屏限制为两个Stage,未来可能:

  • 三分屏或四分屏:支持更多Stage
  • 可调整的Stage数量:根据屏幕尺寸动态调整
  • 嵌套分屏:Stage内部再次分屏

更智能的Task分配

AI辅助的分屏体验:

  • 智能推荐:根据使用场景推荐分屏组合
  • 自动分配:新启动的Task自动进入合适的Stage
  • 场景记忆:记住用户的分屏习惯

跨设备的分屏协同

多设备联动:

  • 跨屏分屏:Task跨多个物理显示设备
  • 设备间迁移:Task在手机、平板、PC间迁移
  • 统一管理:云端同步分屏配置

性能和能效优化

技术改进:

  • 更高效的边界计算:减少配置变化开销
  • 更好的动画性能:利用GPU加速
  • 更智能的资源管理:后台Stage的Task进入休眠

10.5 结语

Android分屏功能的Task组织机制是一个精心设计的系统,它展现了:

  • 清晰的架构设计:WindowContainer体系提供灵活的容器模型
  • 优雅的实现方式:Reparenting和配置继承机制简洁高效
  • 严格的工程规范:原子事务和设计原则保证稳定性

对于Framework开发者而言,深入理解这套机制不仅有助于开发和调试分屏功能,更能从中学习到Android系统设计的精髓——如何在复杂性和可维护性之间找到平衡

希望这篇文章能成为你探索Android多窗口世界的起点。分屏只是开始,更广阔的多窗口生态等待我们去发现和创造。


附录:参考资料

  1. 源码路径

    • Framework层:frameworks/base/services/core/java/com/android/server/wm/
    • Shell层:frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/
  2. 关键类索引

    • WindowContainer.java:容器基类
    • Task.java:Task实现
    • WindowContainerTransaction.java:事务机制
    • StageCoordinator.java:分屏协调器
    • SplitLayout.java:分屏布局计算
  3. 调试命令速查

    1
    2
    3
    4
    5
    6
    7
    8
    # 查看Task层次
    adb shell dumpsys activity containers

    # 查看分屏状态
    adb shell dumpsys activity service SystemUIService WMShell

    # 启用分屏日志
    adb shell wm logging enable-text WM_SHELL_SPLIT_SCREEN


作者说明

本文基于Android 15源码分析,不同Android版本的实现可能有差异。文中的代码示例经过简化以突出核心逻辑,实际源码包含更多的边界检查和错误处理。

源码版本:Android 15 (API Level 35)
分析时间:2025年10月