【Android 14源码分析】ShellTransitions-3-动画前准备

793 阅读8分钟

忽然有一天,我想要做一件事:去代码中去验证那些曾经被“灌输”的理论。

                        -- 服装学院的IT男

建议阅读顺序:

BLASTSyncEngine设计剖析

ShellTransitions总体流程介绍

ShellTransitions-1-同步组初始化

ShellTransitions-2-requestStartTransition处理

ShellTransitions-3-动画前准备

ShellTransitions-4-播放动画与结束处理

1. 概览

SyncGroup::setReady 已经设置为 true ,那后续就是在每次刷新的时候去检查同步组里的容器是不是都绘制完成了。

用不完成会执行创建同步组传递的 TransactionReadyListener::onTransactionReady 回调,当前场景传递的 TransactionReadyListener 实现是 Transition ,所以后续逻辑看 Transition::onTransactionReady 即可。

# Transition

    /** Only use for clean-up after binder death! */
    // 仅用于死亡后的清理!
    private SurfaceControl.Transaction mStartTransaction = null;
    private SurfaceControl.Transaction mFinishTransaction = null;

    ArrayList<ChangeInfo> mTargets;

    @Override
    public void onTransactionReady(int syncId, SurfaceControl.Transaction transaction) {
        if (syncId != mSyncId) {
            Slog.e(TAG, "Unexpected Sync ID " + syncId + ". Expected " + mSyncId);
            return;
        }
        ......
        // 1. 设置状态STATE_PLAYING
        mState = STATE_PLAYING;
        // 2.1 保存回调参数事务到 mStartTransaction
        mStartTransaction = transaction;
        // 2.2 创建一个事务,作为“结束”事务
        mFinishTransaction = mController.mAtm.mWindowManager.mTransactionFactory.get();
        ......
        // Resolve the animating targets from the participants.
        // 3. 计算动画目标
        mTargets = calculateTargets(mParticipants, mChanges);
        ......
        // 4. 构建出 TransitionInfo ,里面包含动画图层,图层名为:"Transition Root: XXX"
        final TransitionInfo info = calculateTransitionInfo(mType, mFlags, mTargets, transaction);
        ......
        // 移动事务为播放状态
        // 当前场景只是添加 transition 到 mPlayingTransitions 集合
        mController.moveToPlaying(this);
        ......
        // 2.3 构建动画结束后的处理事务
        buildFinishTransaction(mFinishTransaction, info);
        ......
                // 5. 第二次 WMcore --> WMShell 
                mController.getTransitionPlayer().onTransitionReady(
                        mToken, info, transaction, mFinishTransaction);
        ......
    }

    1. Transition 的状态在这里被设置为 STATE_PLAYING
    1. 开始和结束的 SurfaceControl.Transaction 处理赋值
    1. 计算动画的目标容器
    1. 根据目标容器创建 TransitionInfo ,内部逻辑会创建对应的 leash 图层
    1. 触发第二次 WMcore --> WMShell 调用,这次会在 SystemUI 端播放动画

本篇后续的内容都将围绕在这些逻辑分析。

2. StartTransaction 和 FinishTransaction

在 Transition 下定义了2个 SurfaceControl.Transaction 类型的成员变量:mStartTransaction 和 mFinishTransaction 。 从代码上来看这2个变量仅在方法内使用和传递,但是却被定义为了全局变量,根据注释可以理解,是为了死亡后的清理。那暂时就不需要关注其他地方的使用,尽关系当前方法内的调用即可。

mStartTransaction 的赋值其实就是 onTransactionReady 回调里的参数,而这个参数我们知道就是同步组所有容器绘制的事务总集。并且这个 SurfaceControl.Transaction 也被传递到了 SystemUI 做动画了, 根据代码的使用,这个 mStartTransaction 是用来做新的应用“开始进入”的动画,而 mFinishTransaction 则是在动画结束后的一些处理,比如动画结束后,动画目标容器需要恢复到原来的父容器之下。 在之前的版本动画结束后再通过“mAnimatable.getSyncTransaction()”拿到容器的事务处理将动画图层移除恢复到动画前的层级结构,现在直接在动画开始前就构建好了 mFinishTransaction ,作为动画结束后的处理事务。 这个事务还有一些别的操作不过都是在 SystemUI 执行完动画后的操作。

2.1 buildFinishTransaction

下面方法的执行时机是在创建完 TransitionInfo 后,也就是做动画的“ Transition Root:”图层也创建完毕了。 是动画结束回的图层的一些操作。

