Android Framework--转屏原理

本文带你从framework的视角了解转屏从产生到结束这一过程,应用开发中转屏相关的知识点已经有很多现成的资料,不在这里的讨论范围。

转屏的流程非常简单,如下:
Android Framework--转屏原理_第1张图片

下面将分为三个阶段进行讨论,本文贴出的代码来源于Android N。


转屏的产生

框架利用一定的策略来确定当前的屏幕方向,依据主要是窗口的screenOrientation,以及其它的一些状态,比如系统是否开启了屏幕旋转、系统是否固定了屏幕方向、是否处于dock模式等。

在讲如何确定屏幕方向前,先介绍窗口的screenOrientation。对于一个窗口,根据是否Activity窗口,以不同的方式来声明screenOrientation:

  • Activity窗口:
    1、(静态)AndroidManifest.xml中配置screenOrientation
    2、(动态)通过Activity.setRequestedOrientation()
/**
 * Change the desired orientation of this activity.  If the activity
 * is currently in the foreground or otherwise impacting the screen
 * orientation, the screen will immediately be changed (possibly causing
 * the activity to be restarted). Otherwise, this will be used the next
 * time the activity is visible.
 *
 * @param requestedOrientation An orientation constant as used in
 * {@link ActivityInfo#screenOrientation ActivityInfo.screenOrientation}.
 */
public void setRequestedOrientation(@ActivityInfo.ScreenOrientation int requestedOrientation) {
    if (mParent == null) {
        try {
            ActivityManagerNative.getDefault().setRequestedOrientation(
                    mToken, requestedOrientation);
        } catch (RemoteException e) {
            // Empty
        }
    } else {
        mParent.setRequestedOrientation(requestedOrientation);
    }
}
  • 非Activity窗口:指定WindowManager.LayoutParams.screenOrientation
/**
 * Specific orientation value for a window.
 * May be any of the same values allowed
 * for {@link android.content.pm.ActivityInfo#screenOrientation}.
 * If not set, a default value of
 * {@link android.content.pm.ActivityInfo#SCREEN_ORIENTATION_UNSPECIFIED}
 * will be used.
 */
public int screenOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;

窗口的screenOrientation默认为ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED(不指定方向),另外还有个特殊的值为ActivityInfo.SCREEN_ORIENTATION_BEHIND(由下方的窗口指定方向)。

现在可以开始讲屏幕方向的确定过程,主要是两个步骤:

  1. 从上到下遍历窗口堆栈,在可见的窗口中确定一个基准screenOrientation
    1)非Activity窗口的screenOrientation如果不是上述的两个值,则结束遍历
    2)一旦遍历到一个Activity窗口,接下来仅在Activity窗口中遍历,不再关心非Activity窗口

  2. 根据第一步算出的基准screenOrientation,按照一定策略计算出最终屏幕方向,有兴趣的可以参考源码

frameworks\base\services\core\java\com\android\server\policy\PhoneWindowManager.java

public int rotationForOrientationLw(int orientation, int lastRotation);

根据第一点可以知道,一个比较高层级的非Activity窗口,是可以通过配置screenOrientation来让自己成为基准screenOrientation的确定者,进而影响最终的屏幕方向。

那么转屏的原因很简单,即系统在某一时刻计算出的屏幕方向发生了变化,常见的场景如下:

  • 横竖屏界面的切换,比如在Launcher上启动了个横屏应用,或者从横屏应用返回Launcher
  • 当前应用未指定方向,然后手动旋转设备方向

关键点在于基准screenOrientation的确定,越顶层的可见窗口越有机会决定基准screenOrientation。


转屏的准备

好,现在系统检测到要改变屏幕方向,接下来屏幕上能看到的所有窗口都要以新的尺寸来重新绘制,大家想一下会遇到什么问题。由于各个窗口的重绘不可能在同一时刻完成,也不可能同时刷新,如果我们什么都不做,在屏幕上将会看到各种闪烁。

怎么办?

