Android V app 冷启动(8) 动画结束

WMShell finish transition

当 Launcher 执行完远程动画之时,会通知 WMShell,最终调用 Transitions#onFinish(),如下

java 复制代码
// Transitions.java

// wct 为 null
// wctCB 为 null
private void onFinish(ActiveTransition active,
        @Nullable WindowContainerTransaction wct,
        @Nullable WindowContainerTransactionCallback wctCB) {
    final Track track = mTracks.get(active.getTrack());
    track.mActiveTransition = null;

    // ...
    
    // 1. apply finish transaction
    SurfaceControl.Transaction fullFinish = active.mFinishT;
    if (active.mMerged != null) {
       // ...
    }
    if (fullFinish != null) {
        fullFinish.apply();
    }
    
    // ...
    
    // 2. 通知 WMCore finish transition
    mOrganizer.finishTransition(active.mToken, wct, wctCB);
    
    // ...
}

WMShell 在 apply finish transaction 后,通知 WMCore finish transition。

WMCore finish transition

java 复制代码
// WindowOrganizerController.java

// t 为 null
// callback 为 null
public int finishTransition(@NonNull IBinder transitionToken,
        @Nullable WindowContainerTransaction t,
        @Nullable IWindowContainerTransactionCallback callback) {
    enforceTaskPermission("finishTransition()");
    final CallerInfo caller = new CallerInfo();
    final long ident = Binder.clearCallingIdentity();
    try {
        synchronized (mGlobalLock) {
            // ...
            
            // 根据 token 获取 Transition
            final Transition transition = Transition.fromBinder(transitionToken);
            
            // ...
            
            // 由 TransitionController 执行 finish transition
            mTransitionController.finishTransition(transition);
            mTransitionController.mFinishingTransition = null;
            
            // ...
        }
    } finally {
        Binder.restoreCallingIdentity(ident);
    }
}
java 复制代码
// TransitionController.java

void finishTransition(Transition record) {
    // ...
    
    ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Finish Transition: %s", record);
    
    // 注意,先从 mPlayingTransitions 移除,在执行 Transition#finishTransition()
    mPlayingTransitions.remove(record);
    
    // ...
    
    record.finishTransition();
    
    // ...
}
java 复制代码
// Transition.java

