当前位置:   article > 正文

Android远程过渡动画_安卓中的leash

安卓中的leash

Android系统动画(二) 过渡动画

Android的系统动画我分为三类:窗口动画,过渡动画,旋转动画。而这篇文章将分析过渡动画。而过渡动画根据创建leash和运行动画是否在同一个进程可以分为本地过渡动画和远程过渡动画,启动远程过渡动画流程相比于其他系统动画的过程,因为涉及到了跨进程,所以涉及到的过程还是比较复杂的,这篇文章将基于Android13分析远程过渡动画。

系统中最常见的远程过渡动画就是从桌面冷启动应用的过程,在这个过程中,触发动画和创建leash是在系统进程完成的,而执行动画却是在launcher的进程中完成的,这就涉及到了跨进程,那为什么要设计成这样呢,为什么不让运行动画的过程也在系统进程进行?我自己认为这样设计的原因大致有两点:

  1. 动画过程中要实现和图标的联动,联动就存在一个同步的问题,要是运行在系统进程的话要不断的和launcher通过binder进行联动,动画可能体验会不好,可能会有卡顿。
  2. 给系统进程减负 。

我把远程过渡动画的过程分为以下几点:

  1. luncher生成运行动画的binder代理,然后在启动新应用的过程中提交给系统。
  2. 运行动画的准备阶段。

一、请求阶段

QuickstepTransitionManager.java

public ActivityOptionsWrapper getActivityLaunchOptions(View v) {
        boolean fromRecents = isLaunchingFromRecents(v, null /* targets */);
        RunnableList onEndCallback = new RunnableList();
        // 本地的运行实例
        mAppLaunchRunner = new AppLaunchAnimationRunner(v, onEndCallback);
        // 把本地封装到亚binder对象中
        RemoteAnimationRunnerCompat runner = new LauncherAnimationRunner(
                mHandler, mAppLaunchRunner, true /* startAtFrontOfQueue */);

        // Note that this duration is a guess as we do not know if the animation will be a
        // recents launch or not for sure until we know the opening app targets.
        long duration = fromRecents
                ? RECENTS_LAUNCH_DURATION
                : APP_LAUNCH_DURATION;

        long statusBarTransitionDelay = duration - STATUS_BAR_TRANSITION_DURATION
                - STATUS_BAR_TRANSITION_PRE_DELAY;
                // 在这里构造出option,里面包含了
        ActivityOptions options = ActivityOptions.makeRemoteAnimation(
                new RemoteAnimationAdapter(runner, duration, statusBarTransitionDelay),
                new RemoteTransition(runner.toRemoteTransition(),
                        mLauncher.getIApplicationThread()));
        return new ActivityOptionsWrapper(options, onEndCallback);
    }
  • 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

在Activity的启动初步,在构造出新的ActivityRecord的阶段,会把启动时设置给Activity的Options通过setOptions设置给新建的ActivityRecord。
如下,因为在通过Launcher启动新的Activity时,在传入fw的options中已经设置了animationType = ANIM_REMOTE_ANIMATION,故会把该ActivityRecord的mPendingRemoteAnimation设置为传入的RemoteAnimationAdapter。这个从Launchar传过来的RemoteAnimationAdapter在后面可有大用处。

ActivityRecord.java

