Android无缝旋转:Fixed Rotation

Android无缝旋转:Fixed Rotation

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 时序图

Android无缝旋转:Fixed Rotation

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时序图

Android无缝旋转:Fixed Rotation

核算模仿旋转信息并传递给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的基本原理和流程注解现已梳理完结。