void finishTransition() {
    // ...
    
    // apply cleanup transaction
    if (mCleanupTransaction != null) {
        mCleanupTransaction.apply();
        mCleanupTransaction = null;
    }
    
    // ...
    
    // 标记 finishing transition
    mController.mFinishingTransition = this;
    
    // ...
    
    // 1. 提交即将不可见的 ActivityRecord 
    // Commit all going-invisible containers
    for (int i = 0; i < mParticipants.size(); ++i) {
        final WindowContainer<?> participant = mParticipants.valueAt(i);
        final ActivityRecord ar = participant.asActivityRecord();
        if (ar != null) {
            final Task task = ar.getTask();
            if (task == null) continue;
            
            // 检测哪些 WindowToken 在 transitin finish 是还可见
            boolean visibleAtTransitionEnd = mVisibleAtTransitionEndTokens.contains(ar);

            if (isTransientLaunch(ar) && !ar.isVisibleRequested()
                    && mController.inCollectingTransition(ar)) {
                // ...
            }
            
            final boolean isScreenOff = ar.mDisplayContent == null
                    || ar.mDisplayContent.getDisplayInfo().state == Display.STATE_OFF;
            if ((!visibleAtTransitionEnd || isScreenOff) && !ar.isVisibleRequested()) {
            
                // 检测 Activity 是否进入PIP
                final boolean commitVisibility = !checkEnterPipOnFinish(ar);
                
                if (commitVisibility) { // Activity不进入 PIP
                    ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
                            "  Commit activity becoming invisible: %s", ar);
                    final SnapshotController snapController = mController.mSnapshotController;
                    if (mTransientLaunches != null && !task.isVisibleRequested()
                            && !task.isActivityTypeHome()) {
                        // ...
                    }
                    
                    // 提交 ActivityRecord 不可见
                    ar.commitVisibility(false /* visible */, false /* performLayout */,
                            true /* fromTransition */);
                    committedSomeInvisible = true;
                } else {
                    
                }
            }
            
            // ...
            
            continue;
        }
        
        // ...
    }
    
    
    // 2. wallpaper target 不可见,那么提交 WallpaperWindowToken 不可见
    // Commit wallpaper visibility after activity, because usually the wallpaper target token is
    // an activity, and wallpaper's visibility depends on activity's visibility.
    for (int i = mParticipants.size() - 1; i >= 0; --i) {
        final WallpaperWindowToken wt = mParticipants.valueAt(i).asWallpaperToken();
        if (wt == null) continue;
        
        final WindowState target = wt.mDisplayContent.mWallpaperController.getWallpaperTarget();
        final boolean isTargetInvisible = target == null || !target.mToken.isVisible();
        final boolean isWallpaperVisibleAtEnd =
                wt.isVisibleRequested() || mVisibleAtTransitionEndTokens.contains(wt);
        
        // wallpaper target 不可见,提交 WallpaperWindowToken 不可见
        if (isTargetInvisible || !isWallpaperVisibleAtEnd) {
            ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
                    "  Commit wallpaper becoming invisible: %s", wt);
            wt.commitVisibility(false /* visible */);
        }
        
        // 在 layout change 标记 FINISH_LAYOUT_REDO_WALLPAPER
        // 在窗口刷新时,会重新计算 wallpaper target
        if (isTargetInvisible) {
            // Our original target went invisible, so we should look for a new target.
            wt.mDisplayContent.pendingLayoutChanges |= FINISH_LAYOUT_REDO_WALLPAPER;
        }
    }
    
    // ...

    // 3. 通知监听者 transition finish
    // dispatch legacy callback in a different loop. This is because multiple legacy handlers
    // (fixed-rotation/displaycontent) make global changes, so we want to ensure that we've
    // processed all the participants first (in particular, we want to trigger pip-enter first)
    for (int i = 0; i < mParticipants.size(); ++i) {
    
        // 必须有 ActivityRecord 参与到 Transition
        final ActivityRecord ar = mParticipants.valueAt(i).asActivityRecord();
        if (ar == null) continue;

        // 此时,待启动的 ActivityRecord 是可见的
        if ((ar.isVisibleRequested() || !ar.isState(ActivityRecord.State.INITIALIZING))
                && !ar.isAnimating(PARENTS, ANIMATION_TYPE_PREDICT_BACK)) {
            mController.dispatchLegacyAppTransitionFinished(ar);
        }

        // ...
    }

    // ...

    // 4.调度 stop activity
    // Always schedule stop processing when transition finishes because activities don't
    // stop while they are in a transition thus their stop could still be pending.
    mController.mAtm.mTaskSupervisor
            .scheduleProcessStoppingAndFinishingActivitiesIfNeeded();

    // ...

    // Transition 状态切换到 STATE_FINISHED
    mState = STATE_FINISHED;
    
    // ...
    
    // 此时没有 finishing transition
    mController.mFinishingTransition = null;
    
    // ...
}

finish transition

  1. Transition 的参与者中,如果有不可见的 ActivityRecord,提交其不可见。对于本案例来说,Launcher ActivityRecord 不可见,因此对它提交了不可见,即更新 ActivityRecord#mVisible 和 ActivityRecord#mClientVisible 为 false。
  2. Launcher ActivityRecord 现已被提交不可见,导致其下的 wallpaper target 窗口即将不可见。因此,得提交 WallpaperWindowToken 的不可见。
  3. 通知监听者 transition finish。在 play transition 前,DisplayContent update orientation 时,执行了 Fixed Rotation,导致推迟了 update orientation。现在 transition finish,DisplayContent 需继续执行 update orientation,更新系统 rotation。
  4. 调度 stop activity。根据 Android V app 冷启动 (1) Activity生命周期 分析,Launcher ActivityRecord 处于 transition 中,无法执行 stop activity。还需正在启动的 Activity 上报 idle ,才可以执行 Launcher Activity 的 stop activity 流程。

提交 WallpaperWindowToken 不可见

java 复制代码
// WallpaperWindowToken.java

// 参数 visible 为 false
/** Commits the visibility of this token. This will directly update the visibility. */
void commitVisibility(boolean visible) {
    if (visible == isVisible()) return;

    ProtoLog.v(WM_DEBUG_APP_TRANSITIONS,
            "commitVisibility: %s: visible=%b mVisibleRequested=%b", this,
            isVisible(), mVisibleRequested);

    // 更新 mVisibleRequested 为 false
    setVisibleRequested(visible);
    
    // 只更新 mClientVisible 为 false
    // 注意,WallpaperWindowToken 的 mClientVisible 就是 mVisible
    setVisible(visible);
}

// 参数 visible 为 false
private void setVisible(boolean visible) {
    final boolean wasClientVisible = isClientVisible();
    // 更新 mClientVisible 为 false
    setClientVisible(visible);
    
    if (visible && !wasClientVisible) {
        
    }
}

