Android无缝旋转:Fixed Rotation

1,956 阅读7分钟

Android WMS动画管理专栏

基本概念和原理

是什么

FixedRotation解决在activity启动时发生方向变化的时候,需要旋转动画的问题。FixedRotation能够保存当前方向,走无缝旋转逻辑,从而实现activity的无缝切换。

原理

当启动Acitivity时判断到方向发生改变的时候,系统会模拟转屏后的相关信息(DisplayAdjustment),然后将这个相关信息传给应用。当应用拿到新的信息之后发起绘制,系统会基于旧方向对新方向进行方向补偿,这样就不需要进行转屏,Activity的切换就可以使用原过渡动画,过渡动画完成之后再进行转屏,从而实现无缝旋转。

时机

进行Fixedrotation必须在应用绘制好之前就进行判断,也就是onResume之前要确定。 否则应用已经绘制完成那么在走Fixedrotation还是要重新绘制会导致屏幕跳闪。

源码流程

基于AOSP android-12代码分析

流程起点

resumeTopActivity时从updateOrientation调用到handleTopActivityLaunchingInDifferentOrientation开始流程

调用堆栈如下:

handleTopActivityLaunchingInDifferentOrientation:1739, DisplayContent (com.android.server.wm) updateOrientation:1688, DisplayContent (com.android.server.wm) updateOrientation:1640, DisplayContent (com.android.server.wm) ensureVisibilityAndConfig:1894, RootWindowContainer (com.android.server.wm) resumeTopActivity:1499, TaskFragment (com.android.server.wm) resumeTopActivityInnerLocked:5875, Task (com.android.server.wm) resumeTopActivityUncheckedLocked:5801, Task (com.android.server.wm) resumeFocusedTasksTopActivities:2544, RootWindowContainer (com.android.server.wm) resumeFocusedTasksTopActivities:2530, RootWindowContainer (com.android.server.wm) completePause:1877, TaskFragment (com.android.server.wm) activityPaused:6582, ActivityRecord (com.android.server.wm) activityPaused:182, ActivityClientController (com.android.server.wm) onTransact:556, IActivityClientController$Stub (android.app) onTransact:122, ActivityClientController (com.android.server.wm) execTransactInternal:1190, Binder (android.os) execTransact:1149, Binder (android.os)

判断是否设置FixedRotation

DisplayContent#handleTopActivityLaunchingInDifferentOrientation

前面的大段逻辑都是在判断不能设置FixedRotation的例外情况,return false

    /**
     * 当一个处于不同的orientation的Activity被启动时,我们需要在一段时间内
     * 保持固定的display rotation,直到启动动画完成,以免以错误的orientation
     * 显示先前的Activity
     *
     * @param r 可能改变display orientation的启动中的Activity
     * @param checkOpening 是否需要检查活动正在执行transition;
     * 如果caller不确定活动是否正在launching,设置true
     * @return fixed rotation启动了返回true
     */
    boolean handleTopActivityLaunchingInDifferentOrientation(@NonNull ActivityRecord r,
            boolean checkOpening) {
        if (!WindowManagerService.ENABLE_FIXED_ROTATION_TRANSFORM) {
            return false;
        }
        if (r.isFinishingFixedRotationTransform()) {
            return false;
        }
        if (r.hasFixedRotationTransform()) {
            // It has been set and not yet finished.
            return true;
        }
        if (r.isVisible()) {
            return false;
        }
        if (!r.occludesParent()) {
            // 启动半透明或浮动Activity,在后台有一个可见Activity,
            // 那么他仍需要rotation动画来覆盖Activity的配置变化
            if (!(mFixedRotationLaunchingApp != null && mFixedRotationLaunchingApp != r)) {
                return false;
            }
        }
        if (r.attachedToProcess() && mayImeShowOnLaunchingActivity(r)) {
            // 目前还不知道IME窗口何时能准备好。Reject这种情况,
            // 以避免在不一致的方向上显示IME而出现闪动。
            return false;
        }
        if (checkOpening) {
            if (!mAppTransition.isTransitionSet() || !mOpeningApps.contains(r)) {
                // 没有activity switch时设置了不同的orientation或transition被unset{@link #mSkipAppTransitionAnimation}
                return false;
            }
            if (r.isState(RESUMED) && !r.getRootTask().mInResumeTopActivity) {
                // 如果Actvity正在执行或已经完成了生命周期回调,那么就使用正常的旋转动画,
                // 这样就可以立即更新显示信息(见 updateDisplayAndOrientation)。 
                // 这可以防止出现兼容性问题,比如在Activity#onCreate中
                // 调用setRequestedOrientation,然后获取显示信息。
                // 如果应用了FixedRotation,显示的旋转仍然是旧的。
                // 除非客户端在Ajustments arrive后再次获取旋转。
                return false;
            }
        } else if (r != topRunningActivity()) {
            // If the transition has not started yet, the activity must be the top.
            return false;
        }
        if (mLastWallpaperVisible && r.windowsCanBeWallpaperTarget()
                && mFixedRotationTransitionListener.mAnimatingRecents == null) {
            // 如果没有在执行recents动画(maybe上滑到桌面场景),使用正常的旋转动画来改变可见的壁纸方向
            return false;
        }
        final int rotation = rotationForActivityInDifferentOrientation(r);
        if (rotation == ROTATION_UNDEFINED) {
            // displayrotation不会被当前的topActivity所改变
            // 客户端对于前一个rotated activity的ajustment需要提前清除
            // 否则如果当前的top是在同一个process里,他能获取到旋转后的state
            // transfrom将在后面通过transition回调清除,以确保动画流畅
            if (hasTopFixedRotationLaunchingApp()) {
                mFixedRotationLaunchingApp.notifyFixedRotationTransform(false /* enabled */);
            }
            return false;
        }
        if (!r.getDisplayArea().matchParentBounds()) {
            // Because the fixed rotated configuration applies to activity directly, if its parent
            // has it own policy for bounds, the activity bounds based on parent is unknown.
            return false;
        }

        setFixedRotationLaunchingApp(r, rotation);
        return true;
    }