以竖屏切换到横屏为例,涉及到两个状态的切换,如果我们把竖屏的界面保存下来,再把横屏时应该显示的界面通通准备好,然后加入动画在两个状态间平滑的过渡,问题就可以迎刃而解。这个从概念上有点类似于framebuffer中的双缓冲区,待back buffer渲染完成后,再推到前台显示。

我们来看一下这个方案会遇到什么问题:

  1. 竖屏的状态以什么方式保存?
  2. 横屏界面在绘制时怎么不被用户察觉?
  3. 怎么确定横屏状态所需的界面?
  4. 过渡的动画怎么设计?

看下Android是如何解决的:

  1. 截屏来保存竖屏状态
  2. 用第一步的截屏生成一个非常高层级的,完全不透明的Layer,让用户在视觉上一直停留在竖屏状态,由于是完全不透明,所以在它下方的所有内容完全不可见,那么就尽情地绘制吧
  3. 方向改变前哪些窗口可见,就必须等待那些窗口绘制完成,这个很好理解
  4. 截屏Layer为整体,施加旋转并淡出的动画,重绘的所有横屏界面为另一个整体,也施加一个旋转动画,这样当动画结束时看到的就是重绘后的横屏界面。还有一点需要注意,转屏过程中系统会禁用所有其它动画,避免动画的叠加。

截屏的代码,关键位置加了注释:

frameworks\base\services\core\java\com\android\server\wm\WindowManagerService.java

public ScreenRotationAnimation(Context context, DisplayContent displayContent,
        SurfaceSession session, boolean inTransaction, boolean forceDefaultOrientation,
        boolean isSecure) {
    ...

    try {
        try {
            int flags = SurfaceControl.HIDDEN;
            if (isSecure) {
                flags |= SurfaceControl.SECURE;
            }

            if (DEBUG_SURFACE_TRACE) {
                mSurfaceControl = new SurfaceTrace(session, "ScreenshotSurface",
                        mWidth, mHeight,
                        PixelFormat.OPAQUE, flags);
                Slog.w(TAG, "ScreenRotationAnimation ctor: displayOffset="
                        + mOriginalDisplayRect.toShortString());
            } else {
                mSurfaceControl = new SurfaceControl(session, "ScreenshotSurface",
                        mWidth, mHeight,
                        PixelFormat.OPAQUE, flags); // 新建一个OPAQUE的Layer,即完全不透明
            }
            // capture a screenshot into the surface we just created
            Surface sur = new Surface();
            sur.copyFrom(mSurfaceControl);
            // FIXME: we should use the proper display
            SurfaceControl.screenshot(SurfaceControl.getBuiltInDisplay(
                    SurfaceControl.BUILT_IN_DISPLAY_ID_MAIN), sur); // 调用screenshot截屏,截屏内容将作为这个新建Layer的内容
            mSurfaceControl.setLayerStack(display.getLayerStack());
            mSurfaceControl.setLayer(SCREEN_FREEZE_LAYER_SCREENSHOT); // 设置层级为2010001,基本没有比这更高的了,这样可以保证盖住所有界面
            mSurfaceControl.setAlpha(0); // 先设置Alpha为0
            mSurfaceControl.show();
            sur.destroy();
        } catch (OutOfResourcesException e) {
            Slog.w(TAG, "Unable to allocate freeze surface", e);
        }

        if (SHOW_TRANSACTIONS || SHOW_SURFACE_ALLOC) Slog.i(TAG_WM,
                "  FREEZE " + mSurfaceControl + ": CREATE");

        setRotationInTransaction(originalRotation);
    } finally {
        if (!inTransaction) {
            SurfaceControl.closeTransaction();
            if (SHOW_LIGHT_TRANSACTIONS) Slog.i(TAG_WM,
                    "<<< CLOSE TRANSACTION ScreenRotationAnimation");
        }
    }
}

注意,截完屏后,系统的配置就已经由竖屏变更为横屏,这样才能使得原来竖屏的那些窗口重绘。

再看下需要等待哪些窗口重绘完成:

frameworks\base\services\core\java\com\android\server\wm\WindowManagerService.java

