1. Android14 ShellTransitions机制概览
在Android14的窗口管理系统中,ShellTransitions扮演着动画调度的核心角色。想象一下你正在玩一款拼图游戏——Transition就像那个负责协调所有拼图块移动节奏的指挥家,而WindowContainer就是那些等待被摆放的拼图块。这套机制最精妙之处在于,它能在毫秒级时间内判断哪些"拼图块"需要参与当前动画,以及它们应该以什么顺序移动。
我曾在实际项目中遇到过这样的场景:当用户从相册应用返回桌面时,系统需要同时处理相册窗口的退出动画和桌面图标的入场动画。这时候Transition系统就会启动COLLECTING状态,像磁铁一样吸附所有相关的WindowContainer对象。关键点在于,这个收集过程不是无脑抓取,而是通过Transition.collect方法进行智能筛选,就像安检仪能自动识别需要开箱检查的行李。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 参与者收集的核心逻辑剖析
2.1 状态校验的双重保险
Transition在收集参与者时,首先会检查自身的mState变量。这就像参加音乐会需要先验票——只有持有STATE_COLLECTING或STATE_STARTED状态"门票"的WindowContainer才能入场。具体校验逻辑如下:
java复制if (mState < STATE_COLLECTING) {
throw new IllegalStateException("Can't collect in state: " + mState);
}
if (mState != STATE_COLLECTING && mState != STATE_STARTED) {
return;
}
实测发现,这种设计能有效避免两种极端情况:一是动画还没准备好就仓促收集参与者(状态过早),二是动画已经结束才姗姗来迟的参与者(状态过晚)。记得去年调试一个多窗口bug时,就是因为有个Activity在STATE_RUNNING状态才尝试加入Transition,导致动画错乱。
2.2 needSync的智能判断
needSync就像交通信号灯,决定参与者是否需要进入同步流程。它的判断条件非常讲究:
- 排除壁纸窗口:WallpaperWindowToken就像舞台背景布,不需要参与前台元素的动画同步
- 显示内容检测:参与者必须属于当前DisplayContent管辖范围
- 可见性检查:被Recents临时遮挡的窗口(瞬态隐藏状态)会被过滤
这让我想起调试折叠屏适配时的案例:当主屏和副屏同时有窗口时,Transition会通过DisplayContent判断只收集当前活动屏幕的窗口。下面是典型判断逻辑:
java复制boolean needSync = !(wc instanceof WallpaperWindowToken)
&& (mParticipants.contains(wc.getDisplayContent())
&& !wc.isTransientlyHidden());
3. 同步组与参与者集合的协作
3.1 SyncGroup的绑定机制
当needSync为true时,系统会调用BLASTSyncEngine.addToSyncSet。这个过程就像给旅行团成员分发同款徽章:
- 如果WindowContainer还没加入任何SyncGroup(
getSyncGroup()==null),就将其加入mRootMembers列表 - 调用
setSyncGroup建立双向绑定关系 - 触发
prepareSync准备同步
特别要注意的是mRootMembers和mParticipants的区别:前者是经过严格筛选的核心成员,后者则是所有可能的参与者。就像公司年会,mRootMembers相当于必须上台领奖的优秀员工,而mParticipants包含所有参会人员。
3.2 三级就绪状态机
WindowContainer使用mSyncState管理同步状态,这就像快递包裹的物流跟踪:
- SYNC_STATE_NONE:刚下单(未参与同步)
- SYNC_STATE_WAITING_FOR_DRAW:运输中(等待绘制完成)
- SYNC_STATE_READY:已签收(可执行动画)
最精妙的是不同类型容器的差异化处理:
java复制// WindowState需要等待绘制
void prepareSync() {
mSyncState = SYNC_STATE_WAITING_FOR_DRAW;
}
// ActivityRecord/Task直接就绪
void prepareSync() {
for (child : mChildren) child.prepareSync();
mSyncState = SYNC_STATE_READY;
}
这种设计使得系统可以智能判断:当Activity的所有WindowState都绘制完成时,整个Activity才被视为就绪状态。在优化启动速度时,我们经常通过hookfinishDrawing来监控各个窗口的绘制耗时。
4. ChangeInfo的时空快照
4.1 变更记录的创建逻辑
Transition.mChanges就像动画导演的场记板,记录每个参与者的初始状态。以启动新Activity为例:
- 新建的ActivityRecord没有父容器,直接创建ChangeInfo
- 将<WindowContainer, ChangeInfo>键值对存入Map
这相当于给每个演员拍了定妆照,等动画开始时再对比当前状态,就知道需要什么样的过渡效果。ChangeInfo的构造过程如下:
java复制ChangeInfo info = new ChangeInfo(wc);
mChanges.put(wc, info);
4.2 层级穿透的收集策略
在应用切换场景中,收集策略会变得更有趣。比如从信息应用返回桌面时:
- 信息应用的Task被收集
- 通过
getAnimatableParent找到TaskDisplayArea - 继续向上获取DisplayContent
- 为这三个层级都创建ChangeInfo
这种设计使得系统能处理复杂的多层级动画。我遇到过这样的案例:当折叠屏设备展开时,需要同时处理DisplayContent尺寸变化和Task布局调整,这时候层级穿透收集就非常关键。
5. 实战中的收集场景
5.1 Activity启动流程
在ActivityStarter.startActivityUnchecked中,典型的收集顺序是:
- 创建Transition并进入COLLECTING状态
- 将openingApps中的ActivityRecord加入收集
- 处理相关Task和DisplayArea
这里有个容易踩坑的点:如果新Activity采用透明主题,需要特别注意对isTransientlyHidden的判断,否则可能导致背景窗口意外参与动画。
5.2 多窗口模式切换
当进入分屏模式时,系统会:
- 收集两个Task作为主要参与者
- 自动包含它们的父TaskDisplayArea
- 根据
needSync决定是否等待绘制
在实际开发中,我们发现分屏动画的流畅度很大程度上取决于prepareSync的调用时机。过早开始动画会导致画面撕裂,过晚则会影响用户体验。