设置FixedRotation

DisplayContent#setFixedRotationLaunchingApp

    /**
     * Sets the provided record to {@link #mFixedRotationLaunchingApp} if possible to apply fixed
     * rotation transform to it and indicate that the display may be rotated after it is launched.
     */
    void setFixedRotationLaunchingApp(@NonNull ActivityRecord r, @Rotation int rotation) {
        final WindowToken prevRotatedLaunchingApp = mFixedRotationLaunchingApp;
        if (prevRotatedLaunchingApp == r
                && r.getWindowConfiguration().getRotation() == rotation) {
            // The given launching app and target rotation are the same as the existing ones.
            return;
        }
        if (prevRotatedLaunchingApp != null
                && prevRotatedLaunchingApp.getWindowConfiguration().getRotation() == rotation
                // It is animating so we can expect there will have a transition callback.
                && prevRotatedLaunchingApp.isAnimating(TRANSITION | PARENTS)) {
            // 可能存在多个Activity连续启动的情况。因为他们的rotation是一样的,
            // 所以transformed state可以共享以避免重复的繁重操作。
            r.linkFixedRotationTransform(prevRotatedLaunchingApp);
            if (r != mFixedRotationTransitionListener.mAnimatingRecents) {
                // 只更新普通activity的record,这样当transition完成他成为top activity
                // 时就可以更新dislay rotation. 近期任务可以在recents animation完成时处理
                setFixedRotationLaunchingAppUnchecked(r, rotation);
            }
            return;
        }

        if (!r.hasFixedRotationTransform()) {
            // 模拟出转屏后的屏幕
            startFixedRotationTransform(r, rotation);
        }
        setFixedRotationLaunchingAppUnchecked(r, rotation);
        if (prevRotatedLaunchingApp != null) {
            prevRotatedLaunchingApp.finishFixedRotationTransform();
        }
    }

setFixedRotationLaunchingApp 时序图

image_yvWlL-6HTQ.png

DisplayContent#startFixedRotationTransform

  1. 基于新方向得到displayInfo的configuration

  2. 得到计算DisplayCutout的信息方向

    private void startFixedRotationTransform(WindowToken token, int rotation) {
        mTmpConfiguration.unset();
        // 计算rotation模拟的displayinfo
        final DisplayInfo info = computeScreenConfiguration(mTmpConfiguration, rotation);
        // 计算rotation模拟的displaycutout、roundedcorner、indicatorbounds
        final WmDisplayCutout cutout = calculateDisplayCutoutForRotation(rotation);
        final RoundedCorners roundedCorners = calculateRoundedCornersForRotation(rotation);
        final PrivacyIndicatorBounds indicatorBounds =
                calculatePrivacyIndicatorBoundsForRotation(rotation);
        final DisplayFrames displayFrames = new DisplayFrames(mDisplayId, new InsetsState(), info,
                cutout, roundedCorners, indicatorBounds);
        // 使用上面计算的信息模拟屏幕
        token.applyFixedRotationTransform(info, displayFrames, mTmpConfiguration);
    }