public boolean updateRotationUncheckedLocked(boolean inTransaction) {
    ...
    final WindowList windows = displayContent.getWindowList();
    for (int i = windows.size() - 1; i >= 0; i--) {
        WindowState w = windows.get(i);
        // Discard surface after orientation change, these can't be reused.
        if (w.mAppToken != null) {
            w.mAppToken.destroySavedSurfaces();
        }
        if (w.mHasSurface) { // 标记有Surface的窗口
            if (DEBUG_ORIENTATION) Slog.v(TAG_WM, "Set mOrientationChanging of " + w);
            w.mOrientationChanging = true; // mOrientationChanging作为重绘是否完成的标记
            mWindowPlacerLocked.mOrientationChangeComplete = false; // mOrientationChangeComplete作为是否全部重绘完成的标记
        }
        w.mLastFreezeDuration = 0;
    }
    ...
}

注意这里的mWindowPlacerLocked.mOrientationChangeComplete,为true时表示已经全部准备完成,前提是所有可见窗口的mOrientationChanging都为false。大家看代码时可以以此为切入点。

最后再看下转屏动画的生成:

private boolean startAnimation(SurfaceSession session, long maxAnimationDuration,
        float animationScale, int finalWidth, int finalHeight, boolean dismissing,
        int exitAnim, int enterAnim) {
    ...
    final boolean customAnim;
    if (exitAnim != 0 && enterAnim != 0) {
        customAnim = true;
        mRotateExitAnimation = AnimationUtils.loadAnimation(mContext, exitAnim);
        mRotateEnterAnimation = AnimationUtils.loadAnimation(mContext, enterAnim);
    } else {
        customAnim = false;
        switch (delta) {
            case Surface.ROTATION_0:
                mRotateExitAnimation = AnimationUtils.loadAnimation(mContext,
                        com.android.internal.R.anim.screen_rotate_0_exit);
                mRotateEnterAnimation = AnimationUtils.loadAnimation(mContext,
                        com.android.internal.R.anim.screen_rotate_0_enter);
                if (USE_CUSTOM_BLACK_FRAME) {
                    mRotateFrameAnimation = AnimationUtils.loadAnimation(mContext,
                            com.android.internal.R.anim.screen_rotate_0_frame);
                }
                break;
            case Surface.ROTATION_90:
                mRotateExitAnimation = AnimationUtils.loadAnimation(mContext,
                        com.android.internal.R.anim.screen_rotate_plus_90_exit);
                mRotateEnterAnimation = AnimationUtils.loadAnimation(mContext,
                        com.android.internal.R.anim.screen_rotate_plus_90_enter);
                if (USE_CUSTOM_BLACK_FRAME) {
                    mRotateFrameAnimation = AnimationUtils.loadAnimation(mContext,
                            com.android.internal.R.anim.screen_rotate_plus_90_frame);
                }
                break;
            case Surface.ROTATION_180:
                mRotateExitAnimation = AnimationUtils.loadAnimation(mContext,
                        com.android.internal.R.anim.screen_rotate_180_exit);
                mRotateEnterAnimation = AnimationUtils.loadAnimation(mContext,
                        com.android.internal.R.anim.screen_rotate_180_enter);
                if (USE_CUSTOM_BLACK_FRAME) {
                    mRotateFrameAnimation = AnimationUtils.loadAnimation(mContext,
                            com.android.internal.R.anim.screen_rotate_180_frame);
                }
                break;
            case Surface.ROTATION_270:
                mRotateExitAnimation = AnimationUtils.loadAnimation(mContext,
                        com.android.internal.R.anim.screen_rotate_minus_90_exit);
                mRotateEnterAnimation = AnimationUtils.loadAnimation(mContext,
                        com.android.internal.R.anim.screen_rotate_minus_90_enter);
                if (USE_CUSTOM_BLACK_FRAME) {
                    mRotateFrameAnimation = AnimationUtils.loadAnimation(mContext,
                            com.android.internal.R.anim.screen_rotate_minus_90_frame);
                }
                break;
        }
    }
    ...
}

