Android Framework--转屏原理

来源:互联网 发布:周哥k101淘宝店 编辑:程序博客网 时间:2024/06/01 15:58

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

转屏的流程非常简单,如下:
这里写图片描述

下面将分为三个阶段进行讨论,本文贴出的代码来源于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.javapublic 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.javapublic 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.javapublic 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.javaprivate 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.javavoid 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.javaprivate 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.javavoid 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());        }        ...    }    ...}

最后,附上动画过程演示图:
转屏动画演示图


原创粉丝点击