本篇文章源码基于android 6.0.1,鉴于源码繁重且作者水平有限,如有不当之处欢迎指正。
上一篇文章浅谈Activity从建立到显示(setContentView源码浅析)
中我们遗留了一个非常重要的问题,就是在整个Activity显示出来之前需要经过View的绘制,我们接着上一篇文章来看。
首先,由于剧情的需要,我们先来介绍一下Activity中Window的几个区域:
Overscan(过扫描区):
overscan是电视机特有的概念,上图中黄色的区域就是overscan区域,指的是电视机四周一圈的黑色区域,成为overscan(过扫描)区域,这个区域也是显示屏的一部分,但通常不能显示。如果窗口的某些内容画在这个区域里,它在某些电视上就会看不到。为了避免这种情况发生,通常要求UI不要画在屏幕的边角上,而是预留一定的空间。因为Overscan的区域大小随着电视不 同而不同,它一般由终端用户通过UI指定,(比如说GoogleTV里就有确定Overscan大小的应用)(更多关于Overscan的内容涉及到早起电视机的显示缺陷问题,对这方法面比较感兴趣的读者可以自行搜索)。
OverscanScreen, Screen:
OverscanScreen是包含Overscan区域的屏幕大小,而Screen则为去除Overscan区域后的屏幕区域, OverscanScreen > Screen.
Restricted and Unrestricted:
某些区域是被系统保留的,比如说手机屏幕上方的状态栏(如图纸绿色区域)和下方的导航栏,根据是否包括这些预留的区域,Android把区域分为Unrestricted Area 和 Resctrited Aread, 前者包括这部分预留区域,后者则不包含, Unrestricted area > Rectricted area。
RestrictedOverScanScreen:
包括overscan区,不包含导航栏、因此这个区域上面到屏幕的顶部,下面就到导航栏的顶部。
RestrictedScreen:
这个区域不包含overscan区域不包含导航栏。
UnRestrictedScreen:
不包含屏幕的overscan区域、包含状态栏和导航栏。
stableFullScreen:
包含状态栏、输入法、不包含导航栏。
Decor区域:
不包含状态栏、不包含导航栏、包含输入法区域。
Curren:
不包含状态栏、不包含导航栏、不包含输入法区域。
Insects:
insets“边衬”的定义如上图所示,其本质就是一块屏幕区域的边衬。对于图二中的示意,我们做一解释:
假如content区域周围有一圈类似于状态栏的东西,此时整个界面就是content内容区+一圈状态栏,那么这一圈状态栏就叫做"内容区边衬(content insects)";同理我们还可以得到”可见区边衬(visible insects)“。
在大多数情况下,Activity窗口的内容区域和可见区域的大小是一致的,而状态栏和输入法窗口所占用的区域又称为屏幕装饰区(decorations)。WMS服务实际上就是需要根据屏幕以及可能出现的状态栏和输入法窗口的大小来计算出Activity窗口的整体大小及其过扫描区域边衬和可见区域边衬的大小。
PhoneWindowManager一共定义了10种区域,剩下的system区域、Stable区域、Content区域的范围和Decor区域相同。这些区域在不同场合含义不同,但是值是相同的。
需要注意的是,overscan区域和智能手机屏幕上的"大黑边"不是一会事,前者是软件层面的问题,后者是硬件层面问题,这里不做过多解释。
理解了上述的区域划分之后,我们正式开始本文的分析。相信不少读者一看到标题“View绘制流程及源码解析”立马就想到我们进场谈及的View绘制三大流程——measure(),Layout(),Draw()。诚然这三大流程是重中之重,网上很多的博客文章都有分析,也是我们第二篇文章中要重点分析的对象,此篇文章中我们重点来分析一下performTraversals()方法,因为这个方法是三大流程的开端,它其中也做了很多的事情,但网上资料却相对较少。
下面我们就来分析一下ViewRootImpl类中的performTraversals()方法,这个巨长的方法一共有800行代码,这里我们尽量省略了一些不相关的代码(frameworks/base/core/android/view/ViewRootImpl):
第一段代码:
private void performTraversals() {
// cache mView since it is used so much below...
final View host = mView; //mView就是DecorView根布局,记录ViewRootImpl管理的View树的根节点,final修饰,避免运行过程中修改
if (host == null || !mAdded) //判断View绘制的条件是否具备.mAdded是DecorView是否成功添加到
return; //Window的标示位,如果一切顺利的话,这里应该为ture,因此不会return.
mIsInTraversal = true; //是否正在遍历
mWillDrawSoon = true; //是否马上绘制View
boolean windowSizeMayChange = false; //视图的大小可能改变
boolean newSurface = false; //新界面
boolean surfaceChanged = false; //界面改变
WindowManager.LayoutParams lp = mWindowAttributes;
int desiredWindowWidth; //顶层视图DecorView所需要窗口的宽度和高度
int desiredWindowHeight;
final int viewVisibility = getHostVisibility(); //DecorView视图是否可见
boolean viewVisibilityChanged = mViewVisibility != viewVisibility
|| mNewSurfaceNeeded; //视图可见性改变
......
Rect frame = mWinFrame; //将一个全局的Rect对象赋给了局部frame对象,它用来保存Activity窗口当前的宽度和高度
if (mFirst) { //在构造方法中mFirst已经设置为true,表示是否是第一次被请求执行测量、布局和绘制操作,
mFullRedrawNeeded = true; //是否需要全部重绘
mLayoutRequested = true; //是否要求重新Layout界面
//如果窗口的类型是有状态栏的,那么Activity窗口的宽度和高度就是除了状态栏
if (lp.type == WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL
|| lp.type == WindowManager.LayoutParams.TYPE_INPUT_METHOD) {
// NOTE -- system code, won't try to do compat mode.
Point size = new Point();
mDisplay.getRealSize(size);
desiredWindowWidth = size.x;
desiredWindowHeight = size.y;
} else {//否则Activity窗口的宽度和高度就是整个屏幕的宽高
DisplayMetrics packageMetrics =
mView.getContext().getResources().getDisplayMetrics();
desiredWindowWidth = packageMetrics.widthPixels;
desiredWindowHeight = packageMetrics.heightPixels;
}
......
} else { //除了第一次被请求执行测量、布局和绘制操作以外,它的当前宽度desiredWindowWidth和高度
//就等于保存在ViewRootImpl类的成员变量mWinFrame/frame中的宽度和高度值,也就是上一次储存的宽高值.
desiredWindowWidth = frame.width(); //以改变后的参数来绘制DecorView的宽高
desiredWindowHeight = frame.height();
if (desiredWindowWidth != mWidth || desiredWindowHeight != mHeight) {
//mWidth此时代表的是上一次执行该方法的时候的frame.width()值,如果此时两值不相等,那就说明视图改变需要重新测量绘制了。
mFullRedrawNeeded = true; //需要重新绘制标志位
mLayoutRequested = true; //要求重新Layout标志位
windowSizeMayChange = true; //Window的尺寸可能改变
}
}
这段代码中主要交代了Activity当前的Window的宽高是如何得出的:
if(Activity窗口是第一次被请求执行测量、布局和绘制操作){
if(如果窗口的类型是有状态栏的){
Activity窗口所需要的宽度和高度就是除了状态栏;
}else{
Activity窗口所需要的宽度和高度就是整个屏幕的宽高;
}
}else{
Activity窗口的宽高为frame成员变量中的保存的上一次测量、布局和绘制时的值;
}
本段代码结尾的地方,关于这个desiredWindowWidth和mWidth的比较,我们需要说一下:首先这个mWidth和mHeight也是用来描述Activity窗口当前宽度和高度的,与desiredWindowWidth和desiredWindowHeight不同,它们的值是由应用程序进程上一次主动请求WindowManagerService服务计算得到的,并且会一直保持不变到应用程序进程下一次再请求WindowManagerService服务来重新计算为止(这个在下文的代码中也有所体现)。
如果Activity窗口不是第一次被请求执行测量、布局和绘制操作,并且Activity窗口主动上一次请求WindowManagerService服务计算得到的宽度mWidth和高度mHeight不等于Activity窗口的当前宽度desiredWindowWidth和当前高度desiredWindowHeight,那么就说明Activity窗口的大小发生了变化,这时候变量windowSizeMayChange的值就会被标记为true,以便接下来可以对Activity窗口的大小变化进行处理。
第二段代码:
......
boolean insetsChanged = false;
boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw);
if (layoutRequested) {
final Resources res = mView.getContext().getResources();
if (mFirst) {
// make sure touch mode code executes by setting cached value
// to opposite of the added touch mode.
mAttachInfo.mInTouchMode = !mAddedTouchMode;
ensureTouchModeLocally(mAddedTouchMode);
} else {
if (!mPendingOverscanInsets.equals(mAttachInfo.mOverscanInsets)) {
insetsChanged = true;
}
if (!mPendingContentInsets.equals(mAttachInfo.mContentInsets)) {
insetsChanged = true;
}
if (!mPendingStableInsets.equals(mAttachInfo.mStableInsets)) {
insetsChanged = true;
}
if (!mPendingVisibleInsets.equals(mAttachInfo.mVisibleInsets)) {
mAttachInfo.mVisibleInsets.set(mPendingVisibleInsets);
if (DEBUG_LAYOUT) Log.v(TAG, "Visible insets changing to: "
+ mAttachInfo.mVisibleInsets);
}
if (!mPendingOutsets.equals(mAttachInfo.mOutsets)) {
insetsChanged = true;
}
if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT
|| lp.height == ViewGroup.LayoutParams.WRAP_CONTENT) {
windowSizeMayChange = true;
if (lp.type == WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL
|| lp.type == WindowManager.LayoutParams.TYPE_INPUT_METHOD) {
// NOTE -- system code, won't try to do compat mode.
Point size = new Point();
mDisplay.getRealSize(size);
desiredWindowWidth = size.x;
desiredWindowHeight = size.y;
} else {
DisplayMetrics packageMetrics = res.getDisplayMetrics();
desiredWindowWidth = packageMetrics.widthPixels;
desiredWindowHeight = packageMetrics.heightPixels;
}
}
}
// Ask host how big it wants to be
windowSizeMayChange |= measureHierarchy(host, lp, res,
desiredWindowWidth, desiredWindowHeight);
}
首先我们来看看这段代码中出现的几个变量——mAttachInfo实际上是View类中的静态内部类AttachInfo类的对象
(frameworks/base/core/java/android/view/View):
/**
* A set of information given to a view when it is attached to its parent
* window.
*/
final static class AttachInfo {
/**
* For windows that are full-screen but using insets to layout inside
* of the screen areas, these are the current insets to appear inside
* the overscan area of the display.
*/
final Rect mOverscanInsets = new Rect(); //下面三个变量的注释基本类似,就不贴了
......(省略注释)
final Rect mContentInsets = new Rect();
final Rect mVisibleInsets = new Rect();
final Rect mStableInsets = new Rect();
......
可以看到这个是View类的一个内部类AttachInfo类的对象,从注释可以看出该类的主要作用就是储存一组当View attach给它的父Window的时候Activity各种属性的信息。我们再来看上一段代码中所涉及到得到该类的几个变量:mOverscanInsets,mContentInsets,mStableInsets,mVisibleInsets,根据注释我们可以对mOverscanInsets所表示的意义稍微理解:这几个变量都是"对于全屏状态下的Window,但是利用'边衬'来布局屏幕内部,这些是当前出现在屏幕上'过扫描区域'内部的'边衬'的变量(这里”insets“翻译为"边衬比较恰当"),关于overscan和insects区域的划分,文章开头的时候已经做了说明,因此不难理解mOverscanInsets这个变量的意义。
这里一共提到了四个变量,事时上,我们看View类的源码就会发现,mContentInsets和mStableInsets都是灰色的代码,也就是没有用到的对象,至于为什么要保留呢?首先我们要知道,这个mOverscanInsets和mContentInsets是WMS用来fitSystemWindows的,我们看老罗的Android窗口管理服务WindowManagerService计算Activity窗口大小的过程分析这篇文章会发现,低版本的Android系统是用mContentInsets来计算的,而在6.0的版本中却换成了mOverscanInsets:
/**
* @hide Compute the insets that should be consumed by this view and the ones
* that should propagate to those under it.
*/
protected boolean computeFitSystemWindows(Rect inoutInsets, Rect outLocalInsets) {
if ((mViewFlags & OPTIONAL_FITS_SYSTEM_WINDOWS) == 0
|| mAttachInfo == null
|| ((mAttachInfo.mSystemUiVisibility & SYSTEM_UI_LAYOUT_FLAGS) == 0
&& !mAttachInfo.mOverscanRequested)) {
outLocalInsets.set(inoutInsets);
inoutInsets.set(0, 0, 0, 0);
return true;
} else {
// The application wants to take care of fitting system window for
// the content... however we still need to take care of any overscan here.
final Rect overscan = mAttachInfo.mOverscanInsets;
outLocalInsets.set(overscan);
inoutInsets.left -= overscan.left;
inoutInsets.top -= overscan.top;
inoutInsets.right -= overscan.right;
inoutInsets.bottom -= overscan.bottom;
return false;
}
}
并且我们可以看到这句无奈的注释:
// The application wants to take care of fitting system window for
// the content... however we still need to take care of any overscan here.
我们再看mPendingOverscanInsets,mPendingContentInsets,mPendingStableInsets,mPendingVisibleInsets四个变量,它们和上面四个变量标示的意义是一样的,只不过它们是由WindowManagerService服务主动请求Activity窗口设置的,但是尚未生效。说的更通俗一点,这个mOverscanInsets是上一次执行performTraversals()函数时保存在mAttachInfo中的值,而mPendingOverscanInsets是这一次请求该函数时还没生效的值,两个值一比较,如果不相等,说明过insets的值发生了改变(insetsChanged = true)。
回到第二段代码中,我们注意到那段代码中的if代码块,如果Activity窗口是第一次被测量,布局和绘制,那么:mAttachInfo.mInTouchMode = !mAddedTouchMode;
这里的mInTouchMode是View内部类AttachInfo类的成员变量,作用是"指示View所处的Window当前是否处于触摸模式",而紧接着ensureTouchModeLocally(mAddedTouchMode);
则是"确保这个Window的触摸模式已经被设置",并且在这个方法的注释中有对传入参数mAddedTouchMode的解释:"我们是否想处于触摸模式",这个方法的返回值是——进入触摸模式,ture;离开触摸模式,flase。但由于再上一段代码中这个方法的返回值并没有进行局部变量赋值等进一步处理,所以可以暂时先不管这个方法。总之,如果Activity窗口是第一次被测量,布局和绘制这种情况下,只是对于是否处于触摸模式情况的一个判断,除了改变了AttachInfo实体类中相关信息以外,并没有做进一步处理。
接着是else中的情况,也就是Activity窗口不是第一次被测量,布局和绘制,即mFrist成员变量中的值为false。此时先判断一下几个insects的值和上一次相比有没有什么变化,不想等的话就将insetsChanged标志位变为ture。接着判断Activity的窗口情况,如果Activity窗口的宽度被设置为ViewGroup.LayoutParams.WRAP_CONTENT或者高度被设置为ViewGroup.LayoutParams.WRAP_CONTENT,那么就意味着Activity窗口的大小要等于内容区域的大小,但是由于Activity窗口的大小是需要覆盖整个屏幕的,因此,这时候就系统会将Activity窗口的当前宽度desiredWindowWidth和当前高度desiredWindowHeight设置为屏幕的宽度和高度。也就是说,如果我们将Activity窗口的宽度和高度设置为ViewGroup.LayoutParams.WRAP_CONTENT,实际上这个设置是无效的,它最终的宽度和高度还是会等于屏幕的宽度和高度。这种情况也意味着Acitivity窗口的大小发生了变化(从设置的WRAP_CONTENT变为屏幕大小),因此将windowSizeMayChange设为true。紧接着是一个窗口类型的判断,如果窗口的类型是有状态栏的,那么Activity窗口的宽度和高度就是屏幕的宽高除了状态栏,否则就是整个屏幕的大小。
最后一句代码:windowSizeMayChange |= measureHierarchy(host, lp, res,desiredWindowWidth, desiredWindowHeight);
,measureHierarchy()这个方法中实际上进行了measure()测量过程,只不过这个测量过程不属于三大流程,而是为了确定Window的大小而打的辅助,只不过最终返回的仍然是widow的size是否改变的boolean值。
紧接着我们看代码:
第三段代码:
......
if (layoutRequested) {
// Clear this now, so that if anything requests a layout in the
// rest of this function we will catch it and re-run a full
// layout pass. --注释写的很清楚了吧?
mLayoutRequested = false;
}
boolean windowShouldResize = layoutRequested && windowSizeMayChange
&& ((mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight())
|| (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT &&
frame.width() < desiredWindowWidth && frame.width() != mWidth)
|| (lp.height == ViewGroup.LayoutParams.WRAP_CONTENT &&
frame.height() < desiredWindowHeight && frame.height() != mHeight));
// Determine whether to compute insets.
// If there are no inset listeners remaining then we may still need to compute
// insets in case the old insets were non-empty and must be reset.
final boolean computesInternalInsets =
mAttachInfo.mTreeObserver.hasComputeInternalInsetsListeners()
|| mAttachInfo.mHasNonEmptyGivenInternalInsets;
第一个if语段注释已经写的很清楚了,我们主要看后面两段,这两段代码做了两件事情:
第一件事情是检查是否需要处理Activity窗口的大小变化事件,如果同时满足下面三个条件,就需要处理,将windowShouldResize标志位变为true:
①layoutRequested等于true,说明程序已经发起(正在执行)了一次测量,布局,绘制流程。
②windowSizeMayChange等于true,说明前面已经检测到了Activity窗口的变化。
③上一段代码的最后一句中,我们说过已经做了一次measure()工作,如果测量出来的宽度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);
}
......
这段代码以及接下来的两段代码都是在满足下面的条件之一的情况下执行的(都在一个if()里边):
①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窗口指定了额外的内容区域边衬和可见区域边衬,那么就需要检查mFirst或者变量viewVisibilityChanged的值是否等于true。如果这些条件能满足,那么变量insetsPending的值就会等于true,表示Activity窗口有额外的过扫描区域边衬和可见区域边衬等待指定。
我们接着看代码:
第五段代码:
try {
......
relayoutResult = relayoutWindow(params, viewVisibility, insetsPending);
......
final boolean overscanInsetsChanged = !mPendingOverscanInsets.equals(
mAttachInfo.mOverscanInsets);
contentInsetsChanged = !mPendingContentInsets.equals(
mAttachInfo.mContentInsets);
final boolean visibleInsetsChanged = !mPendingVisibleInsets.equals(
mAttachInfo.mVisibleInsets);
final boolean stableInsetsChanged = !mPendingStableInsets.equals(
mAttachInfo.mStableInsets);
final boolean outsetsChanged = !mPendingOutsets.equals(mAttachInfo.mOutsets);
if (contentInsetsChanged) {
......
mAttachInfo.mContentInsets.set(mPendingContentInsets);
}
} catch (RemoteException e) {
}
......
mAttachInfo.mWindowLeft = frame.left;
mAttachInfo.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.
if (mWidth != frame.width() || mHeight != frame.height()) {
mWidth = frame.width();// mWidth和mHeight也是用来描述Activity窗口当前宽度和高度的
mHeight = frame.height(); // 与desiredWindowWidth和desiredWindowHeight不同,它们的值是
//由应用程序进程上一次主动请求WindowManagerService服务计算得到的,并且会一直保持不变到应用程序进
//程下一次再请求WindowManagerService服务来重新计算为止
}
这段代码主要就是调用relayoutWindow()来请求WindowManagerService服务计算Activity窗口的大小以及过扫描区域边衬大小和可见区域边衬大小。计算完毕之后,Activity窗口的大小就会保存在成员变量mWinFrame中,而Activity窗口的内容区域边衬大小和可见区域边衬大小分别保存在ViewRoot类的成员变量mPendingOverscanInsets和mPendingVisibleInsets中。
如果这次计算得到的Activity窗口的内容区域边衬大小mPendingContentInsets和可见区域边衬大小mPendingVisibleInsets与上一次计算得到的不一致,即mAttachInfo所指向的一个AttachInfo对象的成员变量mOverscanInsets和mVisibleInsets所描述的大小不一致,那么变量overscanInsetsChanged和visibleInsetsChanged的值就会等于true,表示Activity窗口的内容过扫描边衬大小和可见区域边衬大小发生了变化。(虽然这段代码中一共提到了四个变量,但是上文我们说过,只有mOverscanInsets和mVisibleInsetsshiy是用到的)。
由于变量frame和mWinFrame引用的是同一个Rect对象,因此,这时候变量frame描述的也是Activity窗口请求WindowManagerService服务计算之后得到的大小。这段代码分别将计算得到的Activity窗口的左上角坐标保存在变量attachInfo所指向的一个AttachInfo对象的成员变量mWindowLeft和mWindowTop中,并且将计算得到的Activity窗口的宽度和高度保存在mWidth和mHeight中。
继续看代码:
第六段代码:
......
//mStopped的注释:Set to true if the owner of this window is in the stopped state.如果此窗口的所有者
//(Activity)处于停止状态,则为ture.
if (!mStopped || mReportNextDraw) { //mReportNextDraw Window上报下一次绘制.
boolean focusChangedDueToTouchMode = ensureTouchModeLocally(
(relayoutResult&WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE) != 0);
if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
|| mHeight != host.getMeasuredHeight() || contentInsetsChanged) {
//mWidth != host.getMeasuredWidth() 表示frame的宽不等于初始DecorView宽.
//getMeasuredWidth()方法可以获取View测量后的宽高,host上面说过为DecorView根布局.
//获得view宽高的测量规格,lp.width和lp.height表示DecorView根布局宽和高
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
// Ask host how big it wants to be
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);//开始执行测量操作 ①
int width = host.getMeasuredWidth();//getMeasuredWidth()所获得View的宽高绝大部分情况下等于View最终的宽高
int height = host.getMeasuredHeight();
boolean measureAgain = false;
//lp.horizontalWeight表示将多少额外空间水平地(在水平方向上)分配给与这些LayoutParam关联的视图。如果
//视图不应被拉伸,请指定0。否则,将在所有权重大于0的视图中分配额外的像素。
if (lp.horizontalWeight > 0.0f) {
width += (int) ((mWidth - width) * lp.horizontalWeight);
childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
measureAgain = true; //重新测量标志位
}
if (lp.verticalWeight > 0.0f) {
height += (int) ((mHeight - height) * lp.verticalWeight);
childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height,MeasureSpec.EXACTLY);
measureAgain = true;
}
if (measureAgain) {
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); //重新测量
}
layoutRequested = true;
}
}
这段代码用来检查是否需要重新测量Activity窗口的大小。如果满足以下条件之一,那么就需要重新测量:
① Activity窗口的触摸模式发生了变化,并且由此引发了Activity窗口当前获得焦点的控件发生了变化,即变量focusChangedDueToTouchMode的值等于true。这个检查是通过调用ensureTouchModeLocally来实现的。
② Activity窗口前面测量出来的宽度host.mMeasuredWidth和高度host.mMeasuredHeight不等于WindowManagerService服务计算出来的宽度mWidth和高度mHeight。
③Activity窗口的内容区域边衬大小和可见区域边衬大小发生了变化,即前面得到的变量contentInsetsChanged的值等于true。
重新计算了一次之后,如果Activity窗口的属性lp表明需要对测量出来的宽度width和高度height进行扩展,即变量lp所指向的一个WindowManager.LayoutParams对象的成员变量horizontalWeight和verticalWeight的值大于0.0,那么就需要对Activity窗口的顶层视图host的最大可用空间进行扩展后再进行一次测量工作。
接下来的一段代码:
第七段代码:
} else {
final boolean windowMoved = (mAttachInfo.mWindowLeft != frame.left
|| mAttachInfo.mWindowTop != frame.top);
if (windowMoved) { //如果窗口移动
if (mTranslator != null) {
mTranslator.translateRectInScreenToAppWinFrame(frame);
}
mAttachInfo.mWindowLeft = frame.left;
mAttachInfo.mWindowTop = frame.top;
// Update the light position for the new window offsets.
if (mAttachInfo.mHardwareRenderer != null) {
mAttachInfo.mHardwareRenderer.setLightCenter(mAttachInfo);
}
}
}
首先,这里的else接的是“第四段代码”中的那个if,是的,这个if就是这么长。其次,这段代码实际上就是判断Window有没有移动,如果发生移动则会调用translateRectInScreenToAppWinFrame()执行移动动画。
这个mWindowLeft和mWindowTop代表的分别是View所attach的Window的左坐标和顶坐标,回到上一个方法中,我们可以看到,其实在这个方法中,有这么两句:
mAttachInfo.mWindowLeft = frame.left;
mAttachInfo.mWindowTop = frame.top;
也就是说,final boolean windowMoved = (mAttachInfo.mWindowLeft != frame.left|| mAttachInfo.mWindowTop != frame.top);
这句实际上就是将上一次调用改方法时Window的顶坐标和左坐标与这一次相比,从而得出Window是否Moved。
最后一段代码:
第八段代码:
......
final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
if (didLayout) { //layoutRequested测量要求标志位
performLayout(lp, desiredWindowWidth, desiredWindowHeight); //开始执行Layout操作 ②
// By this point all views have been sized and positioned
// We can compute the transparent area
......(省略计算透明区域的一大块方法)
}
boolean skipDraw = false;
if (mFirst) {
......
} else if (mWindowsAnimating) { //表示窗口动画正在进行中。
//mRemainingFrameCount:How many frames the app is still allowed to draw when a window animation is happening
if (mRemainingFrameCount <= 0) {
skipDraw = true; //跳过绘制,剩下允许的帧数为0了。
}
mRemainingFrameCount--;
}
mFirst = false;
......
if (!cancelDraw && !newSurface) { //既没有取消绘制,也没有创建新的平面。
if (!skipDraw || mReportNextDraw) { //没有跳过绘制或者已经上报进行下一次绘制
......
performDraw(); //开始执行绘制操作 ③
}
} else {
if (viewVisibility == View.VISIBLE) {
// Try again
scheduleTraversals(); //还记得这个方法是干什么的吧?就是重新来过的意思!不记得的话去看上一篇文章。
} else if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
for (int i = 0; i < mPendingTransitions.size(); ++i) {
mPendingTransitions.get(i).endChangingAnimations();
}
mPendingTransitions.clear();
}
}
mIsInTraversal = false;
}
在这段代码中,Activity的Window测量过程已经进行完毕,当满足layoutRequested && (!mStopped || mReportNextDraw);条件时,将进行布局(layout)过程。可以看到“第六段代码”和本段代码中,出现了View绘制的三大流程。
关于View绘制的三大流程以及relayoutWindow()中涉及到的WindowManagerService服务计算Activity窗口的大小的过程我们将另起文章来分析。
站在巨人的肩膀上摘苹果:
Android窗口管理服务WindowManagerService计算Activity窗口大小的过程分析
图解Android - Android GUI 系统 (2) - 窗口管理 (View, Canvas, Window Manager)