该系列文章总纲链接:专题总纲目录 Android Framework 总纲
本章关键点总结 & 说明:
该图关注➕思维导图中左边:窗口尺寸&确定 部分即可。首先说明了overscan区域和窗口尺寸的数据结构;之后说明了计算尺寸的流程和计算得到的结果。放大 窗口尺寸&确定 的局部图,效果如下:
说明:虽然可以在创建窗口时指定窗口大小,但更多时候窗口大小是由系统计算得到的,本章节主要分析窗口计算基础和流程。
1 OverScan区域与表示窗口尺寸的数据结构
@1 overScan是边缘区域(四周有一圈黑色的区域),该区域是显示屏的一部分,但通常不显示画面,在PhoneWindowManager中有4个变量与此有关,代码如下:
public class PhoneWindowManager implements WindowManagerPolicy {
...
//分表表示overscan区域上下左右的宽度、高度值
int mOverscanLeft = 0;
int mOverscanTop = 0;
int mOverscanRight = 0;
int mOverscanBottom = 0;
...
}
初始化是在WMS配置显示设备时完成的,代码如下:
private void configureDisplayPolicyLocked(DisplayContent displayContent) {
mPolicy.setInitialDisplaySize(displayContent.getDisplay(),
displayContent.mBaseDisplayWidth,
displayContent.mBaseDisplayHeight,
displayContent.mBaseDisplayDensity);
DisplayInfo displayInfo = displayContent.getDisplayInfo();
mPolicy.setDisplayOverscan(displayContent.getDisplay(),
displayInfo.overscanLeft, displayInfo.overscanTop,
displayInfo.overscanRight, displayInfo.overscanBottom);
}
这里mPolicy,即PhoneWindowManager中setDisplayOverscan的实现(得以完成初始化),代码如下:
public void setDisplayOverscan(Display display, int left, int top, int right, int bottom) {
if (display.getDisplayId() == Display.DEFAULT_DISPLAY) {//仅支持缺省设备
mOverscanLeft = left;
mOverscanTop = top;
mOverscanRight = right;
mOverscanBottom = bottom;
}
}
@2 表示窗口尺寸的数据结构
public class PhoneWindowManager implements WindowManagerPolicy {
...
//第1组,真实屏幕大小
int mOverscanScreenLeft, mOverscanScreenTop;
int mOverscanScreenWidth, mOverscanScreenHeight;
//第2组,Unrestricted区域,不包含overscan区域,包含导航条
int mUnrestrictedScreenLeft, mUnrestrictedScreenTop;
int mUnrestrictedScreenWidth, mUnrestrictedScreenHeight;
//第3组,RestrictedOverscan区域,包含overscan区域,不包含导航条
int mRestrictedOverscanScreenLeft, mRestrictedOverscanScreenTop;
int mRestrictedOverscanScreenWidth, mRestrictedOverscanScreenHeight;
//第4组,Restricted区域,不包含overscan区域,不包含导航条
int mRestrictedScreenLeft, mRestrictedScreenTop;
int mRestrictedScreenWidth, mRestrictedScreenHeight;
//第5组,System区域
int mSystemLeft, mSystemTop, mSystemRight, mSystemBottom;
//第6组,Stable区域
int mStableLeft, mStableTop, mStableRight, mStableBottom;
//第7组,StableFullscreen区域
int mStableFullscreenLeft, mStableFullscreenTop;
int mStableFullscreenRight, mStableFullscreenBottom;
//第8组,Current区域,即除去装饰区域(状态栏,输入法,导航条)以外的中心显示区域
int mCurLeft, mCurTop, mCurRight, mCurBottom;
//第9组,Content区域
int mContentLeft, mContentTop, mContentRight, mContentBottom;
//第10组,Decor区域
int mDockLeft, mDockTop, mDockRight, mDockBottom;
...
}
屏幕有overscan区域、状态栏、导航栏、输入法,PhoneWindowManager定义了这些区域代表了屏幕上不同的组合,如图所示:
将成员变量与区域对应,如下所示:
- overscanScreen区域,包含了overscan区域,相当于整个屏幕大小
- RestrictedOverscanScreen区域,不包含导航栏
- RestrictedScreen区域,不包含屏幕上的overscan区域,不包含导航条
- UnrestrictedScreen区域,不包含overscan区域,包含状态条和导航栏
- StableFullScreen区域,包含状态栏,不包含导航栏
- Decor区域,不包含状态栏,不包含导航栏,包含输入法的区域
- Current区域,不包含状态栏,不包含导航栏,不包含输入法的区域
特殊说明:
- 对于System区域、Stable区域、Content区域的范围和Decor区域的范围相同,用在不同的场合含义不同,但值是相同的
- 以后屏幕插入更多的区域,这些屏幕的区域可能发生变化(为以后的可维护性做准备)
@3 初始化窗口尺寸(即这些区域的值)
在PhoneWindowManager类中,是使用方法beginLayoutLw来初始化这些区域的值的,代码如下:
public void beginLayoutLw(boolean isDefaultDisplay, int displayWidth, int displayHeight,
int displayRotation) {
final int overscanLeft, overscanTop, overscanRight, overscanBottom;
...//这段代码根据屏幕方向调整4个overscan区域
//根据各个区域是否延伸到屏幕的overscan区域来对各个区域进行初始化
mOverscanScreenLeft = mRestrictedOverscanScreenLeft = 0;
mOverscanScreenTop = mRestrictedOverscanScreenTop = 0;
mOverscanScreenWidth = mRestrictedOverscanScreenWidth = displayWidth;
mOverscanScreenHeight = mRestrictedOverscanScreenHeight = displayHeight;
...
if (isDefaultDisplay) {
...
if (mNavigationBar != null) {//计算导航栏尺寸并调整相关区域的值
...
mNavigationBar.computeFrameLw(mTmpNavigationFrame, mTmpNavigationFrame,
mTmpNavigationFrame, mTmpNavigationFrame, mTmpNavigationFrame, dcf,
mTmpNavigationFrame);
...//之后根据导航栏尺寸调整其他区域尺寸
}
if (mStatusBar != null) {//计算状态条尺寸并调整相关区域的值
...
mStatusBar.computeFrameLw(pf, df, vf, vf, vf, dcf, vf);
...//之后根据状态条尺寸调整其他区域尺寸
}
if (updateSysUiVisibility) {
updateSystemUiVisibilityLw();
}
}
}
经过beginLayoutLw方法的调整,各个布局参数就准备好了。
2 计算窗口尺寸
WMS中计算窗口的过程中很繁琐,不仅仅局限在计算窗口的大小。
@1 下图表示了从performTraversals开始分析一个Activity窗口大小的计算过程,如下所示:
@2 这里仅对针对窗口计算部分进行分析,所以只分析layoutWindowLw。代码如下:
public void layoutWindowLw(WindowState win, WindowState attached) {
final WindowManager.LayoutParams attrs = win.getAttrs();
if ((win == mStatusBar && (attrs.privateFlags & PRIVATE_FLAG_KEYGUARD) == 0) ||
win == mNavigationBar) {
return;
}
...
final Rect pf = mTmpParentFrame;
final Rect df = mTmpDisplayFrame;
final Rect of = mTmpOverscanFrame;
final Rect cf = mTmpContentFrame;
final Rect vf = mTmpVisibleFrame;
final Rect dcf = mTmpDecorFrame;
final Rect sf = mTmpStableFrame;
dcf.setEmpty();
if (!isDefaultDisplay) {
...
} else if (attrs.type == TYPE_INPUT_METHOD) {
...
} else if (win == mStatusBar && (attrs.privateFlags & PRIVATE_FLAG_KEYGUARD) != 0) {
...
} else {
...
if ((fl & (FLAG_LAYOUT_IN_SCREEN | FLAG_LAYOUT_INSET_DECOR))
== (FLAG_LAYOUT_IN_SCREEN | FLAG_LAYOUT_INSET_DECOR)) {
if (attached != null) {
setAttachedWindowFrames(win, fl, adjust, attached, true, pf, df, of, cf, vf);
} else {
...
}
} else if ((fl & FLAG_LAYOUT_IN_SCREEN) != 0 || (sysUiFl
& (View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION)) != 0) {
...
} else if (attached != null) {
...
} else {
...
}
}
...
win.computeFrameLw(pf, df, of, cf, vf, dcf, sf);//最关键的地方
if (attrs.type == TYPE_INPUT_METHOD && !win.getGivenInsetsPendingLw()) {
setLastInputMethodWindowLw(null, null);
offsetInputMethodWindowLw(win);
}
if (attrs.type == TYPE_VOICE_INTERACTION && win.isVisibleOrBehindKeyguardLw()
&& !win.getGivenInsetsPendingLw()) {
offsetVoiceInputWindowLw(win);
}
}
layoutWindowLw的主要内容如下:
- 先计算出窗口能够扩展的最大空间,即6个矩形区域的大小,分别是:
- pf(ParentFrame):本窗口的父窗口大小
- df(DeviceFrame):设备的屏幕大小
- of(OverscanFrame):设备的屏幕大小,=df
- cf(ContentFrame):窗口内容区域大小
- vf(VisibleFrame):可见区域大小
- dcf(DecorFrame):装饰区域大小,除去状态栏和导航栏
- 得到6个区域的值(关键参数)后,调用WS的computeFrameLw计算窗口的最终大小
注意:这些只是临时值,未计算窗口大小服务,与之前的数据结构有关联,但并不完全等同于那些值
特殊说明:窗口大小还要考虑软键盘的影响,如果属性中带有SOFT_ADJUST_RESIZE,表示窗口大小会随着软键盘调整
@3 分析关键点
@@3.1 回顾WS中定义了一些和窗口尺寸相关的矩形变量,如下所示:
final class WindowState implements WindowManagerPolicy.WindowState {
static final String TAG = "WindowState";
...
final Rect mFrame = new Rect();//真实窗口大小
final Rect mCompatFrame = new Rect();//兼容窗口大小
final Rect mParentFrame = new Rect();//父类窗口大小
final Rect mDisplayFrame = new Rect();//显示设备大小
final Rect mOverscanFrame = new Rect();//overscan区域大小
final Rect mDecorFrame = new Rect(); //装饰区域大小
final Rect mContentFrame = new Rect();//内容区域大小
final Rect mVisibleFrame = new Rect();//可见区域大小
...
}
@@3.2 接下来WS的方法win.computeFrameLw(pf, df, of, cf, vf, dcf, sf)会用到这些变量,代码如下:
public void computeFrameLw(Rect pf, Rect df, Rect of, Rect cf, Rect vf, Rect dcf, Rect sf) {
mHaveFrame = true;
TaskStack stack = mAppToken != null ? getStack() : null;
if (stack != null && !stack.isFullscreen() && !isFloatingWindow()) {
getStackBounds(stack, mContainingFrame);
if (mUnderStatusBar) {
mContainingFrame.top = pf.top;
}
} else {
mContainingFrame.set(pf);
}
mDisplayFrame.set(df);
final int pw = mContainingFrame.width();
final int ph = mContainingFrame.height();
int w,h;//计算窗口的高度、宽度
if ((mAttrs.flags & WindowManager.LayoutParams.FLAG_SCALED) != 0) {
//如果指定了FLAG_SCALED标志,说明窗口中属性已经指定了大小,直接使用attr的宽度和高度
if (mAttrs.width < 0) {
w = pw;
} else if (mEnforceSizeCompat) {
w = (int)(mAttrs.width * mGlobalScale + .5f);
} else {
w = mAttrs.width;
}
if (mAttrs.height < 0) {
h = ph;
} else if (mEnforceSizeCompat) {
h = (int)(mAttrs.height * mGlobalScale + .5f);
} else {
h = mAttrs.height;
}
} else {
//如果指定了MATCH_PARENT标志,说明窗口大小==父类窗口大小,否则指定mRequestedWidth或mRequestedHeight
if (mAttrs.width == WindowManager.LayoutParams.MATCH_PARENT) {
w = pw;
} else if (mEnforceSizeCompat) {
w = (int)(mRequestedWidth * mGlobalScale + .5f);
} else {
w = mRequestedWidth;
}
if (mAttrs.height == WindowManager.LayoutParams.MATCH_PARENT) {
h = ph;
} else if (mEnforceSizeCompat) {
h = (int)(mRequestedHeight * mGlobalScale + .5f);
} else {
h = mRequestedHeight;
}
}
//使用参数设置下面的变量
if (!mParentFrame.equals(pf)) {
mParentFrame.set(pf);
mContentChanged = true;
}
if (mRequestedWidth != mLastRequestedWidth || mRequestedHeight != mLastRequestedHeight) {
mLastRequestedWidth = mRequestedWidth;
mLastRequestedHeight = mRequestedHeight;
mContentChanged = true;
}
//采用直接赋值的方法
mOverscanFrame.set(of);
mContentFrame.set(cf);
mVisibleFrame.set(vf);
mDecorFrame.set(dcf);
mStableFrame.set(sf);
final int fw = mFrame.width();
final int fh = mFrame.height();
float x, y;
//对于兼容模式,Android采用的策略就是把窗口进行缩放,使之充满屏幕。
if (mEnforceSizeCompat) {//如果初始化窗口时带有PRIVATE_FLAG_COMPATIBLE_WINDOW,则该值为true
x = mAttrs.x * mGlobalScale;
y = mAttrs.y * mGlobalScale;
} else {
x = mAttrs.x;
y = mAttrs.y;
}
//考虑Gravity属性对窗口大小的影响
//根据窗口属性中的对齐方式以及各种标志位来重新调整窗口大小
Gravity.apply(mAttrs.gravity, w, h, mContainingFrame,
(int) (x + mAttrs.horizontalMargin * pw),
(int) (y + mAttrs.verticalMargin * ph), mFrame);
//将计算出来的窗口大小限制在显示设备的大小范围内
Gravity.applyDisplay(mAttrs.gravity, df, mFrame);
mContentFrame.set(Math.max(mContentFrame.left, mFrame.left),
Math.max(mContentFrame.top, mFrame.top),
Math.min(mContentFrame.right, mFrame.right),
Math.min(mContentFrame.bottom, mFrame.bottom));
...
//兼容模式的窗口大小==mFrame的大小进行mInvGlobalScale倍的缩放
mCompatFrame.set(mFrame);
if (mEnforceSizeCompat) {
mOverscanInsets.scale(mInvGlobalScale);
mContentInsets.scale(mInvGlobalScale);
mVisibleInsets.scale(mInvGlobalScale);
mStableInsets.scale(mInvGlobalScale);
mCompatFrame.scale(mInvGlobalScale);
}
...
}
computeFrameLw对窗口布局的产出主要是以下几个变量,如下所示:
- mFrame:描述窗口的位置和尺寸
- mContainingFrame和mParentFrame:这两个矩形相同,保存了pf参数
- mDisplayFrame:保存了df参数
- mContentFrame和mContentInsets:mContentFrame表示当前窗口中显示内容的区域,mContentInsets表示mContentFrame与mFrame的4条边界之间的距离
- mVisibleFrame和mVisibleInsets:mVisibleFrame表示当前不被系统遮挡的区域,mVisibleInsets表示mVisibleFrame与mFrame的4条边界之间的距离
至此,单个窗口的布局计算完成,布局后,窗口的位置尺寸、内容区域的位置尺寸、可视区域的位置尺寸都得到了更新,这些更新会影响到窗口surface的位置尺寸,还会以回调方式通知窗口客户端,影响窗口内容的绘制。