在Android系统中,Activity窗口的大小是由WindowManagerService服务来计算的。WindowManagerService服务会根据屏幕及其装饰区的大小来决定Activity窗口的大小。一个Activity窗口只有知道自己的大小之后,才能对它里面的UI元素进行测量、布局以及绘制。
一般来说,Activity窗口的大小等于整个屏幕的大小,但是它并不占据着整块屏幕。为了理解这一点,我们首先分析一下Activity窗口的区域是如何划分的。
我们知道,Activity窗口的上方一般会有一个状态栏,用来显示3G信号、电量使用等图标,如图所示:
从Activity窗口剔除掉状态栏所占用的区域之后,所得到的区域就称为内容区域(Content Region)。顾名思义,内容区域就是用来显示Activity窗口的内容的。我们再抽象一下,假设Activity窗口的四周都有一块类似状态栏的区域,那么将这些区域剔除之后,得到中间的那一块区域就称为内容区域,而被剔除出来的区域所组成的区域就称为内容边衬区域(Content Insets)。Activity窗口的内容边衬区域可以用一个四元组(content-left, content-top, content-right, content-bottom)来描述,其中,content-left、content-right、content-top、content-bottom分别用来描述内容区域与窗口区域的左右上下边界距离。
我们还知道,Activity窗口有时候需要显示输入法窗口,如图所示:
这时候Activity窗口的内容区域的大小有可能没有发生变化,这取决于它的Soft Input Mode。我们假设Activity窗口的内容区域没有发生变化,但是它在底部的一些区域被输入法窗口遮挡了,即它在底部的一些内容是不可见的。从Activity窗口剔除掉状态栏和输入法窗口所占用的区域之后,所得到的区域就称为可见区域(Visible Region)。
同样,我们再抽象一下,假设Activity窗口的四周都有一块类似状态栏和输入法窗口的区域,那么将这些区域剔除之后,得到中间的那一块区域就称为可见区域,而被剔除出来的区域所组成的区域就称为可见边衬区域(Visible Insets)。Activity窗口的可见边衬区域可以用一个四元组(visible-left, visible-top, visible-right, visible-bottom)来描述,其中,visible-left、visible-right、visible-top、visible-bottom分别用来描述可见区域与窗口区域的左右上下边界距离。
在大多数情况下,Activity窗口的内容区域和可见区域的大小是一致的,而状态栏和输入法窗口所占用的区域又称为屏幕装饰区。理解了这些概念之后,我们就可以推断,WindowManagerService服务实际上就是需要根据屏幕以及可能出现的状态栏和输入法窗口的大小来计算出Activity窗口的整体大小及其内容区域边衬和可见区域边衬的大小。有了这三个数据之后,Activity窗口就可以对它里面的UI元素进行测量、布局以及绘制等操作了。
应用程序进程是从ViewRoot类的成员函数performTraversals开始,向WindowManagerService服务请求计算一个Activity窗口的大小的,因此,接下来我们就从ViewRoot类的成员函数performTraversals开始分析一个Activity窗口大小的计算过程,如图所示:
Step 1. ViewRoot.performTraversals
public final class ViewRoot extends Handler implements ViewParent,
View.AttachInfo.Callbacks {
......
private void performTraversals() {
......
final View host = mView;
......
int desiredWindowWidth;
int desiredWindowHeight;
int childWidthMeasureSpec;
int childHeightMeasureSpec;
......
Rect frame = mWinFrame;
if (mFirst) {
......
DisplayMetrics packageMetrics =
mView.getContext().getResources().getDisplayMetrics();
desiredWindowWidth = packageMetrics.widthPixels;
desiredWindowHeight = packageMetrics.heightPixels;
} else {
desiredWindowWidth = frame.width();
desiredWindowHeight = frame.height();
if (desiredWindowWidth != mWidth || desiredWindowHeight != mHeight) {
......
windowResizesToFitContent = true;
}
}
这段代码用来获得Activity窗口的当前宽度desiredWindowWidth和当前高度desiredWindowHeight。
注意,Activity窗口当前的宽度和高度是保存ViewRoot类的成员变量mWinFrame中的。ViewRoot类的另外两个成员变量mWidth和mHeight也是用来描述Activity窗口当前的宽度和高度的,但是它们的值是由应用程序进程上一次主动请求WindowManagerService服务计算得到的,并且会一直保持不变到应用程序进程下一次再请求WindowManagerService服务来重新计算为止。Activity窗口的当前宽度和高度有时候是被WindowManagerService服务主动请求应用程序进程修改的,修改后的值就会保存在ViewRoot类的成员变量mWinFrame中,它们可能会与ViewRoot类的成员变量mWidth和mHeight的值不同。
如果Activity窗口是第一次被请求执行测量、布局和绘制操作,即ViewRoot类的成员变量mFirst的值等于true,那么它的当前宽度desiredWindowWidth和当前高度desiredWindowHeight就等于屏幕的宽度和高度,否则的话,它的当前宽度desiredWindowWidth和当前高度desiredWindowHeight就等于保存在ViewRoot类的成员变量mWinFrame中的宽度和高度值。
如果Activity窗口不是第一次被请求执行测量、布局和绘制操作,并且Activity窗口主动上一次请求WindowManagerService服务计算得到的宽度mWidth和高度mHeight不等于Activity窗口的当前宽度desiredWindowWidth和当前高度desiredWindowHeight,那么就说明Activity窗口的大小发生了变化,这时候变量windowResizesToFitContent的值就会被标记为true,以便接下来可以对Activity窗口的大小变化进行处理。
boolean insetsChanged = false;
if (mLayoutRequested) {
......
if (mFirst) {
host.fitSystemWindows(mAttachInfo.mContentInsets);
......
} else {
if (!mAttachInfo.mContentInsets.equals(mPendingContentInsets)) {
mAttachInfo.mContentInsets.set(mPendingContentInsets);
host.fitSystemWindows(mAttachInfo.mContentInsets);
insetsChanged = true;
......
}
if (!mAttachInfo.mVisibleInsets.equals(mPendingVisibleInsets)) {
mAttachInfo.mVisibleInsets.set(mPendingVisibleInsets);
......
}
if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT
|| lp.height == ViewGroup.LayoutParams.WRAP_CONTENT) {
windowResizesToFitContent = true;
DisplayMetrics packageMetrics =
mView.getContext().getResources().getDisplayMetrics();
desiredWindowWidth = packageMetrics.widthPixels;
desiredWindowHeight = packageMetrics.heightPixels;
}
}
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
......
host.measure(childWidthMeasureSpec, childHeightMeasureSpec);
......
}
这段代码用来在Activity窗口主动请求WindowManagerService服务计算大小之前,对它的顶层视图进行一次测量操作。
ViewRoot类的成员变量mAttachInfo指向的一个AttachInfo对象,这个AttachInfo对象用来描述Activity窗口的属性,例如,这个AttachInfo对象的成员变量mContentInsets和mVisibleInsets分别用来描述Activity窗口上一次主动请求WindowManagerService服务计算得到的内容边衬大小和可见边衬大小,即Activity窗口的当前内容边衬大小和可见边衬大小。
ViewRoot类的成员变量mPendingContentInsets和mPendingVisibleInsets也是用来描述Activity窗口的内容边衬大小和可见边衬大小的,不过它们是由WindowManagerService服务主动请求Activity窗口设置的,但是尚未生效。
我们分两种情况来分析这段代码。
第一种情况是Activity窗口是第一次被请求执行测量、布局和绘制操作,即ViewRoot类的成员变量mFirst的值等于true,那么这段代码在测量Activity窗口的顶层视图host的大小之前,首先会调用这个顶层视图host的成员函数fitSystemWindows来设置它的四个内边距(mPaddingLeft,mPaddingTop,mPaddingRight,mPaddingBottom)的大小设置为Activity窗口的初始化内容边衬大小。这样做的目的是可以在Activity窗口的四周留下足够的区域来放置可能会出现的系统窗口,也就是状态栏和输入法窗口。
第二种情况是Activity窗口不是第一次被请求执行测量、布局和绘制操作,即ViewRoot类的成员变量mFirst的值等于false,那么这段代码就会检查Activity窗口是否被WindowManagerService服务主动请求设置了一个新的内容边衬大小mPendingContentInsets和一个新的可见边衬大小mPendingVisibleInsets。如果是的话,那么就会分别将它们保存在ViewRoot类的成员变量mAttachInfo所指向的一个AttachInfo对象的成员变量mContentInsets和成员变量mVisibleInsets中。
注意,如果Activity窗口被WindowManagerService服务主动请求设置了一个新的内容边衬大小mPendingContentInsets,那么这段代码同时还需要同步调用Activity窗口的顶层视图host的成员函数fitSystemWindows来将它的四个内边距(mPaddingLeft,mPaddingTop,mPaddingRight,mPaddingBottom)的大小设置为新的内容边衬大小,并且将变量insetsChanged的值设置为true,表明Activity窗口的内容边衬大小发生了变化。
在第二种情况下,如果Activity窗口的宽度被设置为ViewGroup.LayoutParams.WRAP_CONTENT或者高度被设置为ViewGroup.LayoutParams.WRAP_CONTENT,那么就意味着Activity窗口的大小要等于内容区域的大小。但是由于Activity窗口的大小是需要覆盖整个屏幕的,因此,这时候就会Activity窗口的当前宽度desiredWindowWidth和当前高度desiredWindowHeight设置为屏幕的宽度和高度。也就是说,如果我们将Activity窗口的宽度和高度设置为ViewGroup.LayoutParams.WRAP_CONTENT,实际上就意味着它的宽度和高度等于屏幕的宽度和高度。这种情况也意味着Acitivity窗口的大小发生了变化,因此,就将变量windowResizesToFitContent的值设置为true。
经过上面的一系列处理之后,这段代码就会调用ViewRoot类的成员函数getRootMeasureSpec来根据Activity窗口的当前宽度和宽度测量规范以及高度和高度测量规范来计算得到它的顶层视图host的宽度测量规范childWidthMeasureSpec和高度测量规范childHeightMeasureSpec。有了这两个规范之后,就可以调用Activity窗口的顶层视图host的成员函数measure来执行大小测量的工作了。
boolean windowShouldResize = mLayoutRequested && windowResizesToFitContent
&& ((mWidth != host.mMeasuredWidth || mHeight != host.mMeasuredHeight)
|| (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT &&
frame.width() < desiredWindowWidth && frame.width() != mWidth)
|| (lp.height == ViewGroup.LayoutParams.WRAP_CONTENT &&
frame.height() < desiredWindowHeight && frame.height() != mHeight));
final boolean computesInternalInsets =
attachInfo.mTreeObserver.hasComputeInternalInsetsListeners();
这段代码主要是做两件事情。
第一件事情是检查是否需要处理Activity窗口的大小变化事件。如果满足以下条件,那么就需要处理,即将变量windowShouldResize的值设置为true:
- ViewRoot类的成员变量mLayoutRequest的值等于true,这说明应用程序进程正在请求对Activity窗口执行一次测量、布局和绘制操作;
- 变量windowResizesToFitContent的值等于true,这说明前面检测到了Activity窗口的大小发生了变化;
- 前面我们已经Activity窗口的顶层视图host的大小重新进行了测量。如果测量出来的宽度host.mMeasuredWidth和高度
host.mMeasuredHeight和Activity窗口的当前宽度mWidth和高度mHeight一样,那么即使条件1和条件2能满足,那么也是可以认为是Activity窗口的大小是没有发生变化的。换句话说,只有当测量出来的大小和当前大小不一致时,才认为Activity窗口大小发生了变化。另一方面,如果测量出来的大小和当前大小一致,但是Activity窗口的大小被要求设置成WRAP_CONTENT,即设置成和屏幕的宽度desiredWindowWidth和高度desiredWindowHeight一致,但是WindowManagerService服务请求Activity窗口设置的宽度frame.width()和高度frame.height()与它们不一致,而且与Activity窗口上一次请求WindowManagerService服务计算的宽度mWidth和高度mHeight也不一致,那么也是认为Activity窗口大小发生了变化的。
第二件事情是检查Activity窗口是否需要指定有额外的内容边衬区域和可见边衬区域。如果有的话,那么变量attachInfo所指向的一个AttachInfo对象的成员变量mTreeObserver所描述的一个TreeObserver对象的成员函数hasComputeInternalInsetsListerner的返回值ComputeInternalInsets就会等于true。Activity窗口指定额外的内容边衬区域和可见边衬区域是为了放置一些额外的东西。
if (mFirst || windowShouldResize || insetsChanged
|| viewVisibilityChanged || params != null) {
if (viewVisibility == View.VISIBLE) {
// If this window is giving internal insets to the window
// manager, and it is being added or changing its visibility,
// then we want to first give the window manager "fake"
// insets to cause it to effectively ignore the content of
// the window during layout. This avoids it briefly causing
// other windows to resize/move based on the raw frame of the
// window, waiting until we can finish laying out this window
// and get back to the window manager with the ultimately
// computed insets.
insetsPending = computesInternalInsets
&& (mFirst || viewVisibilityChanged);
......
}
这段代码以及接下来的两段代码都是在满足下面的条件之一的情况下执行的:
- Activity窗口是第一次执行测量、布局和绘制操作,即ViewRoot类的成员变量mFirst的值等于true。
- 前面得到的变量windowShouldResize的值等于true,即Activity窗口的大小的确是发生了变化。
- 前面得到的变量insetsChanged的值等于true,即Activity窗口的内容区域边衬发生了变化。
- Activity窗口的可见性发生了变化,即变量viewVisibilityChanged的值等于true。
- Activity窗口的属性发生了变化,即变量params指向了一个WindowManager.LayoutParams对象。
在满足上述条件之一,并且Activity窗口处于可见状态,即变量viewVisibility的值等于View.VISIBLE,那么就需要检查接下来请求WindowManagerService服务计算大小时,是否要告诉WindowManagerService服务它指定了额外的内容区域边衬和可见区域边衬,但是这些额外的内容区域边衬和可见区域边衬又还不确定。
这种情况发生在Activity窗口第一次执行测量、布局和绘制操作或者由不可见变化可见时。因此,当前面得到的变量computesInternalInsets等于true时,即Activity窗口指定了额外的内容区域边衬和可见区域边衬,那么就需要检查ViewRoot类的成员变量mFirst或者变量viewVisibilityChanged的值是否等于true。如果这些条件能满足,那么变量insetsPending的值就会等于true,表示Activity窗口有额外的内容区域边衬和可见区域边衬等待指定。
boolean contentInsetsChanged = false;
boolean visibleInsetsChanged;
......
try {
......
relayoutResult = relayoutWindow(params, viewVisibility, insetsPending);
contentInsetsChanged = !mPendingContentInsets.equals(
mAttachInfo.mContentInsets);
visibleInsetsChanged = !mPendingVisibleInsets.equals(
mAttachInfo.mVisibleInsets);
if (contentInsetsChanged) {
mAttachInfo.mContentInsets.set(mPendingContentInsets);
host.fitSystemWindows(mAttachInfo.mContentInsets);
......
}
if (visibleInsetsChanged) {
mAttachInfo.mVisibleInsets.set(mPendingVisibleInsets);
......
}
......
} catch (RemoteException e) {
}
......
attachInfo.mWindowLeft = frame.left;
attachInfo.mWindowTop = frame.top;
// !!FIXME!! This next section handles the case where we did not get the
// window size we asked for. We should avoid this by getting a maximum size from
// the window session beforehand.
mWidth = frame.width();
mHeight = frame.height();
这段代码主要就是调用ViewRoot类的另外一个成员函数relayoutWindow来请求WindowManagerService服务计算Activity窗口的大小以及内容区域边衬大小和可见区域边衬大小。计算完毕之后,Activity窗口的大小就会保存在ViewRoot类的成员变量mWinFrame中,而Activity窗口的内容区域边衬大小和可见区域边衬大小分别保存在ViewRoot类的成员变量mPendingContentInsets和mPendingVisibleInsets中。
如果这次计算得到的Activity窗口的内容区域边衬大小mPendingContentInsets和可见区域边衬大小mPendingVisibleInsets与上一次计算得到的不一致,即与ViewRoot类的成员变量mAttachInfo所指向的一个AttachInfo对象的成员变量mContentInsets和mVisibleInsets所描述的大小不一致,那么变量contentInsetsChanged和visibleInsetsChanged的值就会等于true,表示Activity窗口的内容区域边衬大小和可见区域边衬大小发生了变化。
由于变量frame和ViewRoot类的成员变量mWinFrame引用的是同一个Rect对象,因此,这时候变量frame描述的也是Activity窗口请求WindowManagerService服务计算之后得到的大小。这段代码分别将计算得到的Activity窗口的左上角坐标保存在变量attachInfo所指向的一个AttachInfo对象的成员变量mWindowLeft和mWindowTop中,并且将计算得到的Activity窗口的宽度和高度保存在ViewRoot类的成员变量mWidth和mHeight中。
boolean focusChangedDueToTouchMode = ensureTouchModeLocally(
(relayoutResult&WindowManagerImpl.RELAYOUT_IN_TOUCH_MODE) != 0);
if (focusChangedDueToTouchMode || mWidth != host.mMeasuredWidth
|| mHeight != host.mMeasuredHeight || contentInsetsChanged) {
childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
.....