mRotateExitAnimation即施加给截屏Layer的动画,而mRotateEnterAnimation则施加到参与转屏的其它窗口上,大家可以看下代码中相关的动画资源以确认动画的最终效果。从这里也可以看到,动画可以自行定制。


转屏动画的释放

准备过程完成后,即mWindowPlacerLocked.mOrientationChangeComplete为true,就意味着转屏动画的释放,代码如下:

frameworks\base\services\core\java\com\android\server\wm\WindowSurfacePlacer.java

private void performSurfacePlacementInner(boolean recoveringMemory) {
    ...
    if (mOrientationChangeComplete) {
        if (mService.mWindowsFreezingScreen != WINDOWS_FREEZING_SCREENS_NONE) {
            mService.mWindowsFreezingScreen = WINDOWS_FREEZING_SCREENS_NONE;
            mService.mLastFinishedFreezeSource = mLastWindowFreezeSource;
            mService.mH.removeMessages(WINDOW_FREEZE_TIMEOUT);
        }
        // 开启转屏动画
        mService.stopFreezingDisplayLocked();
    }
    ...
}
frameworks\base\services\core\java\com\android\server\wm\WindowManagerService.java

void stopFreezingDisplayLocked() {
    ...
    if (screenRotationAnimation.dismiss(mFxSession, MAX_ANIMATION_DURATION,
            getTransitionAnimationScaleLocked(), displayInfo.logicalWidth,
                displayInfo.logicalHeight, mExitAnimId, mEnterAnimId)) {
        // 调用screenRotationAnimation.dismiss()来最终释放转屏动画
        scheduleAnimationLocked();
    } else {
        screenRotationAnimation.kill();
        mAnimator.setScreenRotationAnimationLocked(displayId, null);
        updateRotation = true;
    }
    ...
}

到此为止就会看到界面开始旋转,还有最后一个问题,上面说的两个动画是在哪里施加的?

说到动画的步进,第一反应就要想到下面的方法,先看截屏Layer的动画:

frameworks\base\services\core\java\com\android\server\wm\WindowAnimator.java

private void animateLocked(long frameTimeNs) {
    if (screenRotationAnimation != null && screenRotationAnimation.isAnimating()) {
        if (screenRotationAnimation.stepAnimationLocked(mCurrentTime)) { // 调用该方法步进截屏Layer的转屏动画,即mRotateExitAnimation
            setAnimating(true);
        } else {
            mBulkUpdateParams |= SET_UPDATE_ROTATION;
            screenRotationAnimation.kill();
            displayAnimator.mScreenRotationAnimation = null;

            //TODO (multidisplay): Accessibility supported only for the default display.
            if (mService.mAccessibilityController != null
                    && displayId == Display.DEFAULT_DISPLAY) {
                // We just finished rotation animation which means we did not
                // anounce the rotation and waited for it to end, announce now.
                mService.mAccessibilityController.onRotationChangedLocked(
                        mService.getDefaultDisplayContentLocked(), mService.mRotation);
            }
        }
    }
}

再看mRotateEnterAnimation,即参与转屏的其它窗口的动画:

frameworks\base\services\core\java\com\android\server\wm\WindowStateAnimator.java

void computeShownFrameLocked() {
    ...
    final int displayId = mWin.getDisplayId();
    final ScreenRotationAnimation screenRotationAnimation =
            mAnimator.getScreenRotationAnimationLocked(displayId);
    final boolean screenAnimation =
            screenRotationAnimation != null && screenRotationAnimation.isAnimating();
    ...
    if (selfTransformation || attachedTransformation != null
            || appTransformation != null || screenAnimation) {
        ...
        if (screenAnimation) {
            // 在这里
            tmpMatrix.postConcat(screenRotationAnimation.getEnterTransformation().getMatrix());
        }
        ...
    }
    ...
}

最后,附上动画过程演示图:
Android Framework--转屏原理_第2张图片


你可能感兴趣的:(Android Framework--转屏原理)