1. 分屏窗口尺寸计算
1.1 窗口添加到WMS
Activity首次启动之后,在其resume阶段会将自己的Window添加到WMS:
void makeVisible() {
if (!mWindowAdded) {
ViewManager wm = getWindowManager();
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE);
}
ViewRootImpl.setView
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
mView = view;
.......
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(), mTmpFrame,
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel,
mTempInsets);
setFrame(mTmpFrame);
......
}
}
}
窗口通过addToDisplay 添加到WMS之后,WMS会粗略计算当前窗口的尺寸
WMS.addWindow
public int addWindow(Session session, IWindow client, int seq,
LayoutParams attrs, int viewVisibility, int displayId, Rect outFrame,
Rect outContentInsets, Rect outStableInsets, Rect outOutsets,
DisplayCutout.ParcelableWrapper outDisplayCutout, InputChannel outInputChannel,
InsetsState outInsetsState) {
......
final Rect taskBounds;
final boolean floatingStack;
if (atoken != null && atoken.getTask() != null) {
taskBounds = mTmpRect;
atoken.getTask().getBounds(mTmpRect);
floatingStack = atoken.getTask().isFloating();
} else {
taskBounds = null;
floatingStack = false;
}
if (displayPolicy.getLayoutHintLw(win.mAttrs, taskBounds, displayFrames, floatingStack,
outFrame, outContentInsets, outStableInsets, outOutsets, outDisplayCutout)) {
res |= WindowManagerGlobal.ADD_FLAG_ALWAYS_CONSUME_SYSTEM_BARS;
}
......
}
以上同样只关注和窗口尺寸相关的代码,上述最重要的点是获取taskBounds ,这个值会获取Activity启动阶段调用setBounds 设置的值,对于分屏窗口来说当分屏窗口启动之后会调用resizeDockedStack 设置分屏栈边界,这个方法最终调用的就是setBounds 方法设置边界的值,WMS此处就是获取分屏窗口自己设置的值,接着在看getLayoutHintLw 方法之前,先来看看DisplayFrames 中的几个边界Rect :
public final Rect mOverscan = new Rect();
public final Rect mCurrent = new Rect();
public final Rect mUnrestricted = new Rect();
public final Rect mRestricted = new Rect();
public final Rect mStable = new Rect();
public final Rect mStableFullscreen = new Rect();
上述的几个边界值在窗口计算中非常重要,它们的值是在DisplayPolicy.beginLayoutLw 中完成初始化的,Android窗口尺寸计算非常依赖这四个边界值,屏幕区域,状态栏区域,导航栏区域,输入法区域。
有了上述几个尺寸接下来看DisplayPolicy.getLayoutHintLw :
public boolean getLayoutHintLw(LayoutParams attrs, Rect taskBounds,
DisplayFrames displayFrames, boolean floatingStack, Rect outFrame,
Rect outContentInsets, Rect outStableInsets,
Rect outOutsets, DisplayCutout.ParcelableWrapper outDisplayCutout) {
final int fl = PolicyControl.getWindowFlags(null, attrs);
final int pfl = attrs.privateFlags;
final int requestedSysUiVis = PolicyControl.getSystemUiVisibility(null, attrs);
final int sysUiVis = requestedSysUiVis | getImpliedSysUiFlagsForLayout(attrs);
final int displayRotation = displayFrames.mRotation;
final boolean useOutsets = outOutsets != null && shouldUseOutsets(attrs, fl);
if (useOutsets) {
int outset = mWindowOutsetBottom;
if (outset > 0) {
if (displayRotation == Surface.ROTATION_0) {
outOutsets.bottom += outset;
} else if (displayRotation == Surface.ROTATION_90) {
outOutsets.right += outset;
} else if (displayRotation == Surface.ROTATION_180) {
outOutsets.top += outset;
} else if (displayRotation == Surface.ROTATION_270) {
outOutsets.left += outset;
}
}
}
final boolean layoutInScreen = (fl & FLAG_LAYOUT_IN_SCREEN) != 0;
final boolean layoutInScreenAndInsetDecor = layoutInScreen
&& (fl & FLAG_LAYOUT_INSET_DECOR) != 0;
final boolean screenDecor = (pfl & PRIVATE_FLAG_IS_SCREEN_DECOR) != 0;
if (layoutInScreenAndInsetDecor && !screenDecor) {
if ((sysUiVis & SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION) != 0) {
outFrame.set(displayFrames.mUnrestricted);
} else {
outFrame.set(displayFrames.mRestricted);
}
final Rect sf;
if (floatingStack) {
sf = null;
} else {
sf = displayFrames.mStable;
}
final Rect cf;
if (floatingStack) {
cf = null;
} else if ((sysUiVis & View.SYSTEM_UI_FLAG_LAYOUT_STABLE) != 0) {
if ((fl & FLAG_FULLSCREEN) != 0) {
cf = displayFrames.mStableFullscreen;
} else {
cf = displayFrames.mStable;
}
} else if ((fl & FLAG_FULLSCREEN) != 0 || (fl & FLAG_LAYOUT_IN_OVERSCAN) != 0) {
cf = displayFrames.mOverscan;
} else {
cf = displayFrames.mCurrent;
}
if (taskBounds != null) {
outFrame.intersect(taskBounds);
}
InsetUtils.insetsBetweenFrames(outFrame, cf, outContentInsets);
InsetUtils.insetsBetweenFrames(outFrame, sf, outStableInsets);
outDisplayCutout.set(displayFrames.mDisplayCutout.calculateRelativeTo(outFrame)
.getDisplayCutout());
return mForceShowSystemBars;
} else {
if (layoutInScreen) {
outFrame.set(displayFrames.mUnrestricted);
} else {
outFrame.set(displayFrames.mStable);
}
if (taskBounds != null) {
outFrame.intersect(taskBounds);
}
outContentInsets.setEmpty();
outStableInsets.setEmpty();
outDisplayCutout.set(DisplayCutout.NO_CUTOUT);
return mForceShowSystemBars;
}
}
上述代码的核心其实就是DisplayFrames 中的几个边界Rect 和Activity类型窗口的栈边界,无非就是根据窗口添加的flag选用不同的Rect ,然后做一下Rect 的加减,
outFrame 就是最终WMS计算得到的Activity的窗口尺寸,上述getLayoutHintLw 方法计算的到的尺寸并非最终的尺寸,这里的尺寸更多可以理解为父窗口尺寸,例如如果添加的是一个Activity中的Dialog,这里outFrame 得到的就是Dialog所属的Activity所在栈的边界值而并非Dialog自己的尺寸,后续将outFrame 返回给应用进程之后Dialog拿到这个尺寸进行measure ,此时才能得到Dialog的所要求的尺寸,但这任然可能不是最终尺寸,真正尺寸计算会在后续的WMS.relayout中进行。
这个值粗略计算得到的值会返回给Activity所在应用进程:
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
mView = view;
.......
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(), mTmpFrame,
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel,
mTempInsets);
setFrame(mTmpFrame);
......
}
}
}
setFrame 将WMS计算得到的Activity窗口尺寸保存在了ViewRootImpl 的成员变量mWinFrame 中:
private void setFrame(Rect frame) {
mWinFrame.set(frame);
mInsetsController.onFrameChanged(frame);
}
有了Activity窗口尺寸,接下来Activity要进行自己的测量工作了:
1.2 performTraversals
在添加窗口到WMS成功之后会执行View的三大流程,onMeasure ,onLayout ,onDraw ,这三大流程在ViewRootImpl 的performTraversals 中执行,这个方法非常复杂,这里重点关注和窗口尺寸计算相关的代码:
private void performTraversals() {
final View host = mView;
......
WindowManager.LayoutParams lp = mWindowAttributes;
int desiredWindowWidth;
int desiredWindowHeight;
......
Rect frame = mWinFrame;
if (mFirst) {
mFullRedrawNeeded = true;
mLayoutRequested = true;
final Configuration config = mContext.getResources().getConfiguration();
if (shouldUseDisplaySize(lp)) {
Point size = new Point();
mDisplay.getRealSize(size);
desiredWindowWidth = size.x;
desiredWindowHeight = size.y;
} else {
desiredWindowWidth = mWinFrame.width();
desiredWindowHeight = mWinFrame.height();
}
......
dispatchApplyInsets(host);
} else {
desiredWindowWidth = frame.width();
desiredWindowHeight = frame.height();
if (desiredWindowWidth != mWidth || desiredWindowHeight != mHeight) {
mFullRedrawNeeded = true;
mLayoutRequested = true;
windowSizeMayChange = true;
}
}
...
windowSizeMayChange |= measureHierarchy(host, lp, res,
desiredWindowWidth, desiredWindowHeight);
......
if (mFirst || windowShouldResize || insetsChanged ||
viewVisibilityChanged || params != null || mForceNextWindowRelayout) {
......
relayoutResult = relayoutWindow(params, viewVisibility, insetsPending);
......
}
.....
}
上面代码关于尺寸计算的代码中最重要的代码就是relayoutWindow ,此方法用于计算窗口尺寸,insetsPending 代表是否指定额外的边衬区域,默认为false:
relayoutWindow
private int relayoutWindow(WindowManager.LayoutParams params, int viewVisibility,
boolean insetsPending) throws RemoteException {
float appScale = mAttachInfo.mApplicationScale;
......
int relayoutResult = mWindowSession.relayout(mWindow, mSeq, params,
(int) (mView.getMeasuredWidth() * appScale + 0.5f),
(int) (mView.getMeasuredHeight() * appScale + 0.5f), viewVisibility,
insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0, frameNumber,
mTmpFrame, mPendingOverscanInsets, mPendingContentInsets, mPendingVisibleInsets,
mPendingStableInsets, mPendingOutsets, mPendingBackDropFrame, mPendingDisplayCutout,
mPendingMergedConfiguration, mSurfaceControl, mTempInsets);
if (mSurfaceControl.isValid()) {
mSurface.copyFrom(mSurfaceControl);
} else {
destroySurface();
}
......
setFrame(mTmpFrame);
mInsetsController.onStateChanged(mTempInsets);
return relayoutResult;
}
1.3 WMS.relayoutWindow
public int relayoutWindow(Session session, IWindow client, int seq, LayoutParams attrs,
int requestedWidth, int requestedHeight, int viewVisibility, int flags,
long frameNumber, Rect outFrame, Rect outOverscanInsets, Rect outContentInsets,
Rect outVisibleInsets, Rect outStableInsets, Rect outOutsets, Rect outBackdropFrame,
DisplayCutout.ParcelableWrapper outCutout, MergedConfiguration mergedConfiguration,
SurfaceControl outSurfaceControl, InsetsState outInsetsState) {
......
if (viewVisibility != View.GONE) {
win.setRequestedSize(requestedWidth, requestedHeight);
}
......
mWindowPlacerLocked.performSurfacePlacement(true );
......
win.getCompatFrame(outFrame);
......
}
WMS.relayoutWindow 将Activity窗口经过测量后得到的自己的想要的宽高保存在了其对应的WindowState 中, mWindowPlacerLocked.performSurfacePlacement 最终回调到WindowState 的computeFrameLw 方法去计算窗口的尺寸:
1.4 WindowState.computeFrameLw
每个WindowState 内部都有一个类WindowFrames ,这个类中提供了众多Rect 来描述不同的区域边界,这些Rect 的值有的来自DisplayFrames 中,有的来自computeFrameLw 的计算,DisplayFrames 中的Rect 描述的是窗口的基础边界,例如,屏幕宽高,状态栏,导航栏宽高,而一个窗口的真正尺寸还是需要自己经过计算得到,例如有的窗口没有状态栏或者导航栏,有的窗口有输入法等,这些情况都是要实际计算时才能知道。
先看WindowFrames 中的窗口基础边界如何赋值的,Android系统中任意一个窗口的添加都会触发系统中所有窗口尺寸重新计算,每个窗口都会调用DisplayPolicy 的layoutWindowLw 方法,
public void layoutWindowLw(WindowState win, WindowState attached, DisplayFrames displayFrames) {
......
final WindowFrames windowFrames = win.getWindowFrames();
final Rect pf = windowFrames.mParentFrame;
final Rect df = windowFrames.mDisplayFrame;
final Rect of = windowFrames.mOverscanFrame;
final Rect cf = windowFrames.mContentFrame;
final Rect vf = windowFrames.mVisibleFrame;
final Rect dcf = windowFrames.mDecorFrame;
final Rect sf = windowFrames.mStableFrame;
......
.....
win.computeFrameLw();
.....
}
上述赋值过程省略了,给一张最后赋值完成的图:
关注重点是窗口自己尺寸的计算:
@Override
public void computeFrameLw() {
if (mWillReplaceWindow && (mAnimatingExit || !mReplacingRemoveRequested)) {
return;
}
mHaveFrame = true;
final Task task = getTask();
final boolean isFullscreenAndFillsDisplay = !inMultiWindowMode() && matchesDisplayBounds();
final boolean windowsAreFloating = task != null && task.isFloating();
final DisplayContent dc = getDisplayContent();
mInsetFrame.set(getBounds());
final Rect layoutContainingFrame;
final Rect layoutDisplayFrame;
final int layoutXDiff;
final int layoutYDiff;
final WindowState imeWin = mWmService.mRoot.getCurrentInputMethodWindow();
final boolean isImeTarget =
imeWin != null && imeWin.isVisibleNow() && isInputMethodTarget();
if (isFullscreenAndFillsDisplay || layoutInParentFrame()) {
......
} else {
mWindowFrames.mContainingFrame.set(getDisplayedBounds());
if (mAppToken != null && !mAppToken.mFrozenBounds.isEmpty()) {
......
}
if (isImeTarget) {
......
}
if (windowsAreFloating) {
......
}
final TaskStack stack = getStack();
if (inPinnedWindowingMode() && stack != null
&& stack.lastAnimatingBoundsWasToFullscreen()) {
......
}
layoutDisplayFrame = new Rect(mWindowFrames.mDisplayFrame);
mWindowFrames.mDisplayFrame.set(mWindowFrames.mContainingFrame);
layoutXDiff = mInsetFrame.left - mWindowFrames.mContainingFrame.left;
layoutYDiff = mInsetFrame.top - mWindowFrames.mContainingFrame.top;
layoutContainingFrame = mInsetFrame;
mTmpRect.set(0, 0, dc.getDisplayInfo().logicalWidth, dc.getDisplayInfo().logicalHeight);
subtractInsets(mWindowFrames.mDisplayFrame, layoutContainingFrame, layoutDisplayFrame,
mTmpRect);
if (!layoutInParentFrame()) {
subtractInsets(mWindowFrames.mContainingFrame, layoutContainingFrame,
mWindowFrames.mParentFrame, mTmpRect);
subtractInsets(mInsetFrame, layoutContainingFrame, mWindowFrames.mParentFrame,
mTmpRect);
}
layoutDisplayFrame.intersect(layoutContainingFrame);
}
final int pw = mWindowFrames.mContainingFrame.width();
final int ph = mWindowFrames.mContainingFrame.height();
if (mRequestedWidth != mLastRequestedWidth || mRequestedHeight != mLastRequestedHeight) {
mLastRequestedWidth = mRequestedWidth;
mLastRequestedHeight = mRequestedHeight;
mWindowFrames.setContentChanged(true);
}
final int fw = mWindowFrames.mFrame.width();
final int fh = mWindowFrames.mFrame.height();
applyGravityAndUpdateFrame(layoutContainingFrame, layoutDisplayFrame);
.....
if (windowsAreFloating && !mWindowFrames.mFrame.isEmpty()) {
......
} else if (mAttrs.type == TYPE_DOCK_DIVIDER) {
......
} else {
mWindowFrames.mContentFrame.set(
Math.max(mWindowFrames.mContentFrame.left, mWindowFrames.mFrame.left),
Math.max(mWindowFrames.mContentFrame.top, mWindowFrames.mFrame.top),
Math.min(mWindowFrames.mContentFrame.right, mWindowFrames.mFrame.right),
Math.min(mWindowFrames.mContentFrame.bottom, mWindowFrames.mFrame.bottom));
mWindowFrames.mVisibleFrame.set(
Math.max(mWindowFrames.mVisibleFrame.left, mWindowFrames.mFrame.left),
Math.max(mWindowFrames.mVisibleFrame.top, mWindowFrames.mFrame.top),
Math.min(mWindowFrames.mVisibleFrame.right, mWindowFrames.mFrame.right),
Math.min(mWindowFrames.mVisibleFrame.bottom, mWindowFrames.mFrame.bottom));
mWindowFrames.mStableFrame.set(
Math.max(mWindowFrames.mStableFrame.left, mWindowFrames.mFrame.left),
Math.max(mWindowFrames.mStableFrame.top, mWindowFrames.mFrame.top),
Math.min(mWindowFrames.mStableFrame.right, mWindowFrames.mFrame.right),
Math.min(mWindowFrames.mStableFrame.bottom, mWindowFrames.mFrame.bottom));
}
if (isFullscreenAndFillsDisplay && !windowsAreFloating) {
......
}
if (mAttrs.type == TYPE_DOCK_DIVIDER) {
......
} else {
getDisplayContent().getBounds(mTmpRect);
mWindowFrames.calculateInsets(
windowsAreFloating, isFullscreenAndFillsDisplay, mTmpRect);
}
......
mWindowFrames.mCompatFrame.set(mWindowFrames.mFrame);
......
if (mIsWallpaper && (fw != mWindowFrames.mFrame.width()
|| fh != mWindowFrames.mFrame.height())) {
......
}
......
}
computeFrameLw 方法的核心是计算出mWindowFrames.mFrame 的值,这个值就是窗口的实际尺寸,计算方法applyGravityAndUpdateFrame .
1.5 mFrame计算
对于分屏窗口,这里接收的两个参数相等都是分屏栈的大小 Rect(400, 57 - 800, 396)
private void applyGravityAndUpdateFrame(Rect containingFrame, Rect displayFrame) {
final int pw = containingFrame.width();
final int ph = containingFrame.height();
final Task task = getTask();
final boolean inNonFullscreenContainer = !inAppWindowThatMatchesParentBounds();
final boolean noLimits = (mAttrs.flags & FLAG_LAYOUT_NO_LIMITS) != 0;
final boolean fitToDisplay = (task == null || !inNonFullscreenContainer)
|| ((mAttrs.type != TYPE_BASE_APPLICATION) && !noLimits);
float x, y;
int w,h;
final boolean inSizeCompatMode = inSizeCompatMode();
if ((mAttrs.flags & FLAG_SCALED) != 0) {
.......
} else {
if (mAttrs.width == MATCH_PARENT) {
w = pw;
} else if (inSizeCompatMode) {
w = (int)(mRequestedWidth * mGlobalScale + .5f);
} else {
w = mRequestedWidth;
}
if (mAttrs.height == MATCH_PARENT) {
h = ph;
} else if (inSizeCompatMode) {
h = (int)(mRequestedHeight * mGlobalScale + .5f);
} else {
h = mRequestedHeight;
}
}
if (inSizeCompatMode) {
x = mAttrs.x * mGlobalScale;
y = mAttrs.y * mGlobalScale;
} else {
x = mAttrs.x;
y = mAttrs.y;
}
if (inNonFullscreenContainer && !layoutInParentFrame()) {
w = Math.min(w, pw);
h = Math.min(h, ph);
}
Gravity.apply(mAttrs.gravity, w, h, containingFrame,
(int) (x + mAttrs.horizontalMargin * pw),
(int) (y + mAttrs.verticalMargin * ph), mWindowFrames.mFrame);
if (fitToDisplay) {
Gravity.applyDisplay(mAttrs.gravity, displayFrame, mWindowFrames.mFrame);
}
mWindowFrames.mCompatFrame.set(mWindowFrames.mFrame);
if (inSizeCompatMode) {
mWindowFrames.mCompatFrame.scale(mInvGlobalScale);
}
}
窗口的尺寸计算到此就完成了,最终的结果是保存在mFrame 中,最后这个值会返回给APP进程,APP进程ViewRootImpl 中调用的relayoutWindow 方法主要目的就是请求WMS对窗口进行计算得到mFrame 的值,最后APP将此值保存在了ViewRootImpl 的成员变量mWinFrame 中。
总结,窗口尺寸的计算依赖应用进程和WMS协同,应用进程首次添加到WMS时,WMS返回一个不保证正确的尺寸值(这个尺寸对于Activity窗口来说一般为其栈的边界,非Activity窗口一般为屏幕尺寸),应用进程用这个尺寸对自己的根View进行测量,测量完成之后通过WMS.relayout 对应用请求的尺寸进行再次计算,因为WMS这边要考虑屏幕尺寸,状态栏,导航栏等,所以WMS这一步计算是保证应用请求的尺寸是合理的。
|