private void setOptions(@NonNull ActivityOptions options) {
        mLaunchedFromBubble = options.getLaunchedFromBubble();
        mPendingOptions = options;
        // 在Launchar启动Activity的时候,已经设置了animationType = ANIM_REMOTE_ANIMATION
        if (options.getAnimationType() == ANIM_REMOTE_ANIMATION) {
            mPendingRemoteAnimation = options.getRemoteAnimationAdapter();
        }
        mPendingRemoteTransition = options.getRemoteTransition();
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

其中mPendingRemoteAnimation的类型为RemoteAnimationAdapter,简单了解下RemoteAnimationAdapter:
可见RemoteAnimationAdapter继承自Parcelable,说明RemoteAnimationAdapter对象可被跨进程传输,这是因为RemoteAnimationAdapter是在从Launchar启动activity过程中设置进入到option的,随后RemoteAnimationAdapter对象通过binder发送到了system_server进程,还需要注意的是在该对象中有一个IRemoteAnimationRunner的对象,明显该对象来自于Launchar,同时实现了IBinder,这就暗示着,后续system_server可以通过该binder对象调用到Launchar进程中。

RemoteAnimationAdapter.java

public class RemoteAnimationAdapter implements Parcelable {

    private final IRemoteAnimationRunner mRunner;
    private final long mDuration;
    private final long mStatusBarTransitionDelay;
    private final boolean mChangeNeedsSnapshot;

    /** @see #getCallingPid */
    private int mCallingPid;
    private int mCallingUid;

    .....
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

在Actiivty启动过程中会调用到ActivityRecord的applyOptionsAnimation方法,如上文中,已经设置了mPendingRemoteAnimation,随后进一步调用了AppTransition.overridePendingAppTransitionRemote方法中

ActivityRecord.java

void applyOptionsAnimation() {
        .....
        if (mPendingRemoteAnimation != null) { // 不为空
            mDisplayContent.mAppTransition.overridePendingAppTransitionRemote(
                    mPendingRemoteAnimation);  // 进一步设置
            .....
        } else {
           ......
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
AppTransition.java

void overridePendingAppTransitionRemote(RemoteAnimationAdapter remoteAnimationAdapter,
            boolean sync, boolean isActivityEmbedding) {
        .....
        if (isTransitionSet() && !mNextAppTransitionIsSync) {
            .....
            mRemoteAnimationController = new RemoteAnimationController(mService, mDisplayContent,
                    remoteAnimationAdapter, mHandler, isActivityEmbedding);
            .....
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

启动activity阶段根据需要启动activity的如果option中否包含远程过渡动画请求,则设置过渡动画。堆栈如下:
at com.android.server.wm.AppTransition.overridePendingAppTransitionRemote(AppTransition.java:1065) at com.android.server.wm.AppTransition.overridePendingAppTransitionRemote(AppTransition.java:1057) at com.android.server.wm.ActivityRecord.applyOptionsAnimation(ActivityRecord.java:4811) at com.android.server.wm.TaskFragment.resumeTopActivity(TaskFragment.java:1379) at com.android.server.wm.Task.resumeTopActivityInnerLocked(Task.java:5020) at com.android.server.wm.Task.resumeTopActivityUncheckedLocked(Task.java:4947) at com.android.server.wm.RootWindowContainer.resumeFocusedTasksTopActivities(RootWindowContainer.java:2267) at com.android.server.wm.RootWindowContainer.resumeFocusedTasksTopActivities(RootWindowContainer.java:2250) at com.android.server.wm.TaskFragment.completePause(TaskFragment.java:1801) at com.android.server.wm.ActivityRecord.activityPaused(ActivityRecord.java:6199) at com.android.server.wm.ActivityClientController.activityPaused(ActivityClientController.java:190) at android.app.IActivityClientController$Stub.onTransact(IActivityClientController.java:609) at com.android.server.wm.ActivityClientController.onTransact(ActivityClientController.java:127) at android.os.Binder.execTransactInternal(Binder.java:1280) at android.os.Binder.execTransact(Binder.java:1244)
上文中设置了mPendingRemoteAnimation ,则不为null,则会调用到
mDisplayContent.mAppTransition.overridePendingAppTransitionRemote( mPendingRemoteAnimation);

二、准备

1.准备阶段1

准备阶段的prepareAppTransition中主要完成了三件事:

  1. 将transit加入到当前屏幕的AppTransition中的mNextAppTransitionRequests列表中
  2. 添加超时回调
  3. 将ppTransition的state设置为APP_STATE_IDLE
DisplayContent.java

@Deprecated
    void prepareAppTransition(@WindowManager.TransitionType int transit,
            @WindowManager.TransitionFlags int flags) {
        final boolean prepared = mAppTransition.prepareAppTransition(transit, flags);
        if (prepared && okToAnimate() && transit != TRANSIT_NONE) {
            mSkipAppTransitionAnimation = false;
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
AppTransition.java

boolean prepareAppTransition(@TransitionType int transit, @TransitionFlags int flags) {
        if (mDisplayContent.mTransitionController.isShellTransitionsEnabled()) {
            return false;
        }
        // 将transit加入到当前屏幕的AppTransition中的mNextAppTransitionRequests列表中
        mNextAppTransitionRequests.add(transit);
        mNextAppTransitionFlags |= flags;
        updateBooster();
        removeAppTransitionTimeoutCallbacks();
        // 添加超时回调
        mHandler.postDelayed(mHandleAppTransitionTimeoutRunnable,
                APP_TRANSITION_TIMEOUT_MS);
        // 调用了prepare
        return prepare();
    }
    
private boolean prepare() {
        if (!isRunning()) {
            // 将会把该AppTransition的state设置为APP_STATE_IDLE
            setAppTransitionState(APP_STATE_IDLE);
            notifyAppTransitionPendingLocked();
            return true;
        }
        return false;
}
  • 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

在从桌面点击启动阶段,会两次触发这个函数,分别是在启动的前期和需要pasue的Activity完成了pause后回调到服务端进行resume的阶段:

如下堆栈反映的是启动前期触发prepareAppTransition,transit 类型为TRANSIT_OPEN
prepareAppTransition transit:TRANSIT_OPEN flag:0 at com.android.server.wm.AppTransition.prepareAppTransition(AppTransition.java:1448) at com.android.server.wm.DisplayContent.prepareAppTransition(DisplayContent.java:5415) at com.android.server.wm.DisplayContent.prepareAppTransition(DisplayContent.java:5406) at com.android.server.wm.Task.startActivityLocked(Task.java:5110) at com.android.server.wm.ActivityStarter.startActivityInner(ActivityStarter.java:1942) at com.android.server.wm.ActivityStarter.startActivityUnchecked(ActivityStarter.java:1671)

如下堆栈反映的是pause后回调到服务端进行resume的阶段调用prepareAppTransition的堆栈:类型也为TRANSIT_OPEN
repareAppTransition transit:TRANSIT_OPEN flag:0 at com.android.server.wm.AppTransition.prepareAppTransition(AppTransition.java:1448) at com.android.server.wm.DisplayContent.prepareAppTransition(DisplayContent.java:5415) at com.android.server.wm.TaskFragment.resumeTopActivity(TaskFragment.java:1364) at com.android.server.wm.Task.resumeTopActivityInnerLocked(Task.java:5020) at com.android.server.wm.Task.resumeTopActivityUncheckedLocked(Task.java:4947) at com.android.server.wm.RootWindowContainer.resumeFocusedTasksTopActivities(RootWindowContainer.java:2267) at com.android.server.wm.RootWindowContainer.resumeFocusedTasksTopActivities(RootWindowContainer.java:2250) at com.android.server.wm.TaskFragment.completePause(TaskFragment.java:1801)
可见这两次触发的过程向mNextAppTransitionRequests添加的transit类型均为TRANSIT_OPEN
在窗口的大遍历阶段会检查每一个屏幕上的过渡动画是否准备完毕,等条件成熟后执行过渡动画。

private void checkAppTransitionReady(WindowSurfacePlacer surfacePlacer) {
        // Trace all displays app transition by Z-order for pending layout change.
        for (int i = mChildren.size() - 1; i >= 0; --i) {
            final DisplayContent curDisplay = mChildren.get(i);

            // If we are ready to perform an app transition, check through all of the app tokens
            // to be shown and see if they are ready to go.
            if (curDisplay.mAppTransition.isReady()) {
                // handleAppTransitionReady may modify curDisplay.pendingLayoutChanges.
                // 处理每个displaycontent上的过渡动画
                curDisplay.mAppTransitionController.handleAppTransitionReady();
                if (DEBUG_LAYOUT_REPEATS) {
                    surfacePlacer.debugLayoutRepeats("after handleAppTransitionReady",
                            curDisplay.pendingLayoutChanges);
                }
            }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

接下来会进入到checkAppTransitionReady中,过渡动画的触发条件检查,选择合适的执行动画的窗口层级,创建leash到最后的跨进程启动动画都在这个函数中完成。

2.准备阶段2

在从桌面启动应用阶段,会把设置启动的ActivityRecord和paused的ActivityRecord的可见性,把启动的ActivityRecord设置为可见,pause的设置为不可见(具体执行流程本文不涉及):

ActivityRecord.java

void setVisibility(boolean visible) {
        if (getParent() == null) {
            Slog.w(TAG_WM, "Attempted to set visibility of non-existing app token: " + token);
            return;
        }
        if (visible) {
            mDeferHidingClient = false;
        }
        // 进一步设置
        setVisibility(visible, mDeferHidingClient);
        mAtmService.addWindowLayoutReasons(
                ActivityTaskManagerService.LAYOUT_REASON_VISIBILITY_CHANGED);
        mTaskSupervisor.getActivityMetricsLogger().notifyVisibilityChanged(this);
        mTaskSupervisor.mAppVisibilitiesChangedSinceLastPause = true;
    }
    
void setVisibility(boolean visible, boolean deferHidingClient) {
        final AppTransition appTransition = getDisplayContent().mAppTransition;
        ....
        // 清空当前displayContent的mOpeningApps和mClosingApps列表
        final DisplayContent displayContent = getDisplayContent();
        displayContent.mOpeningApps.remove(this);
        displayContent.mClosingApps.remove(this);
        waitingToShow = false;
        ....
        // 这里进一步设置
        if (deferCommitVisibilityChange(visible)) {
            return;
        }
        ....
    }

private boolean deferCommitVisibilityChange(boolean visible) {
      .....
        // 可见的话则把该ActivityRecord添加到isplayContent.mOpeningApps,反之则加入到mDisplayContent.mClosingApps
        if (visible) {
            mDisplayContent.mOpeningApps.add(this);
            mEnteringAnimation = true;
        } else if (mVisible) {
            mDisplayContent.mClosingApps.add(this);
            mEnteringAnimation = false;
        }
        
        return true;
        ....
    }
  • 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

3.准备阶段3

在窗口大遍历阶段,遍历到启动activity的启动窗口的状态是COMMIT_DRAW_PENDING或者READY_TO_SHOW时会调用到DisplayContent.executeAppTransition,当需要resume的ActivityRecord完成 向应用端下发 将生命周期状态转移至resumed后也会调用到DisplayContent.executeAppTransition :

DisplayContent.java 

void executeAppTransition() {
        android.util.Log.i("remote_animation_trace","executeAppTransition:" , new Exception());
        mTransitionController.setReady(this);
        if (mAppTransition.isTransitionSet()) {
            ProtoLog.w(WM_DEBUG_APP_TRANSITIONS,
                    "Execute app transition: %s, displayId: %d Callers=%s",
                    mAppTransition, mDisplayId, Debug.getCallers(5));
            mAppTransition.setReady();
            mWmService.mWindowPlacerLocked.requestTraversal();
        }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

三、正式开始处理过渡动画

handleAppTransitionReady()是启动过渡动画过程中,比较关键的一个步骤,在这个方法里面主要完成了以下步骤:

  • 通过transitionGoodToGo检查启动的Activity的所有窗口状态是否都已HAS_DRAW,或者启动窗口已经完成显示
  • 通过getTransitCompatType确定过渡动画类型
  • 构造出用于控制窗口动画的Leash
  • 把在system_server构造出的过渡动画参数(主要是Leash)发送给Launchar,随机开始动画
 void handleAppTransitionReady() {
        mTempTransitionReasons.clear();
        // 1.检查是否allDraw,或者启动窗口已经绘制
        if (!transitionGoodToGo(mDisplayContent.mOpeningApps, mTempTransitionReasons)
                || !transitionGoodToGo(mDisplayContent.mChangingContainers, mTempTransitionReasons)
                || !transitionGoodToGoForTaskFragments()) {
            return;
        }
        
        .....
        
        ProtoLog.v(WM_DEBUG_APP_TRANSITIONS, "**** GOOD TO GO");
        // TODO(b/205335975): Remove window which stuck in animatingExit status. Find actual cause.
        
        ....
        
        // Adjust wallpaper before we pull the lower/upper target, since pending changes
        // (like the clearAnimatingFlags() above) might affect wallpaper target result.
        // Or, the opening app window should be a wallpaper target.
        mWallpaperControllerLocked.adjustWallpaperWindowsForAppTransitionIfNeeded(
                mDisplayContent.mOpeningApps);
                
        // 2.确定过渡动画类型
        @TransitionOldType final int transit = getTransitCompatType(
                mDisplayContent.mAppTransition, mDisplayContent.mOpeningApps,
                mDisplayContent.mClosingApps, mDisplayContent.mChangingContainers,
                mWallpaperControllerLocked.getWallpaperTarget(), getOldWallpaper(),
                mDisplayContent.mSkipAppTransitionAnimation);
        mDisplayContent.mSkipAppTransitionAnimation = false;

        ProtoLog.v(WM_DEBUG_APP_TRANSITIONS,
                "handleAppTransitionReady: displayId=%d appTransition={%s}"
                        + " openingApps=[%s] closingApps=[%s] transit=%s",
                mDisplayContent.mDisplayId, appTransition.toString(), mDisplayContent.mOpeningApps,
                mDisplayContent.mClosingApps, AppTransition.appTransitionOldToString(transit));
        
        ....

       // 确定最顶部的activity
        final ActivityRecord topOpeningApp =
                getTopApp(mDisplayContent.mOpeningApps, false /* ignoreHidden */);
        final ActivityRecord topClosingApp =
                getTopApp(mDisplayContent.mClosingApps, false /* ignoreHidden */);
        final ActivityRecord topChangingApp =
                getTopApp(mDisplayContent.mChangingContainers, false /* ignoreHidden */);
        final WindowManager.LayoutParams animLp = getAnimLp(animLpActivity);

        try {
        // 3.重要步骤,在applyAnimations将会确定动画的windowContainer,并为其创造出控制动画的leash
            applyAnimations(mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps, transit,
                    animLp, voiceInteraction);
            handleClosingApps();
            handleOpeningApps();
            handleChangingApps(transit);
            handleClosingChangingContainers();

            appTransition.setLastAppTransition(transit, topOpeningApp,
                    topClosingApp, topChangingApp);

            final int flags = appTransition.getTransitFlags();
            // 4.重要步骤 在这里正式启动remoteAnimation
            layoutRedo = appTransition.goodToGo(transit, topOpeningApp);
            handleNonAppWindowsInTransition(transit, flags);
            appTransition.postAnimationCallback();
        } finally {
            appTransition.clear();
            mService.mSurfaceAnimationRunner.continueStartingAnimations();
        }
        // 对close的app进行截图
        mService.mTaskSnapshotController.onTransitionStarting(mDisplayContent);
        // 清理
        mDisplayContent.mOpeningApps.clear();
        mDisplayContent.mClosingApps.clear();
        mDisplayContent.mChangingContainers.clear();
        mDisplayContent.mUnknownAppVisibilityController.clear();
        mDisplayContent.mClosingChangingContainers.clear();

        .....
    }
  • 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

1.检查前提条件

在transitionGoodToGo会对条件进行检查,因为是从桌面启动应用的过渡动画所以只关注transitionGoodToGo(mDisplayContent.mOpeningApps, mTempTransitionReasons)

void handleAppTransitionReady() {
        mTempTransitionReasons.clear();
        // 检查是否allDraw,或者启动窗口已经绘制
        if (!transitionGoodToGo(mDisplayContent.mOpeningApps, mTempTransitionReasons)
                || !transitionGoodToGo(mDisplayContent.mChangingContainers, mTempTransitionReasons)
                || !transitionGoodToGoForTaskFragments()) {
            return;
        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

2.确定合适的transit

在这方法里面会根据打开的Activity,关闭的Activity,关闭和打开的app是否含有壁纸,之前传入的appTransition类型来决定最终的Transit类型,在桌面冷启动Activity场景下的最终类型为TRANSIT_OLD_WALLPAPER_CLOSE

AppTransitionController.java

static int getTransitCompatType(AppTransition appTransition,
                                    ArraySet<ActivityRecord> openingApps, ArraySet<ActivityRecord> closingApps,
                                    ArraySet<WindowContainer> changingContainers, @Nullable WindowState wallpaperTarget,
                                    @Nullable WindowState oldWallpaper, boolean skipAppTransitionAnimation) {

        final ActivityRecord topOpeningApp = getTopApp(openingApps, false /* ignoreHidden */);
        final ActivityRecord topClosingApp = getTopApp(closingApps, true /* ignoreHidden */);

        // Determine if closing and opening app token sets are wallpaper targets, in which case
        // special animations are needed.
        final boolean openingAppHasWallpaper = canBeWallpaperTarget(openingApps)
                && wallpaperTarget != null;
        final boolean closingAppHasWallpaper = canBeWallpaperTarget(closingApps)
                && wallpaperTarget != null;
        .....
        if (closingAppHasWallpaper && openingAppHasWallpaper) {
            ProtoLog.v(WM_DEBUG_APP_TRANSITIONS, "Wallpaper animation!");
            switch (firstTransit) {
                case TRANSIT_OPEN:
                case TRANSIT_TO_FRONT:
                    return TRANSIT_OLD_WALLPAPER_INTRA_OPEN;
                case TRANSIT_CLOSE:
                case TRANSIT_TO_BACK:
                    return TRANSIT_OLD_WALLPAPER_INTRA_CLOSE;
            }
        } else if (oldWallpaper != null && !openingApps.isEmpty()
                && !openingApps.contains(oldWallpaper.mActivityRecord)
                && closingApps.contains(oldWallpaper.mActivityRecord)
                && topClosingApp == oldWallpaper.mActivityRecord) {
            // We are transitioning from an activity with a wallpaper to one without.
            // 最终类型
            return TRANSIT_OLD_WALLPAPER_CLOSE;
        ....
  • 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

3. applyAnimations

接下来的重要流程会进入到applyAnimations()中:

void handleAppTransitionReady() {
        .....
        // 检查是否allDraw,或者启动窗口已经绘制
        if (!transitionGoodToGo(mDisplayContent.mOpeningApps, mTempTransitionReasons)
                || !transitionGoodToGo(mDisplayContent.mChangingContainers, mTempTransitionReasons)
                || !transitionGoodToGoForTaskFragments()) {
            return;
        }
        .....
        @TransitionOldType final int transit = getTransitCompatType(
                mDisplayContent.mAppTransition, mDisplayContent.mOpeningApps,
                mDisplayContent.mClosingApps, mDisplayContent.mChangingContainers,
                mWallpaperControllerLocked.getWallpaperTarget(), getOldWallpaper(),
                mDisplayContent.mSkipAppTransitionAnimation);
        mDisplayContent.mSkipAppTransitionAnimation = false;

        ProtoLog.v(WM_DEBUG_APP_TRANSITIONS,
                "handleAppTransitionReady: displayId=%d appTransition={%s}"
                        + " openingApps=[%s] closingApps=[%s] transit=%s",
                mDisplayContent.mDisplayId, appTransition.toString(), mDisplayContent.mOpeningApps,
                mDisplayContent.mClosingApps, AppTransition.appTransitionOldToString(transit));
        .....
        final ActivityRecord topOpeningApp =
                getTopApp(mDisplayContent.mOpeningApps, false /* ignoreHidden */);
        final ActivityRecord topClosingApp =
                getTopApp(mDisplayContent.mClosingApps, false /* ignoreHidden */);
        final ActivityRecord topChangingApp =
                getTopApp(mDisplayContent.mChangingContainers, false /* ignoreHidden */);
        final WindowManager.LayoutParams animLp = getAnimLp(animLpActivity);
        .....
        try {
            // 重要流程
            applyAnimations(mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps, transit,
                    animLp, voiceInteraction);
            handleClosingApps();
            handleOpeningApps();
            handleChangingApps(transit);
            handleClosingChangingContainers();

            appTransition.setLastAppTransition(transit, topOpeningApp,
                    topClosingApp, topChangingApp);

            final int flags = appTransition.getTransitFlags();
            // 在这里正式启动remoteAnimation
            layoutRedo = appTransition.goodToGo(transit, topOpeningApp);
            handleNonAppWindowsInTransition(transit, flags);
            appTransition.postAnimationCallback();
        } finally {
            appTransition.clear();
            mService.mSurfaceAnimationRunner.continueStartingAnimations();
        }
  • 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

在applyAnimations第一个只重要的步骤是获取执行动画的目标WindowContainer,后续的创建leash将围绕这个WindowContainer

1.如下在getAnimationTargets中根据传入的开启的ActivityRecord和关闭的ActivityRecord分别计算出了执行openings和closing动画是的窗口容器。通常情况下在却确定层级时会比开启或关闭的activityRecord高一个层级,比如在从桌面启动Activity的过程中,openingApps只有一个,即新打开的ActivityRecord,那么在执行打开动画的窗口容器即比新打开的ActivityRecord的高一个层级,即它所在的Task。同理,此时的closingApps就是桌面的ActivityRecord,那么执行close动画的窗口容器就是Launchar的Task。

2.可见openingApps,closingApps是两个列表,这意味着在一次处理过渡动画的过程中可同时处理多个同时打开或者关闭的Activity的过渡动画,这就意味着多个动画要同时运行,同时显然,在applyAnimations分别处理打开的和关闭的Activity的过渡动画,这更能说明了在打开一个Activity的过程中,同时执行了多种动画。

3.1 计算出执行动画的窗口容器
AppTransitionController.java

private void applyAnimations(ArraySet<ActivityRecord> openingApps,
                                 ArraySet<ActivityRecord> closingApps, @TransitionOldType int transit,
                                 LayoutParams animLp, boolean voiceInteraction) {
        // 分别计算出执行open,close动画的Target
        final ArraySet<WindowContainer> openingWcs = getAnimationTargets(
                openingApps, closingApps, true /* visible */);
        final ArraySet<WindowContainer> closingWcs = getAnimationTargets(
                openingApps, closingApps, false /* visible */);
        // 继续
        applyAnimations(openingWcs, openingApps, transit, true /* visible */, animLp,
                voiceInteraction);
        applyAnimations(closingWcs, closingApps, transit, false /* visible */, animLp,
                voiceInteraction);
        if (rac != null) {
            rac.sendTasksAppeared();
        }

        for (int i = 0; i < openingApps.size(); ++i) {
            openingApps.valueAtUnchecked(i).mOverrideTaskTransition = false;
        }
        for (int i = 0; i < closingApps.size(); ++i) {
            closingApps.valueAtUnchecked(i).mOverrideTaskTransition = false;
        }

        final AccessibilityController accessibilityController =
                mDisplayContent.mWmService.mAccessibilityController;
        if (accessibilityController.hasCallbacks()) {
            accessibilityController.onAppWindowTransition(mDisplayContent.getDisplayId(), transit);
        }
    }
  • 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
3.2 AppTransitionController.applyAnimations

如上,在确定了执行动画的窗口层级后,接下以打开动画为例:applyAnimations(openingWcs, openingApps, transit, true /* visible */, animLp, voiceInteraction);
如下会遍历每个执行open动画的窗口容器,然后执行该容器的applyAnimation方法。

AppTransitionController.java

private void applyAnimations(ArraySet<WindowContainer> wcs, ArraySet<ActivityRecord> apps,
                                 @TransitionOldType int transit, boolean visible, LayoutParams animLp,
                                 boolean voiceInteraction) {
        final int wcsCount = wcs.size();
        for (int i = 0; i < wcsCount; i++) {
            final WindowContainer wc = wcs.valueAt(i);
            // If app transition animation target is promoted to higher level, SurfaceAnimator
            // triggers WC#onAnimationFinished only on the promoted target. So we need to take care
            // of triggering AR#onAnimationFinished on each ActivityRecord which is a part of the
            // app transition.
            final ArrayList<ActivityRecord> transitioningDescendants = new ArrayList<>();
            for (int j = 0; j < apps.size(); ++j) {
                final ActivityRecord app = apps.valueAt(j);
                if (app.isDescendantOf(wc)) {
                    transitioningDescendants.add(app);
                }
            }
            wc.applyAnimation(animLp, transit, visible, voiceInteraction, transitioningDescendants);
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
WindowContainer.java
随后调用到 WindowContainer的applyAnimationUnchecked方法,
boolean applyAnimation(WindowManager.LayoutParams lp, @TransitionOldType int transit,
            boolean enter, boolean isVoiceInteraction,
            @Nullable ArrayList<WindowContainer> sources) {
        if (mWmService.mDisableTransitionAnimation) {
            ProtoLog.v(WM_DEBUG_APP_TRANSITIONS_ANIM,
                    "applyAnimation: transition animation is disabled or skipped. "
                            + "container=%s", this);
            cancelAnimation();
            return false;
        }

        // Only apply an animation if the display isn't frozen. If it is frozen, there is no reason
        // to animate and it can cause strange artifacts when we unfreeze the display if some
        // different animation is running.
        try {
            Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "WC#applyAnimation");
            if (okToAnimate()) {
                ProtoLog.v(WM_DEBUG_APP_TRANSITIONS_ANIM,
                        "applyAnimation: transit=%s, enter=%b, wc=%s",
                        AppTransition.appTransitionOldToString(transit), enter, this);
                // 这个
                applyAnimationUnchecked(lp, enter, transit, isVoiceInteraction, sources);
            } else {
                cancelAnimation();
            }
        } finally {
            Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
        }

        return isAnimating();
    }
  • 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

如下,会执行WindowContainer.applyAnimationUnchecked方法,需要注意的是,用open动画举例,这里的WindowContainer的对象是执行opening动画的容器,在从桌面打开新应用阶段,这里的动画容器就是新打开应用的Task。回到正题,在applyAnimationUnchecked中主要做了两件事情:

  • 构造出该容器,该次动画的的RemoteAnimationgAdaptar对象,该对象用于描述动画并将动画起点连接到负责运行动画的组件。
  • 调用RemoteAnimationgAdaptar的startAnimation方法,RemoteAnimationgAdaptar中该方法并不会启动动画,而是会构造出执行动画所用的Leash。
WindowContainer.java

protected void applyAnimationUnchecked(WindowManager.LayoutParams lp, boolean enter,
            @TransitionOldType int transit, boolean isVoiceInteraction,
            @Nullable ArrayList<WindowContainer> sources) {
            
        .......
        
        // 这里会得到RemoteAnimationgAdaptar
        final Pair<AnimationAdapter, AnimationAdapter> adapters = getAnimationAdapter(lp,
                transit, enter, isVoiceInteraction);
        AnimationAdapter adapter = adapters.first;
        AnimationAdapter thumbnailAdapter = adapters.second;
        .....
        
            // 根据之前创造出的adpter,还所在的窗口容器,在这里会构造出用于控制动画执行的leash
            animationRunnerBuilder.build()
                    .startAnimation(getPendingTransaction(), adapter, !isVisible(),
                            ANIMATION_TYPE_APP_TRANSITION, thumbnailAdapter);
        ....
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

首先来了解getAnimationAdapter方法该方法会返回一个RemoteAnimationRecord,该对象代表一次远程动画,在RemoteAnimationRecord中有两个重要的对象mAdapter和mThumbnailAdapter,目前只关注打开Activity是的远程动画,所以不关注mThumbnailAdapter,只关注mAdapter。

WindowContainer.java

Pair<AnimationAdapter, AnimationAdapter> getAnimationAdapter(WindowManager.LayoutParams lp,
            @TransitionOldType int transit, boolean enter, boolean isVoiceInteraction) {
        final Pair<AnimationAdapter, AnimationAdapter> resultAdapters;
        // 获取之前launchar启动activity时通过启动参数Options设置的RemoteAnimationController
        final RemoteAnimationController controller =
                getDisplayContent().mAppTransition.getRemoteAnimationController();

        if (controller != null && !mSurfaceAnimator.isAnimationStartDelayed()) {
            final Rect localBounds = new Rect(mTmpRect);
            localBounds.offsetTo(mTmpPoint.x, mTmpPoint.y);
            // 构造出一个RemoteAnimationRecord
            final RemoteAnimationController.RemoteAnimationRecord adapters =
                    controller.createRemoteAnimationRecord(this, mTmpPoint, localBounds,
                            screenBounds, (isChanging ? mSurfaceFreezer.mFreezeBounds : null));
            // 把构造出的RemoteAnimationRecord中的mAdapter和mThumbnailAdapter放入至resultAdapters并返回
            resultAdapters = new Pair<>(adapters.mAdapter, adapters.mThumbnailAdapter);
        } else if (isChanging) {
            
         .....
        return resultAdapters;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

如上,通过RemoteAnimationController和传入的执行窗口容器,位置和边界等信息,构建出了一个RemoteAnimatonController,如下继续进入到createRemoteAnimationRecord,可见构造出了一个RemoteAnimationRecord对象,并把该对象加入到mPendingAnimations中。

总结一下,在处理一次过渡动画的过程中会对应一个RemoteAnimationController,对应一个mPendingAnimations,在上文中我们了解到在一次处理过渡动画的过程中会有可能存在多个打开的Activity和关闭的Activity,正常情况下每个Activity的打开和关闭都对应一个动画,一个窗口容器,一个RemoteAnimationRecord(远程动画的情况下),每一个RemoteAnimationRecord都会加入到mPendingAnimations中,这个mPendingAnimations有大用,动画将会通过mPendingAnimations进行统一触发。在远程动画情况下RemoteAnimationController.mPendingAnimations中保存是各个动画的RemoteAnimationController。

RemoteAnimationController.java


RemoteAnimationRecord createRemoteAnimationRecord(WindowContainer windowContainer,
            Point position, Rect localBounds, Rect endBounds, Rect startBounds,
            boolean showBackdrop, boolean shouldCreateSnapshot) {
        ProtoLog.d(WM_DEBUG_REMOTE_ANIMATIONS, "createAnimationAdapter(): container=%s",
                windowContainer);
        // 构造出
        final RemoteAnimationRecord adapters = new RemoteAnimationRecord(windowContainer, position,
                localBounds, endBounds, startBounds, showBackdrop, shouldCreateSnapshot);
        // 添加至mPendingAnimations
        mPendingAnimations.add(adapters);
        return adapters;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

接下来分析一下代表一次远程动画的RemoteAnimationRecord的构造过程中发生了什么:
如下,如下在RemoteAnimationRecord的构造函数中主要是构造出了一个RemoteAnimationAdapterWrapper对象,而RemoteAnimationAdapterWrapper派生自AnimationAdapter,并把RemoteAnimationAdapterWrapper保存在mAdapter中,终于,构造出了RemoteAnimationAdapterWrapper。

RemoteAnimationController.java

RemoteAnimationRecord(WindowContainer windowContainer, Point endPos, Rect localBounds,
                Rect endBounds, @Nullable Rect startBounds, boolean showBackdrop,
                boolean shouldCreateSnapshot) {
            mWindowContainer = windowContainer;
            mShowBackdrop = showBackdrop;
            // 构造出一个RemoteAnimationAdapterWrapper对象,并白该对象设置给RemoteAnimationRecord的mAdapter
            if (startBounds != null) {
                mStartBounds = new Rect(startBounds);
                mAdapter = new RemoteAnimationAdapterWrapper(this, endPos, localBounds, endBounds,
                        mStartBounds, mShowBackdrop);
                if (shouldCreateSnapshot && mRemoteAnimationAdapter.getChangeNeedsSnapshot()) {
                    final Rect thumbnailLocalBounds = new Rect(startBounds);
                    thumbnailLocalBounds.offsetTo(0, 0);
                    // Snapshot is located at (0,0) of the animation leash. It doesn't have size
                    // change, so the startBounds is its end bounds, and no start bounds for it.
                    mThumbnailAdapter = new RemoteAnimationAdapterWrapper(this, new Point(0, 0),
                            thumbnailLocalBounds, startBounds, new Rect(), mShowBackdrop);
                }
            } else {
                mAdapter = new RemoteAnimationAdapterWrapper(this, endPos, localBounds, endBounds,
                        new Rect(), mShowBackdrop);
                mStartBounds = null;
            }
        }
  • 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

回到WindowContainer.applyAnimationUnchecked方:如上文你分析,在getAnimationAdapter构造出了相应的RemoteAnimationRecord和RemoteAnimationAdapterWrapper,接下来会进入到animationRunnerBuilder.build().startAnimation去为动画构造leash,关于leash的介绍在这里不做展开。

WindowContainer.java

protected void applyAnimationUnchecked(WindowManager.LayoutParams lp, boolean enter,
            @TransitionOldType int transit, boolean isVoiceInteraction,
            @Nullable ArrayList<WindowContainer> sources) {
            
        .......
        
        // 这里会得到RemoteAnimationgAdaptar
        final Pair<AnimationAdapter, AnimationAdapter> adapters = getAnimationAdapter(lp,
                transit, enter, isVoiceInteraction);
        AnimationAdapter adapter = adapters.first;
        AnimationAdapter thumbnailAdapter = adapters.second;
        .....
        
            // 根据之前创造出的adpter,还所在的窗口容器,在这里会构造出用于控制动画执行的leash
            animationRunnerBuilder.build()
                    .startAnimation(getPendingTransaction(), adapter, !isVisible(),
                            ANIMATION_TYPE_APP_TRANSITION, thumbnailAdapter);
        ....
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

最终startAnimation将会调用到SurfaceAnimation的startAnimation,SurfaceAnimatior是每个WindowContainer中用来处理动画的辅助类:
如下,在创造出leash后随后调用了mAnimation.startAnimation(mLeash, t, type, mInnerAnimationFinishedCallback);其中参数mLeash是构造出的leash,t是发消息给surfaceflinger的事务对象,mInnerAnimationFinishedCallback是动画执行完毕后的回调。其中mAnimation便是RemoteAnimationAdapterWrapper

SurfaceAnimation.java

void startAnimation(Transaction t, AnimationAdapter anim, boolean hidden,
            @AnimationType int type,
            @Nullable OnAnimationFinishedCallback animationFinishedCallback,
            @Nullable SurfaceFreezer freezer) {
        cancelAnimation(t, true /* restarting */, true /* forwardCancel */);
        mAnimation = anim;
        mAnimationType = type;
        mAnimationFinishedCallback = animationFinishedCallback;
        // 这里的mAnimatable是一个WindowContainer,在这里指的是执行动画的窗口容器。
        final SurfaceControl surface = mAnimatable.getSurfaceControl();
        if (surface == null) {
            Slog.w(TAG, "Unable to start animation, surface is null or no children.");
            cancelAnimation();
            return;
        }
        mLeash = freezer != null ? freezer.takeLeashForAnimation() : null;
        // 构造出leash
        if (mLeash == null) {
            mLeash = createAnimationLeash(mAnimatable, surface, t, type,
                    mAnimatable.getSurfaceWidth(), mAnimatable.getSurfaceHeight(), 0 /* x */,
                    0 /* y */, hidden, mService.mTransactionFactory);
            mAnimatable.onAnimationLeashCreated(t, mLeash);
        }
        mAnimatable.onLeashAnimationStarting(t, mLeash);
        if (mAnimationStartDelayed) {
            if (DEBUG_ANIM) Slog.i(TAG, "Animation start delayed");
            return;
        }
        // 继续调用
        mAnimation.startAnimation(mLeash, t, type, mInnerAnimationFinishedCallback);
    }
  • 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

继续分析,如下RemoteAnimationAdapterWrapper.startAnimation主要是把创造的leash和动画之心完毕后的回调等保存在下RemoteAnimationAdapterWrapper中。至此控制动画的重要组件leash的构造已经完成

RemoteAnimationAdapterWrapper:

@Override
        public void startAnimation(SurfaceControl animationLeash, Transaction t,
                @AnimationType int type, OnAnimationFinishedCallback finishCallback) {
            ProtoLog.d(WM_DEBUG_REMOTE_ANIMATIONS, "startAnimation");

            // Restore position and stack crop until client has a chance to modify it.
            if (mStartBounds.isEmpty()) {
                t.setPosition(animationLeash, 0, 0);
                t.setWindowCrop(animationLeash, -1, -1);
            } else {
                t.setPosition(animationLeash, mStartBounds.left, mStartBounds.top);
                t.setWindowCrop(animationLeash, mStartBounds.width(), mStartBounds.height());
            }
            mCapturedLeash = animationLeash;
            mCapturedFinishCallback = finishCallback;
            mAnimationType = type;
        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

4. 正式开启触发动画

如下,回顾之前的流程,在检查transitionGoodToGo,确定过渡动画类型,确定动画的windowContainer,并为各个动画其创造出控制动画的leash和ReomteAnimationAdapterWrap之后,接下来将会把之前确定的动画逐一触发:layoutRedo = appTransition.goodToGo(transit, topOpeningApp);

 void handleAppTransitionReady() {
        mTempTransitionReasons.clear();
        // 1.检查是否allDraw,或者启动窗口已经绘制
        if (!transitionGoodToGo(mDisplayContent.mOpeningApps, mTempTransitionReasons)
                || !transitionGoodToGo(mDisplayContent.mChangingContainers, mTempTransitionReasons)
                || !transitionGoodToGoForTaskFragments()) {
            return;
        }
        
        .....
        
        ProtoLog.v(WM_DEBUG_APP_TRANSITIONS, "**** GOOD TO GO");
        // TODO(b/205335975): Remove window which stuck in animatingExit status. Find actual cause.
        
        ....
        
        // Adjust wallpaper before we pull the lower/upper target, since pending changes
        // (like the clearAnimatingFlags() above) might affect wallpaper target result.
        // Or, the opening app window should be a wallpaper target.
        mWallpaperControllerLocked.adjustWallpaperWindowsForAppTransitionIfNeeded(
                mDisplayContent.mOpeningApps);
                
        // 2.确定过渡动画类型
        @TransitionOldType final int transit = getTransitCompatType(
                mDisplayContent.mAppTransition, mDisplayContent.mOpeningApps,
                mDisplayContent.mClosingApps, mDisplayContent.mChangingContainers,
                mWallpaperControllerLocked.getWallpaperTarget(), getOldWallpaper(),
                mDisplayContent.mSkipAppTransitionAnimation);
        mDisplayContent.mSkipAppTransitionAnimation = false;

        ProtoLog.v(WM_DEBUG_APP_TRANSITIONS,
                "handleAppTransitionReady: displayId=%d appTransition={%s}"
                        + " openingApps=[%s] closingApps=[%s] transit=%s",
                mDisplayContent.mDisplayId, appTransition.toString(), mDisplayContent.mOpeningApps,
                mDisplayContent.mClosingApps, AppTransition.appTransitionOldToString(transit));
        
        ....

       // 3.确定最顶部的activity
        final ActivityRecord topOpeningApp =
                getTopApp(mDisplayContent.mOpeningApps, false /* ignoreHidden */);
        final ActivityRecord topClosingApp =
                getTopApp(mDisplayContent.mClosingApps, false /* ignoreHidden */);
        final ActivityRecord topChangingApp =
                getTopApp(mDisplayContent.mChangingContainers, false /* ignoreHidden */);
        final WindowManager.LayoutParams animLp = getAnimLp(animLpActivity);

        try {
        // 4.重要步骤,在applyAnimations将会确定动画的windowContainer,并为其创造出控制动画的leash
            applyAnimations(mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps, transit,
                    animLp, voiceInteraction);
            handleClosingApps();
            handleOpeningApps();
            handleChangingApps(transit);
            handleClosingChangingContainers();

            appTransition.setLastAppTransition(transit, topOpeningApp,
                    topClosingApp, topChangingApp);

            final int flags = appTransition.getTransitFlags();
            // 5.重要步骤 在这里正式启动remoteAnimation
            layoutRedo = appTransition.goodToGo(transit, topOpeningApp);
            handleNonAppWindowsInTransition(transit, flags);
            appTransition.postAnimationCallback();
        } finally {
            appTransition.clear();
            mService.mSurfaceAnimationRunner.continueStartingAnimations();
        }
        // 对close的app进行截图
        mService.mTaskSnapshotController.onTransitionStarting(mDisplayContent);
        // 清理
        mDisplayContent.mOpeningApps.clear();
        mDisplayContent.mClosingApps.clear();
        mDisplayContent.mChangingContainers.clear();
        mDisplayContent.mUnknownAppVisibilityController.clear();
        mDisplayContent.mClosingChangingContainers.clear();

        .....
    }
  • 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

AppTransition.goodTogo,开始触发动画

接上文,将会进入到AppTransition.goodTogo来触发动画:
mRemoteAnimationController是在launchar启动新Activity的过程中一直设置过来的,故不为空,接下来将进入到RemoteAnimationController.goodToGo方法

AppTransition.java

int goodToGo(@TransitionOldType int transit, ActivityRecord topOpeningApp) {
        .....
        // 之前设置过,故不为空
        if (mRemoteAnimationController != null) {
            android.util.Log.i("remote_animation_trace","goodToGo:" , new Exception());
            mRemoteAnimationController.goodToGo(transit);
        } else if ((isTaskOpenTransitOld(transit) || transit == TRANSIT_OLD_WALLPAPER_CLOSE)
                && topOpeningAnim != null) {
        ....
        return redoLayout;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

如下,在RemoteAnimationController的goodToGo的方法中,主要做了两件事:

  • 构造出 appTargets数组
  • 通过mRemoteAnimationAdapter.getRunner().onAnimationStart(transit, appTargets, wallpaperTargets, nonAppTargets, mFinishedCallback);正式触发
RemoteAnimationController.java

void goodToGo(@WindowManager.TransitionOldType int transit) {
        mFinishedCallback = new FinishedCallback(this)
        .....
        // 构造RemoteAnimationTarget
        final RemoteAnimationTarget[] appTargets = createAppAnimations();
        ......
        // Create the remote wallpaper animation targets (if any)
        final RemoteAnimationTarget[] wallpaperTargets = createWallpaperAnimations();

        // Create the remote non app animation targets (if any)
        final RemoteAnimationTarget[] nonAppTargets = createNonAppWindowAnimations(transit);
        mService.mAnimator.addAfterPrepareSurfacesRunnable(() -> {
            try {
                linkToDeathOfRunner();
                ProtoLog.d(WM_DEBUG_REMOTE_ANIMATIONS, "goodToGo(): onAnimationStart,"
                                + " transit=%s, apps=%d, wallpapers=%d, nonApps=%d",
                        AppTransition.appTransitionOldToString(transit), appTargets.length,
                        wallpaperTargets.length, nonAppTargets.length);
                mRemoteAnimationAdapter.getRunner().onAnimationStart(transit, appTargets,
                        wallpaperTargets, nonAppTargets, mFinishedCallback);
            } catch (RemoteException e) {
                Slog.e(TAG, "Failed to start remote animation", e);
                onAnimationFinished();
            }
            .......
        });
  • 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

先来看看RemoteAnimationTarget[] 是怎么构造出来的,进入到createAppAnimations()方法:
可见这个方法是遍历之前设置的mPendingAnimations,mPendingAnimations是一个列表,保存着RemoteAnimationRecord对象,而RemoteAnimationRecord中保存着RemoteAnimationAdapterWrap对象,之前强调过在处理一次过渡动画对应一个RemoteAnimationController,过程中会同时处理多个应用打开和关闭的动画,mPendingAnimations里面保存着就是多个动画的记录。
如下,先回遍历构造出对于对应的RemoteAnimationTarget,并把这些RemoteAnimationTarget保存在列表中并返回:

RemoteAnimationController.java

private RemoteAnimationTarget[] createAppAnimations() {
        final ArrayList<RemoteAnimationTarget> targets = new ArrayList<>();
        // mPendingAnimations是之前构造出来的,遍历mPendingAnimations
        for (int i = mPendingAnimations.size() - 1; i >= 0; i--) {
            final RemoteAnimationRecord wrappers = mPendingAnimations.get(i);
            // 通过RemoteAnimationRecord构造出RemoteAnimationTarget对象并保存在返回列表中
            final RemoteAnimationTarget target = wrappers.createRemoteAnimationTarget();
            if (target != null) {
                ProtoLog.d(WM_DEBUG_REMOTE_ANIMATIONS, "\tAdd container=%s",
                        wrappers.mWindowContainer);
                targets.add(target);
            } else {
            .....
        }
        return targets.toArray(new RemoteAnimationTarget[targets.size()]);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

继续深入 RemoteAnimationTarget 是怎么构造出来的,继续追踪wrappers.createRemoteAnimationTarget()方法:
如下,mTarget是通过mWindowContainer.createRemoteAnimationTarget(this)构造出来的,而mWindowContainer就是执行动画的窗口容器,即新启动应用的Task,即调用了Task.createRemoteAnimationTarget

RemoteAnimationController.java

RemoteAnimationTarget createRemoteAnimationTarget() {
            if (mAdapter == null
                    || mAdapter.mCapturedFinishCallback == null
                    || mAdapter.mCapturedLeash == null) {
                return null;
            }
            // 这里
            mTarget = mWindowContainer.createRemoteAnimationTarget(this);
            return mTarget;
        
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

如下,可见Task.createRemoteAnimationTarget中还是调用了ActivityRecord的createRemoteAnimationTarget方法

Task.java

@Override
    RemoteAnimationTarget createRemoteAnimationTarget(
            RemoteAnimationController.RemoteAnimationRecord record) {
        final ActivityRecord activity = getTopMostActivity();
        return activity != null ? activity.createRemoteAnimationTarget(record) : null;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

最终会调用到ActivityRecord的createRemoteAnimationTarget方法:
如下可见 是把之前RemoteAnimationRecord中的众多参数填入到RemoteAnimationTarget中,我认为最关键的一个参数就是Leash,位置信息等:

ActivityRecord.java

@Override
    RemoteAnimationTarget createRemoteAnimationTarget(
            RemoteAnimationController.RemoteAnimationRecord record) {
        final WindowState mainWindow = findMainWindow();
        if (task == null || mainWindow == null) {
            return null;
        }
        final Rect insets = mainWindow.getInsetsStateWithVisibilityOverride().calculateInsets(
                task.getBounds(), Type.systemBars(), false /* ignoreVisibility */).toRect();
        InsetUtils.addInsets(insets, getLetterboxInsets());

        final RemoteAnimationTarget target = new RemoteAnimationTarget(task.mTaskId,
                record.getMode(), record.mAdapter.mCapturedLeash,/*leash*/ !fillsParent(),
                new Rect(), insets,
                getPrefixOrderIndex(), record.mAdapter.mPosition, record.mAdapter.mLocalBounds,
                record.mAdapter.mEndBounds, task.getWindowConfiguration(),
                false /*isNotInRecents*/,
                record.mThumbnailAdapter != null ? record.mThumbnailAdapter.mCapturedLeash : null,
                record.mStartBounds, task.getTaskInfo(), checkEnterPictureInPictureAppOpsState());
        target.setShowBackdrop(record.mShowBackdrop);
        target.setWillShowImeOnTarget(mStartingData != null && mStartingData.hasImeSurface());
        target.hasAnimatingParent = record.hasAnimatingParent();
        return target;
    }
  • 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

在了解完RemoteAnimationTarget是如何构造出来后,回归主线,开始触发:mRemoteAnimationAdapter.getRunner().onAnimationStart(transit, appTargets, wallpaperTargets, nonAppTargets, mFinishedCallback);

RemoteAnimationController.java

void goodToGo(@WindowManager.TransitionOldType int transit) {
        mFinishedCallback = new FinishedCallback(this)
        .....
        // 构造RemoteAnimationTarget
        final RemoteAnimationTarget[] appTargets = createAppAnimations();
        ......
        // Create the remote wallpaper animation targets (if any)
        final RemoteAnimationTarget[] wallpaperTargets = createWallpaperAnimations();

        // Create the remote non app animation targets (if any)
        final RemoteAnimationTarget[] nonAppTargets = createNonAppWindowAnimations(transit);
        mService.mAnimator.addAfterPrepareSurfacesRunnable(() -> {
            try {
                linkToDeathOfRunner();
                ProtoLog.d(WM_DEBUG_REMOTE_ANIMATIONS, "goodToGo(): onAnimationStart,"
                                + " transit=%s, apps=%d, wallpapers=%d, nonApps=%d",
                        AppTransition.appTransitionOldToString(transit), appTargets.length,
                        wallpaperTargets.length, nonAppTargets.length);
                mRemoteAnimationAdapter.getRunner().onAnimationStart(transit, appTargets,
                        wallpaperTargets, nonAppTargets, mFinishedCallback);
            } catch (RemoteException e) {
                Slog.e(TAG, "Failed to start remote animation", e);
                onAnimationFinished();
            }
            .......
        });
  • 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

如上,这里的RemoteAnimationAdapter就是一开始从Luanchar传过来的一个Parceble对象,getRunner()获得的是该对象中的其中IRemoteAnimationRunner类型的IBinder对象,public IRemoteAnimationRunner getRunner() { return mRunner;}在文章开始的时候讲过RemoteAnimationAdapter对象是Launchar通过binder发送给system_server的,而该对象中的IRemoteAnimationRunner类型的对象mRunner则是Launchar传递给system_server的一个IBinder对象,通过该对象System_server可以把执行动画的重要参数(主要是执行动画的Leash和动画时长等)通过binder从system_server传递给Launchar,launchar在收到执行动画的参数后,随机开始执行动画。

回到上文在RemoteAnimationController调用了 mRemoteAnimationAdapter.getRunner().onAnimationStart(transit, appTargets, wallpaperTargets, nonAppTargets, mFinishedCallback);

IRemoteAnimationRunner对象binder服务端的实现在Luanchar中

三、Launchar中正式开始刷新动画

具体在Launchar如何具体实现过渡动画大体上还是属性动画的流程,通过属性动画对桌面上的图标,对leash进行形状,位置,大小,透明度等属性的不断修改进而完成整个远程过渡动画的过程,需要注意的是,因为Launcahr可以在执行动画过程中能同时处理Leash和launchar上的view组件,这就能使得窗口动画和view的动画得到了同步。

RemoteAnimationAdapterCompat.java

private static IRemoteAnimationRunner.Stub wrapRemoteAnimationRunner(
            final RemoteAnimationRunnerCompat remoteAnimationAdapter) {
        return new IRemoteAnimationRunner.Stub() {
            @Override
            public void onAnimationStart(@TransitionOldType int transit,
                    RemoteAnimationTarget[] apps,
                    RemoteAnimationTarget[] wallpapers,
                    RemoteAnimationTarget[] nonApps,
                    final IRemoteAnimationFinishedCallback finishedCallback) {
                final RemoteAnimationTargetCompat[] appsCompat =
                        RemoteAnimationTargetCompat.wrap(apps);
                final RemoteAnimationTargetCompat[] wallpapersCompat =
                        RemoteAnimationTargetCompat.wrap(wallpapers);
                final RemoteAnimationTargetCompat[] nonAppsCompat =
                        RemoteAnimationTargetCompat.wrap(nonApps);
                final Runnable animationFinishedCallback = new Runnable() {
                    @Override
                    public void run() {
                        try {
                            finishedCallback.onAnimationFinished();
                        } catch (RemoteException e) {
                            Log.e("ActivityOptionsCompat", "Failed to call app controlled animation"
                                    + " finished callback", e);
                        }
                    }
                };
                remoteAnimationAdapter.onAnimationStart(transit, appsCompat, wallpapersCompat,
                        nonAppsCompat, animationFinishedCallback);
            }

            @Override
            public void onAnimationCancelled() {
                remoteAnimationAdapter.onAnimationCancelled();
            }
        };
    }
  • 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
 private class AppLaunchAnimationRunner implements RemoteAnimationFactory {

        private final View mV;
        private final RunnableList mOnEndCallback;

        AppLaunchAnimationRunner(View v, RunnableList onEndCallback) {
            mV = v;
            mOnEndCallback = onEndCallback;
        }

        @Override
        public void onCreateAnimation(int transit,
                RemoteAnimationTarget[] appTargets,
                RemoteAnimationTarget[] wallpaperTargets,
                RemoteAnimationTarget[] nonAppTargets,
                LauncherAnimationRunner.AnimationResult result) {
            AnimatorSet anim = new AnimatorSet();
            boolean launcherClosing =
                    launcherIsATargetWithMode(appTargets, MODE_CLOSING);

            final boolean launchingFromWidget = mV instanceof LauncherAppWidgetHostView;
            final boolean launchingFromRecents = isLaunchingFromRecents(mV, appTargets);
            final boolean skipFirstFrame;
            if (launchingFromWidget) {
                composeWidgetLaunchAnimator(anim, (LauncherAppWidgetHostView) mV, appTargets,
                        wallpaperTargets, nonAppTargets, launcherClosing);
                addCujInstrumentation(
                        anim, InteractionJankMonitorWrapper.CUJ_APP_LAUNCH_FROM_WIDGET);
                skipFirstFrame = true;
            } else if (launchingFromRecents) {
                composeRecentsLaunchAnimator(anim, mV, appTargets, wallpaperTargets, nonAppTargets,
                        launcherClosing);
                addCujInstrumentation(
                        anim, InteractionJankMonitorWrapper.CUJ_APP_LAUNCH_FROM_RECENTS);
                skipFirstFrame = true;
            } else {
                composeIconLaunchAnimator(anim, mV, appTargets, wallpaperTargets, nonAppTargets,
                        launcherClosing);
                addCujInstrumentation(anim, InteractionJankMonitorWrapper.CUJ_APP_LAUNCH_FROM_ICON);
                skipFirstFrame = false;
            }

            if (launcherClosing) {
                anim.addListener(mForceInvisibleListener);
            }

            result.setAnimation(anim, mLauncher, mOnEndCallback::executeAllAndDestroy,
                    skipFirstFrame);
        }

        @Override
        public void onAnimationCancelled() {
            mOnEndCallback.executeAllAndDestroy();
        }
    }
  • 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
LauncherAnimationRunner.java

public class LauncherAnimationRunner extends RemoteAnimationRunnerCompat {

    private static final RemoteAnimationFactory DEFAULT_FACTORY =
            (transit, appTargets, wallpaperTargets, nonAppTargets, result) ->
                    result.setAnimation(null, null);

    private final Handler mHandler;
    private final boolean mStartAtFrontOfQueue;
    private final WeakReference<RemoteAnimationFactory> mFactory;

    private AnimationResult mAnimationResult;

    /**
     * @param startAtFrontOfQueue If true, the animation start will be posted at the front of the
     *                            queue to minimize latency.
     */
    public LauncherAnimationRunner(Handler handler, RemoteAnimationFactory factory,
            boolean startAtFrontOfQueue) {
        mHandler = handler;
        mFactory = new WeakReference<>(factory);
        mStartAtFrontOfQueue = startAtFrontOfQueue;
    }

    // Called only in S+ platform
    @BinderThread
    public void onAnimationStart(
            int transit,
            RemoteAnimationTarget[] appTargets,
            RemoteAnimationTarget[] wallpaperTargets,
            RemoteAnimationTarget[] nonAppTargets,
            Runnable runnable) {
        Runnable r = () -> {
            finishExistingAnimation();
            mAnimationResult = new AnimationResult(() -> mAnimationResult = null, runnable);
            getFactory().onCreateAnimation(transit, appTargets, wallpaperTargets, nonAppTargets,
                    mAnimationResult);
        };
        if (mStartAtFrontOfQueue) {
            postAtFrontOfQueueAsynchronously(mHandler, r);
        } else {
            postAsyncCallback(mHandler, r);
        }
    }

    private RemoteAnimationFactory getFactory() {
        RemoteAnimationFactory factory = mFactory.get();
        return factory != null ? factory : DEFAULT_FACTORY;
    }

    @UiThread
    private void finishExistingAnimation() {
        if (mAnimationResult != null) {
            mAnimationResult.finish();
            mAnimationResult = null;
        }
    }

    /**
     * Called by the system
     */
    @BinderThread
    @Override
    public void onAnimationCancelled(boolean isKeyguardOccluded) {
        postAsyncCallback(mHandler, () -> {
            finishExistingAnimation();
            getFactory().onAnimationCancelled();
        });
    }

    public static final class AnimationResult {

        private final Runnable mSyncFinishRunnable;
        private final Runnable mASyncFinishRunnable;

        private AnimatorSet mAnimator;
        private Runnable mOnCompleteCallback;
        private boolean mFinished = false;
        private boolean mInitialized = false;

        private AnimationResult(Runnable syncFinishRunnable, Runnable asyncFinishRunnable) {
            mSyncFinishRunnable = syncFinishRunnable;
            mASyncFinishRunnable = asyncFinishRunnable;
        }

        @UiThread
        private void finish() {
            if (!mFinished) {
                mSyncFinishRunnable.run();
                UI_HELPER_EXECUTOR.execute(() -> {
                    mASyncFinishRunnable.run();
                    if (mOnCompleteCallback != null) {
                        MAIN_EXECUTOR.execute(mOnCompleteCallback);
                    }
                });
                mFinished = true;
            }
        }

        @UiThread
        public void setAnimation(AnimatorSet animation, Context context) {
            setAnimation(animation, context, null, true);
        }

        /**
         * Sets the animation to play for this app launch
         * @param skipFirstFrame Iff true, we skip the first frame of the animation.
         *                       We set to false when skipping first frame causes jank.
         */
        @UiThread
        public void setAnimation(AnimatorSet animation, Context context,
                @Nullable Runnable onCompleteCallback, boolean skipFirstFrame) {
            if (mInitialized) {
                throw new IllegalStateException("Animation already initialized");
            }
            mInitialized = true;
            mAnimator = animation;
            mOnCompleteCallback = onCompleteCallback;
            if (mAnimator == null) {
                finish();
            } else if (mFinished) {
                // Animation callback was already finished, skip the animation.
                mAnimator.start();
                mAnimator.end();
                if (mOnCompleteCallback != null) {
                    mOnCompleteCallback.run();
                }
            } else {
                // Start the animation
                mAnimator.addListener(new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationEnd(Animator animation) {
                        finish();
                    }
                });
                mAnimator.start();

                if (skipFirstFrame) {
                    // Because t=0 has the app icon in its original spot, we can skip the
                    // first frame and have the same movement one frame earlier.
                    mAnimator.setCurrentPlayTime(
                            Math.min(getSingleFrameMs(context), mAnimator.getTotalDuration()));
                }
            }
        }
    }

    /**
     * Used with LauncherAnimationRunner as an interface for the runner to call back to the
     * implementation.
     */
    @FunctionalInterface
    public interface RemoteAnimationFactory {

        /**
         * Called on the UI thread when the animation targets are received. The implementation must
         * call {@link AnimationResult#setAnimation} with the target animation to be run.
         */
        void onCreateAnimation(int transit,
                RemoteAnimationTarget[] appTargets,
                RemoteAnimationTarget[] wallpaperTargets,
                RemoteAnimationTarget[] nonAppTargets,
                LauncherAnimationRunner.AnimationResult result);

        /**
         * Called when the animation is cancelled. This can happen with or without
         * the create being called.
         */
        default void onAnimationCancelled() { }
    }
}
  • 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
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小小林熬夜学编程/article/detail/523757
推荐阅读
相关标签
  

闽ICP备14008679号