本文带你从framework的视角了解转屏从产生到结束这一过程,应用开发中转屏相关的知识点已经有很多现成的资料,不在这里的讨论范围。
下面将分为三个阶段进行讨论,本文贴出的代码来源于Android N。
框架利用一定的策略来确定当前的屏幕方向,依据主要是窗口的screenOrientation,以及其它的一些状态,比如系统是否开启了屏幕旋转、系统是否固定了屏幕方向、是否处于dock模式等。
在讲如何确定屏幕方向前,先介绍窗口的screenOrientation。对于一个窗口,根据是否Activity窗口,以不同的方式来声明screenOrientation:
/**
* 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);
}
}
/**
* 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(由下方的窗口指定方向)。
现在可以开始讲屏幕方向的确定过程,主要是两个步骤:
从上到下遍历窗口堆栈,在可见的窗口中确定一个基准screenOrientation
1)非Activity窗口的screenOrientation如果不是上述的两个值,则结束遍历
2)一旦遍历到一个Activity窗口,接下来仅在Activity窗口中遍历,不再关心非Activity窗口
根据第一步算出的基准screenOrientation,按照一定策略计算出最终屏幕方向,有兴趣的可以参考源码
frameworks\base\services\core\java\com\android\server\policy\PhoneWindowManager.java
public int rotationForOrientationLw(int orientation, int lastRotation);
根据第一点可以知道,一个比较高层级的非Activity窗口,是可以通过配置screenOrientation来让自己成为基准screenOrientation的确定者,进而影响最终的屏幕方向。
那么转屏的原因很简单,即系统在某一时刻计算出的屏幕方向发生了变化,常见的场景如下:
关键点在于基准screenOrientation的确定,越顶层的可见窗口越有机会决定基准screenOrientation。
好,现在系统检测到要改变屏幕方向,接下来屏幕上能看到的所有窗口都要以新的尺寸来重新绘制,大家想一下会遇到什么问题。由于各个窗口的重绘不可能在同一时刻完成,也不可能同时刷新,如果我们什么都不做,在屏幕上将会看到各种闪烁。
怎么办?
以竖屏切换到横屏为例,涉及到两个状态的切换,如果我们把竖屏的界面保存下来,再把横屏时应该显示的界面通通准备好,然后加入动画在两个状态间平滑的过渡,问题就可以迎刃而解。这个从概念上有点类似于framebuffer中的双缓冲区,待back buffer渲染完成后,再推到前台显示。
我们来看一下这个方案会遇到什么问题:
看下Android是如何解决的:
截屏的代码,关键位置加了注释:
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());
}
...
}
...
}