startFixedRotationTransform时序图

DisplayContent_startFixedRotationTransform_Gf7Hhig.png

计算模拟旋转信息并传递给app端

WindowToken#applyFixedRotationTransform

  1. 创建FixedRotationTransformState对象

  2. 模拟inset信息 (使用FixedRotation之前状态栏、导航栏的状态)

  3. 通知应用  onConfigurationChanged

  4. mFixedRotationTransformState将在发送给client端的FixedRotationAjustments创建时使用

    /** Applies the rotated layout environment to this token in the simulated rotated display. */
    void applyFixedRotationTransform(DisplayInfo info, DisplayFrames displayFrames,
            Configuration config) {
        if (mFixedRotationTransformState != null) {
            mFixedRotationTransformState.disassociate(this);
        }
        mFixedRotationTransformState = new FixedRotationTransformState(info, displayFrames,
                new Configuration(config), mDisplayContent.getRotation());
        mFixedRotationTransformState.mAssociatedTokens.add(this);
        mDisplayContent.getDisplayPolicy().simulateLayoutDisplay(displayFrames,
                mFixedRotationTransformState.mBarContentFrames);
        onFixedRotationStatePrepared();
    }

DisplayPolicy#simulateLayoutDisplay

计算用于layout window的displayframes(logical size、rotation和cutout已经设置好) 这个方法仅变更传进来的displayframes、insets和一些临时state,不改变真正用于屏幕窗口显示的window frames

   void simulateLayoutDisplay(DisplayFrames displayFrames, SparseArray<Rect> barContentFrames) {
        final WindowFrames simulatedWindowFrames = new WindowFrames();
        if (mNavigationBar != null) {
            simulateLayoutDecorWindow(mNavigationBar, displayFrames, simulatedWindowFrames,
                    barContentFrames, contentFrame -> layoutNavigationBar(displayFrames,
                            contentFrame));
        }
        if (mStatusBar != null) {
            simulateLayoutDecorWindow(mStatusBar, displayFrames, simulatedWindowFrames,
                    barContentFrames, contentFrame -> layoutStatusBar(displayFrames, contentFrame));
        }
    }

WindowToken#onFixedRotationStatePrepared

使旋转后的state在windowcontainer和client端进程生效

    private void onFixedRotationStatePrepared() {
        // 先发送ajustment info到客户端
        // 这样客户端收到configuration change时可以获取旋转后的display metrics
        notifyFixedRotationTransform(true /* enabled */);
        // Resolve the rotated configuration.
        onConfigurationChanged(getParent().getConfiguration());
        final ActivityRecord r = asActivityRecord();
        if (r != null && r.hasProcess()) {
            // The application needs to be configured as in a rotated environment for compatibility.
            // This registration will send the rotated configuration to its process.
            r.app.registerActivityConfigurationListener(r);
        }
    }

WindowToken#notifyFixedRotationTransform

通知应用端启用/禁用 displayinfo的rotation adjustment

    void notifyFixedRotationTransform(boolean enabled) {
        FixedRotationAdjustments adjustments = null;
        // 一个token可以包含相同进程或不同进程的窗口
        // 该列表用于避免多次向进程发送相同的adjustment
        ArrayList<WindowProcessController> notifiedProcesses = null;
        for (int i = mChildren.size() - 1; i >= 0; i--) {
            final WindowState w = mChildren.get(i);
            final WindowProcessController app;
            if (w.mAttrs.type == TYPE_APPLICATION_STARTING) {
                // Use the host activity because starting window is controlled by window manager.
                final ActivityRecord r = asActivityRecord();
                if (r == null) {
                    continue;
                }
                app = r.app;
            } else {
                app = mWmService.mAtmService.mProcessMap.getProcess(w.mSession.mPid);
            }
            if (app == null || !app.hasThread()) {
                continue;
            }
            if (notifiedProcesses == null) {
                notifiedProcesses = new ArrayList<>(2);
                // 使用mFixedRotationTransformState里的信息构建FixedRotationAdjustments
                adjustments = enabled ? createFixedRotationAdjustmentsIfNeeded() : null;
            } else if (notifiedProcesses.contains(app)) {
                continue;
            }
            notifiedProcesses.add(app);
            try {
                // FixedRotationAdjustmentsItem通知app端
                mWmService.mAtmService.getLifecycleManager().scheduleTransaction(
                        app.getThread(), FixedRotationAdjustmentsItem.obtain(token, adjustments));
            } catch (RemoteException e) {
                Slog.w(TAG, "Failed to schedule DisplayAdjustmentsItem to " + app, e);
            }
        }
    }