WallpaperWindowToken 的提交不可见,只需更新 mVisibleRequested 和 mClientVisible 为 false。WallpaperWindowToken 并没有 mVisible 属性,因为它是 ActivityRecord 专有的属性。

注意,mClientVisible 被更新为 false,会通知 app 端窗口不可见,app 端不会再绘制壁纸窗口。

DisplayContent 处理 transition finish

java 复制代码
// DisplayContent.java

class FixedRotationTransitionListener extends WindowManagerInternal.AppTransitionListener {

    public void onAppTransitionFinishedLocked(IBinder token) {
        // 这个是正在启动的 ActivityRecord
        final ActivityRecord r = ActivityRecord.forTokenLocked(token);
          
        // ...
        
        continueUpdateOrientationForDiffOrienLaunchingApp();
    }
}


/**
 * Continue updating the orientation change of display if it was deferred by a top activity
 * launched in a different orientation.
 */
void continueUpdateOrientationForDiffOrienLaunchingApp() {
    // ...
    
    
    // 1.继续 update orientation
    // Update directly because the app which will change the orientation of display is ready.
    if (mDisplayRotation.updateOrientation(getOrientation(), false /* forceUpdate */)) {
        // If a transition is collecting, let the transition apply the rotation change on
        // display thread. See Transition#shouldApplyOnDisplayThread().
        if (!mTransitionController.isCollecting(this)) {
            // 2. 发送新配置
            sendNewConfiguration();
        }
        return;
    }
    
    // ...
}

play transition 前的 DisplayContent 的 update orientation,由于执行了 fixed rotation 而被推迟。

现在,transition finish,得继续执行 DisplayContent update orientation。此时,会导致系统 rotation 被更新,从而触发配置更新。

play transition 前的 apply fixed rotation,对正在启动的 ActivityRecord surface 设置了 transform,使横屏绘制的 Activity 窗口,可以"正常地"显示在竖屏的屏幕上。

现在,系统 rotation 的更新,会导致屏幕方向的更新,此时需要移除正在启动的 ActivityRecord surface 的 transform,使横屏绘制的窗口,真正地可以正常的显示在横屏的屏幕上。这就是配置更新中的 finish fixed rotation 所做的。

update orientation

java 复制代码
// DisplayRotation.java

// newOrientation 是计算出的 orientation
// forceUpdate 为 false
boolean updateOrientation(@ScreenOrientation int newOrientation, boolean forceUpdate) {
    if (newOrientation == mLastOrientation && !forceUpdate) {
        return false;
    }
    
    // 更新 orientation
    mLastOrientation = newOrientation;
    
    if (newOrientation != mCurrentAppOrientation) {
        mCurrentAppOrientation = newOrientation;
        if (isDefaultDisplay) {
            updateOrientationListenerLw();
        }
    }
    
    // 更新 rotation
    return updateRotationUnchecked(forceUpdate);
}



boolean updateRotationUnchecked(boolean forceUpdate) {
    // ...

    final int oldRotation = mRotation;
    final int lastOrientation = mLastOrientation;
    
    // 根据 orientation 计算 rotation
    int rotation = rotationForOrientation(lastOrientation, oldRotation);
    
    
    // ...

    // 保存 rotation
    mRotation = rotation;

    // 标记 DisplayContent 需要 layout
    mDisplayContent.setLayoutNeeded();
    
    // 标记 DisplayContent 正在等待配置更新
    mDisplayContent.mWaitingForConfig = true;

    if (mDisplayContent.mTransitionController.isShellTransitionsEnabled()) {
        // 此时已经没有处于 collecting 的 Transition
        final boolean wasCollecting = mDisplayContent.mTransitionController.isCollecting();
        
        if (!wasCollecting) {
            if (mDisplayContent.getLastHasContent()) {
                
                // 请求类型为 CHANGE 的 transition
                final TransitionRequestInfo.DisplayChange change =
                        new TransitionRequestInfo.DisplayChange(mDisplayContent.getDisplayId(),
                                oldRotation, mRotation);
                mDisplayContent.requestChangeTransition(
                        ActivityInfo.CONFIG_WINDOW_CONFIGURATION, change);
            }
        } else {
            // ...
        }
        return true;
    }

   
    // ...
}

update orientation 触发了 rotation 更新,导致请求了一个类型为 CHANGE transition,这是一个无缝的屏幕旋转动画。屏幕旋转动画,我在前面的文章中分析过,它会导致屏幕方向的更新。

我在前面的文章中,详细分析过屏幕旋转动画,而无缝的屏幕旋转动画,只是看不到动画而已。

更新配置