# Transition
    private void buildFinishTransaction(SurfaceControl.Transaction t, TransitionInfo info) {
        final Point tmpPos = new Point();
        // usually only size 1
        final ArraySet<DisplayContent> displays = new ArraySet<>();
        // 当前场景有2个目标 Task ,分别为桌面和新应用的
        for (int i = mTargets.size() - 1; i >= 0; --i) {
            final WindowContainer target = mTargets.get(i).mContainer;
            if (target.getParent() != null) {
                final SurfaceControl targetLeash = getLeashSurface(target, null /* t */);
                final SurfaceControl origParent = getOrigParentSurface(target);
                // Ensure surfaceControls are re-parented back into the hierarchy.
                // 把动画目标容器 图层进行 reparent  ,当前是动画结束后执行,所以 2 个Task还是需要挂回 DefaultTaskDisplayArea
                t.reparent(targetLeash, origParent);
                ......
                t.setMatrix(targetLeash, 1, 0, 0, 1);
                Log.d("biubiubiu", "buildFinishTransaction: targetLeash = "+ targetLeash);
                // 透明度设置回1
                t.setAlpha(targetLeash, 1);
                ......
            }
        }
        ......

        for (int i = 0; i < info.getRootCount(); ++i) {
            // 设置动画图层父节点为 null (等价于移除)
            t.reparent(info.getRoot(i).getLeash(), null);
        }
    }

3. 计算动画容器

这一步和以前的类似,都是计算出动画目标层级,只不过计算规则和一样不一样,但是代码设计思想都是一样的,就是要找到一个最合适执行动画的目标图层,然后创建动画 leash 图层进行挂载。

这里直观的比较下 T 和 U 动画图层的区别:

T 版本的 AppTransition 动画图层名字是“app_transition",leash 图层是目标是在一个 Task 前面, 桌面和新启动的各有一个 “app_transition"。

图层-T.png

U 版本上可以看到动画图层是“ Transition Root:”,而且这个图层下面有 桌面和新启动的应用的2个 Task

图层-U.png

# Transition

    /**
     * Find WindowContainers to be animated from a set of opening and closing apps. We will promote
     * animation targets to higher level in the window hierarchy if possible.
     */
    @VisibleForTesting
    @NonNull
    static ArrayList<ChangeInfo> calculateTargets(ArraySet<WindowContainer> participants,
            ArrayMap<WindowContainer, ChangeInfo> changes) {
        // 打印有哪些容器参与了手机
        ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
                "Start calculating TransitionInfo based on participants: %s", participants);

        // Add all valid participants to the target container.
        final Targets targets = new Targets();
        // 参数 participants 是被收集的容器
        for (int i = participants.size() - 1; i >= 0; --i) {
            final WindowContainer<?> wc = participants.valueAt(i);
            // 没有挂载到窗口树的容器不需要处理
            if (!wc.isAttached()) {
                ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
                        "  Rejecting as detached: %s", wc);
                continue;
            }
            // The level of transition target should be at least window token.
            // WindowState 才有值,有值则不需要后面的逻辑
            // 说明当前处理的容器级别最少也是 WindowToken 级别
            if (wc.asWindowState() != null) continue;

            final ChangeInfo changeInfo = changes.get(wc);

            // Reject no-ops
            // 如果没有改变也不需要执行后续逻辑
            if (!changeInfo.hasChanged()) {
                ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
                        "  Rejecting as no-op: %s", wc);
                continue;
            }
            // 放入集合
            targets.add(changeInfo);
        }
        // 打印集合里有多少容器
        ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "  Initial targets: %s",
                targets.mArray);
        // 开始做提升
        // Combine the targets from bottom to top if possible.
        // 尝试将目标按照层级顺序从下往上提升
        tryPromote(targets, changes);
        // Establish the relationship between the targets and their top changes.
        // 建立目标与其父级改变之间的关系
        populateParentChanges(targets, changes);

        final ArrayList<ChangeInfo> targetList = targets.getListSortedByZ();
        // 最终得到的目标容器打印
        ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "  Final targets: %s", targetList);
        return targetList;
    }

进入方法时 participants 集合的数据打印:

打印日志: WindowManager: Start calculating TransitionInfo based on participants: {ActivityRecord{7f4bc3f u0 com.android.dialer/.main.impl.MainActivity t9}, WallpaperWindowToken{a2af6c4 token=android.os.Binder@b94edd7}, ActivityRecord{a7b06cc u0 com.android.launcher3/.uioverrides.QuickstepLauncher t6}, Task{b5d9cd3 #9 type=standard A=10162:com.android.dialer}}

最终被符合条件被添加进 targets 集合的容器打印:

打印日志: WindowManager: Initial targets: {508=Task{b5d9cd3 #9 type=standard A=10162:com.android.dialer}, 579=ActivityRecord{7f4bc3f u0 com.android.dialer/.main.impl.MainActivity t9}, 645=ActivityRecord{a7b06cc u0 com.android.launcher3/.uioverrides.QuickstepLauncher t6}}

壁纸窗口被过滤掉了,然后执行 tryPromote 方法进行提升, populateParentChanges 方法再处理一下就会得到一个最终的目标容器集合,这2个方法的计算规则感兴趣的可以自己看一下。 正常情况直接从日志里看到处理结果就好了:

打印日志: WindowManager: Final targets: [Task{b5d9cd3 #9 type=standard A=10162:com.android.dialer}, Task{e985e9c #1 type=home}]

看结果和以前版本差不多,也是把桌面和新应用所在的 Task 作为了动画目标,但是根据前面的截图,这2个 Task 最后都被放在了动画图层下面,后续的代码会介绍到。

4. 创建动画图层,构建TransitionInfo

后面的逻辑由需要调用到 SystemUI 了,所以 system_server 端把一些关键参数包装在 TransitionInfo 里传递过去,比较重要的就是里面有做动画的 leash 图层。

# Transition
    static TransitionInfo calculateTransitionInfo(@TransitionType int type, int flags,
            ArrayList<ChangeInfo> sortedTargets,
            @NonNull SurfaceControl.Transaction startT) {
            // 构建个返回值
            final TransitionInfo out = new TransitionInfo(type, flags);
            // 创建事务要操作的 root 图层(动画图层)保存到 TransitionInfo
            calculateTransitionRoots(out, sortedTargets, startT);
            ...... // 其他信息的逻辑,在 SystemUI 端看到使用了哪些再来这里找赋值逻辑
            return out;
    }

这里只看 Transition::calculateTransitionRoots 方法,因为内部会创建动画图层保存在 TransitionInfo 然后传递到 WMShell ,其他的处理代码也很多,暂时不看,后面如果发现在 WMShell 从 TransitionInfo 里接触出什么数据再看这个方法。

# Transition
    @VisibleForTesting
    static void calculateTransitionRoots(@NonNull TransitionInfo outInfo,
            ArrayList<ChangeInfo> sortedTargets,
            @NonNull SurfaceControl.Transaction startT) {
        // There needs to be a root on each display.
        for (int i = 0; i < sortedTargets.size(); ++i) {
            final WindowContainer<?> wc = sortedTargets.get(i).mContainer;
            // Don't include wallpapers since they are in a different DA.
            if (isWallpaper(wc)) continue;
            final DisplayContent dc = wc.getDisplayContent();
            if (dc == null) continue;
            // 拿到屏幕ID
            final int endDisplayId = dc.getDisplayId();

            // Check if Root was already created for this display with a higher-Z window
            // 如果屏幕ID已经被添加过了,则不需要重复操作
            if (outInfo.findRootIndex(endDisplayId) >= 0) continue;
            ......
            WindowContainer leashReference = wc;
            ......
            Log.d("biubiubiu", "calculateTransitionRoots: Transition Root:"+leashReference.getName());
            // 创建动画图层 这里的 leashReference 就是 Task  
            // 所以图层创建好就挂载到了其父亲 DefaultTaskDisplayArea 下了
            final SurfaceControl rootLeash = leashReference.makeAnimationLeash().setName(
                    "Transition Root: " + leashReference.getName()).build();
            rootLeash.setUnreleasedWarningCallSite("Transition.calculateTransitionRoots");
            // Update layers to start transaction because we prevent assignment during collect, so
            // the layer of transition root can be correct.
            updateDisplayLayers(dc, startT);
            Log.d("biubiubiu", "calculateTransitionRoots: startT.setLayer = " + rootLeash);
            // 设置图层
            startT.setLayer(rootLeash, leashReference.getLastLayer());
            // 保存这个屏幕对应动画的根图层(rootLeash)
            // 保存到 TransitionInfo 下面
            outInfo.addRootLeash(endDisplayId, rootLeash,
                    ancestor.getBounds().left, ancestor.getBounds().top);
        }
    }

这里是在 DefaultTaskDisplayArea 下创建了一个"Transition Root: "图层,下面这个图可以直观的显示图层的变化:

图层-图层变化-动画前.png

但是我们知道执行动画的时候,2个 Task 都要挂载到"Transition Root: "下面,这一步是后面在 WMShell 播放动画前操作的。

这里需要注意的是在 Transition::calculateTransitionRoots 方法参数进来的 sortedTargets 其实就是在 Transition::calculateTargets ,也就是说集合里应该是有2个 Task ,所以这里的 for 循环会执行2次,第一次为新启动应用的 Task 创建了 "Transition Root: "图层并添加到 outInfo ,第二次for 循环由于这次的 Task 和上次的是一个屏幕下,所以就不会再执行后续逻辑了,这样是为什么经过 Transition::calculateTransitionRoots 方法处理后 outInfo 下只有1个"Transition Root: "图层的原因,现在逻辑控制了一个屏幕只能播放一个 "Transition Root: "图层的动画。

5. 触发 SystemUI 播放动画(第二次 WMcore --> WMShell)

后面的逻辑是在 SystemUI 播放动画。 当前场景通过winscop 的分析 "Transition Root: "并没有做动画,这个和以前的版本是不一样的,不过这个图层已经传递到了 SystemUI 以后如果要做动画,也是很容易实现的。