ActivityThread端接收FixedRotationAdjustments

ActivityThread#handleFixedRotationAdjustments

FixedRotationAdjustmentsItem是一个ClientTransactionItem,流程将跳转到app端进程的ActivityThread中执行。这里定义了Consumer接口: displayAdjustments -> displayAdjustments.setFixedRotationAdjustments(fixedRotationAdjustments)

    /**
     * 应用rotation adjustment以覆盖属于所提供的token里的显示信息。
     * 如果是activity token,那么adjustment也适用于application,
     * 因为activity的外观通常对application resource更敏感。
     * @param token就是上面FixedRotationAdjustmentsItem.obtain里传递的token
     * @param fixedRotationAdjustments 用于覆盖Resources#mOverrideDisplayAdjustments
     *                                 如果是null就清空现有的
     */
    @Override
    public void handleFixedRotationAdjustments(@NonNull IBinder token,
            @Nullable FixedRotationAdjustments fixedRotationAdjustments) {
        final Consumer<DisplayAdjustments> override = fixedRotationAdjustments != null
                ? displayAdjustments -> displayAdjustments
                        .setFixedRotationAdjustments(fixedRotationAdjustments)
                : null;
        if (!mResourcesManager.overrideTokenDisplayAdjustments(token, override)) {
            // No resources are associated with the token.
            return;
        }
        if (mActivities.get(token) == null) {
            // Nothing to do for application if it is not an activity token.
            return;
        }

        overrideApplicationDisplayAdjustments(token, override);
    }

将DisplayAdjustments设置进Application的Resources

调用Resouces#overrideDisplayAdjustments方法设置resources的mOverrideDisplayAdjustments

    /**
     * 将最后一次override应用与application的reources以实现兼容,
     * 因为显示用的resources来源于application, e.g.
     *   applicationContext.getSystemService(DisplayManager.class).getDisplay(displayId)
     * and the deprecated usage:
     *   applicationContext.getSystemService(WindowManager.class).getDefaultDisplay();
     *
     * @param token The owner and target of the override.
     * @param 用于override的displayadjustment.如果是null就删除token对应的override,然后pop出最后一个
     */
    private void overrideApplicationDisplayAdjustments(@NonNull IBinder token,
            @Nullable Consumer<DisplayAdjustments> override) {
        final Consumer<DisplayAdjustments> appOverride;
        if (mActiveRotationAdjustments == null) {
            mActiveRotationAdjustments = new ArrayList<>(2);
        }
        if (override != null) {
            mActiveRotationAdjustments.add(Pair.create(token, override));
            appOverride = override;
        } else {
            mActiveRotationAdjustments.removeIf(adjustmentsPair -> adjustmentsPair.first == token);
            appOverride = mActiveRotationAdjustments.isEmpty()
                    ? null
                    : mActiveRotationAdjustments.get(mActiveRotationAdjustments.size() - 1).second;
        }
        mInitialApplication.getResources().overrideDisplayAdjustments(appOverride);
    }

ResourcesManager#applyConfigurationToResources

ActivityThread将在ViewRootImpl的configChangedCallback中调用applyConfigurationToResources应用adjustments,目前adjustment生效的唯一场景就是模拟将应用程序放置在旋转显示中。 后面在app绘制流程时将使用调整过的configuration进行绘制。

    /** Applies the global configuration to the managed resources. */
    public final boolean applyConfigurationToResources(@NonNull Configuration config,
            @Nullable CompatibilityInfo compat, @Nullable DisplayAdjustments adjustments) {
        ...
        DisplayMetrics displayMetrics = getDisplayMetrics();
        if (adjustments != null) {
            // 目前adjustment生效的唯一场景就是模拟将应用程序放置在旋转显示中。
            adjustments.adjustGlobalAppMetrics(displayMetrics);
        }
        Resources.updateSystemConfiguration(config, displayMetrics, compat);
        ... 
    }

至此,FixedRotation的基本原理和流程注解已经梳理完成。