由于 update orientation 触发了 rotation 更新,因此需要更新系统配置。我在屏幕旋转动画的系列文章中,已经详细分析过配置更新,这里只展示与 Fixed Rotation 相关的配置更新

java 复制代码
// DisplayContent.java

public void onRequestedOverrideConfigurationChanged(Configuration overrideConfiguration) {
    final Configuration currOverrideConfig = getRequestedOverrideConfiguration();
    final int currRotation = currOverrideConfig.windowConfiguration.getRotation();
    final int overrideRotation = overrideConfiguration.windowConfiguration.getRotation();
    if (currRotation != ROTATION_UNDEFINED && overrideRotation != ROTATION_UNDEFINED
            && currRotation != overrideRotation) {
        // rotation 改变,执行 finish fixed rotation
        applyRotationAndFinishFixedRotation(currRotation, overrideRotation);
    }
    
    // ...
    
    // 通过基类方法,更新配置
    super.onRequestedOverrideConfigurationChanged(overrideConfiguration);
    
    // ...
}



/**
 * If the launching rotated activity ({@link #mFixedRotationLaunchingApp}) is null, it simply
 * applies the rotation to display. Otherwise because the activity has shown as rotated, the
 * fixed rotation transform also needs to be cleared to make sure the rotated activity fits
 * the display naturally.
 */
private void applyRotationAndFinishFixedRotation(int oldRotation, int newRotation) {
    final WindowToken rotatedLaunchingApp = mFixedRotationLaunchingApp;
    if (rotatedLaunchingApp == null) {
        // ...
    }

    // finish fixed rotation,并且 apply rotation
    rotatedLaunchingApp.finishFixedRotationTransform(
            () -> applyRotation(oldRotation, newRotation));
            
    // 重置 mFixedRotationLaunchingApp 为 null
    setFixedRotationLaunchingAppUnchecked(null);
}
java 复制代码
// WindowToken.java

/**
 * Finishes the transform and apply display rotation if the action is given. If the display will
 * not rotate, the transformed containers are restored to their original states.
 */
void finishFixedRotationTransform(Runnable applyDisplayRotation) {
    final FixedRotationTransformState state = mFixedRotationTransformState;
    if (state == null) {
        return;
    }
    // 重置 FixedRotationTransformState
    state.resetTransform();
    
    // Clear the flag so if the display will be updated to the same orientation, the transform
    // won't take effect.
    state.mIsTransforming = false;
    
    // 执行回调 applyRotation()
    if (applyDisplayRotation != null) {
        applyDisplayRotation.run();
    }
    
    // 清理相关 WindowToken#mFixedRotationTransformState
    // The state is cleared at the end, because it is used to indicate that other windows can
    // use seamless rotation when applying rotation to display.
    for (int i = state.mAssociatedTokens.size() - 1; i >= 0; i--) {
        final WindowToken token = state.mAssociatedTokens.get(i);
        token.mFixedRotationTransformState = null;
        if (applyDisplayRotation == null) {
            // ...
        }
    }
}

注意,finish fixed rotation 是在 DisplayContent 正式更新配置前执行的。而它只是清理了正在启动 ActivityRecord 的 FixedRotationTransformState。当 ActivityRecord 正式更新配置时,会 update surface position,正式清理 ActivityRecord 的 Fixed Rotation transform。

相关推荐
2501_915106321 小时前
iOS 打包 IPA 全流程详解,签名配置、工具选择与跨平台上传实战指南
android·macos·ios·小程序·uni-app·cocoa·iphone
超低空1 小时前
Android MediaSession深度解析:车载音乐播放器完整案例
android·架构·客户端
QmDeve1 小时前
Android 集成与使用模糊开关按钮视图 (BlurSwitchButtonView)
android·github
00后程序员张1 小时前
iOS 混淆实操指南多工具组合实现 IPA 混淆、加固与发布治理 IPA 加固
android·ios·小程序·https·uni-app·iphone·webview
xiaoshiquan12062 小时前
as强制过滤指定依赖版本库,解决该依赖不同版本冲突
android
2501_929157683 小时前
Switch 20.5.0系统最新PSP模拟器懒人包
android·游戏·ios·pdf
用户095 小时前
Kotlin Flow的6个必知高阶技巧
android·面试·kotlin
用户095 小时前
Flutter插件与包的本质差异
android·flutter·面试
用户095 小时前
Jetpack Compose静态与动态CompositionLocal深度解析
android·面试·kotlin
聆风吟º8 小时前
【Spring Boot 报错已解决】别让端口配置卡壳!Spring Boot “Binding to target failed” 报错解决思路
android·java·spring boot