在Android中,Veiw从内存中到呈现在UI界面上需要经过measure(测量)、layout(布局)、draw(绘制)这样一个过程。为什么需要measure过程?因为在Android中View有自适应尺寸的机制,在用自适应尺寸来定义View大小的时候,View的真实尺寸还不能确定,这时候就需要根据View的宽高匹配规则,经过计算,得到具体的像素值,measure过程就是干这件事。
本文将从源码角度解析View的measure过程,这其中会涉及某些关键类以及关键方法。
MeasureSpec
MeasureSpec封装了父布局传递给子布局的布局要求,它通过一个32位int类型的值来表示,该值包含了两种信息,高两位表示的是SpecMode
(测量模式),低30位表示的是SpecSize
(测量的具体大小)。下面通过注释的方式来分析来类:
/**
* 三种SpecMode:
* 1.UNSPECIFIED
* 父ViewGroup没有对子View施加任何约束,子view可以是任意大小。这种情况比较少见,主要用于系统内部多次measure的情形,用到的一般都是可以滚动的容器中的子view,比如ListView、GridView、RecyclerView中某些情况下的子view就是这种模式。一般来说,我们不需要关注此模式。
* 2.EXACTLY
* 该view必须使用父ViewGroup给其指定的尺寸。对应match_parent或者具体数值(比如30dp)
* 3.AT_MOST
* 该View最大可以取父ViewGroup给其指定的尺寸。对应wrap_content
*
* MeasureSpec使用了二进制去减少对象的分配。
*/
public class MeasureSpec {
// 进位大小为2的30次方(int的大小为32位,所以进位30位就是要使用int的最高位和第二高位也就是32和31位做标志位)
private static final int MODE_SHIFT = 30;
// 运算遮罩,0x3为16进制,10进制为3,二进制为11。3向左进位30,就是11 00000000000(11后跟30个0)
// (遮罩的作用是用1标注需要的值,0标注不要的值。因为1与任何数做与运算都得任何数,0与任何数做与运算都得0)
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
// 0向左进位30,就是00 00000000000(00后跟30个0)
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
// 1向左进位30,就是01 00000000000(01后跟30个0)
public static final int EXACTLY = 1 << MODE_SHIFT;
// 2向左进位30,就是10 00000000000(10后跟30个0)
public static final int AT_MOST = 2 << MODE_SHIFT;
/**
* 根据提供的size和mode得到一个详细的测量结果
*/
// 第一个return:
// measureSpec = size + mode; (注意:二进制的加法,不是十进制的加法!)
// 这里设计的目的就是使用一个32位的二进制数,32和31位代表了mode的值,后30位代表size的值
// 例如size=100(4),mode=AT_MOST,则measureSpec=100+10000...00=10000..00100
//
// 第二个return:
// size & ~MODE_MASK就是取size 的后30位,mode & MODE_MASK就是取mode的前两位,最后执行或运算,得出来的数字,前面2位包含代表mode,后面30位代表size
public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
/**
* 获得SpecMode
*/
// mode = measureSpec & MODE_MASK;
// MODE_MASK = 11 00000000000(11后跟30个0),原理是用MODE_MASK后30位的0替换掉measureSpec后30位中的1,再保留32和31位的mode值。
// 例如10 00..00100 & 11 00..00(11后跟30个0) = 10 00..00(AT_MOST),这样就得到了mode的值
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
/**
* 获得SpecSize
*/
// size = measureSpec & ~MODE_MASK;
// 原理同上,不过这次是将MODE_MASK取反,也就是变成了00 111111(00后跟30个1),将32,31替换成0也就是去掉mode,保留后30位的size
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
}
measure()
当View的父ViewGroup对View进行测量时,会调用View的measure
方法,ViewGroup会传入widthMeasureSpec
和heightMeasureSpec
,分别表示父控件对View的宽度和高度的一些限制条件。源码分析该方法:
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
//首先判断当前View的layoutMode是不是特例LAYOUT_MODE_OPTICAL_BOUNDS
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
//LAYOUT_MODE_OPTICAL_BOUNDS是特例情况,比较少见,不分析
Insets insets = getOpticalInsets();
int oWidth = insets.left + insets.right;
int oHeight = insets.top + insets.bottom;
widthMeasureSpec = MeasureSpec.adjust(widthMeasureSpec, optical ? -oWidth : oWidth);
heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
}
//根据widthMeasureSpec和heightMeasureSpec计算key值,在下面用key值作为键,缓存我们测量得到的结果
long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
//mMeasureCache是LongSparseLongArray类型的成员变量,
//其缓存着View在不同widthMeasureSpec、heightMeasureSpec下测量过的结果
if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);
//mOldWidthMeasureSpec和mOldHeightMeasureSpec分别表示上次对View进行测量时的widthMeasureSpec和heightMeasureSpec
//执行View的measure方法时,View总是先检查一下是不是真的有必要费很大力气去做真正的测量工作
//mPrivateFlags是一个Int类型的值,其记录了View的各种状态位
//如果(mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT,
//那么表示当前View需要强制进行layout(比如执行了View的forceLayout方法),所以这种情况下要尝试进行测量
//如果新传入的widthMeasureSpec/heightMeasureSpec与上次测量时的mOldWidthMeasureSpec/mOldHeightMeasureSpec不等,
//那么也就是说该View的父ViewGroup对该View的尺寸的限制情况有变化,这种情况下要尝试进行测量
if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
widthMeasureSpec != mOldWidthMeasureSpec ||
heightMeasureSpec != mOldHeightMeasureSpec) {
//通过按位操作,重置View的状态标志mPrivateFlags,将其标记为未测量状态
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
//对阿拉伯语、希伯来语等从右到左书写、布局的语言进行特殊处理
resolveRtlPropertiesIfNeeded();
//在View真正进行测量之前,View还想进一步确认能不能从已有的缓存mMeasureCache中读取缓存过的测量结果
//如果是强制layout导致的测量,那么将cacheIndex设置为-1,即不从缓存中读取测量结果
//如果不是强制layout导致的测量,那么我们就用上面根据measureSpec计算出来的key值作为缓存索引cacheIndex,这时候有可能找到相应的值,找到就返回对应索引;也可能找不到,找不到就返回-1
int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
//在缓存中找不到相应的值或者需要忽略缓存结果的时候,重新测量一次
//此处调用onMeasure方法,并把尺寸限制条件widthMeasureSpec和heightMeasureSpec传入进去
//onMeasure方法中将会进行实际的测量工作,并把测量的结果保存到成员变量中
onMeasure(widthMeasureSpec, heightMeasureSpec);
//onMeasure执行完后,通过位操作,重置View的状态mPrivateFlags,将其标记为在layout之前不必再进行测量的状态
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} else {
//如果运行到此处,那么表示当前的条件允许View从缓存成员变量mMeasureCache中读取测量过的结果
//用上面得到的cacheIndex从缓存mMeasureCache中取出值,不必在调用onMeasure方法进行测量了
long value = mMeasureCache.valueAt(cacheIndex);
//一旦我们从缓存中读到值,我们就可以调用setMeasuredDimensionRaw方法将当前测量的结果保存到成员变量中
setMeasuredDimensionRaw((int) (value >> 32), (int) value);
mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
//如果我们自定义的View重写了onMeasure方法,但是没有调用setMeasuredDimension()方法,
//那么此处就会抛出异常,提醒开发者在onMeasure方法中调用setMeasuredDimension()方法
//Android是如何知道我们有没有在onMeasure方法中调用setMeasuredDimension()方法的呢?
//方法很简单,还是通过解析状态位mPrivateFlags。
//setMeasuredDimension()方法中会将mPrivateFlags设置为PFLAG_MEASURED_DIMENSION_SET状态,即已测量状态,
//此处就检查mPrivateFlags是否含有PFLAG_MEASURED_DIMENSION_SET状态即可判断setMeasuredDimension是否被调用
if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
throw new IllegalStateException("View with id " + getId() + ": "
+ getClass().getName() + "#onMeasure() did not set the"
+ " measured dimension by calling"
+ " setMeasuredDimension()");
}
//到了这里,View已经测量完了并且将测量的结果保存在View的mMeasuredWidth和mMeasuredHeight中,将标志位置为可以layout的状态
mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
}
//mOldWidthMeasureSpec和mOldHeightMeasureSpec保存着最近一次测量时的MeasureSpec,
//在测量完成后将这次新传入的MeasureSpec赋值给它们
mOldWidthMeasureSpec = widthMeasureSpec;
mOldHeightMeasureSpec = heightMeasureSpec;
//最后用上面计算出的key作为键,测量结果作为值,将该键值对放入成员变量mMeasureCache中,
//这样就实现了对本次测量结果的缓存,以便在下次measure方法执行的时候,有可能将其从中直接读出,
//从而省去实际测量的步骤
mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
(long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
}
上面的注释已经一目了然,这里再总结一下measure()
都干了什么事:
-
首先,调用
View.measure()
方法时View并不是立即就去测量,而是先判断一下是否有必要进行测量操作,如果不是强制测量或者MeasureSpec
与上次的MeasureSpec
相同的时候,那么View就不需要重新测量了. -
如果不满足上面条件,View就考虑去做测量工作了.但在测量之前,View还想偷懒,如果能在缓存中找到上次的测量结果,那直接从缓存中获取就可以了.它会以MeasureSpec计算出的key值作为键,去成员变量
mMeasureCache
中查找是否缓存过对应key的测量结果,如果能找到,那么就简单调用一下setMeasuredDimensionRaw
方法,将从缓存中读到的测量结果保存到成员变量mMeasuredWidth
和mMeasuredHeight
中。 -
如果不能从
mMeasureCache
中读到缓存过的测量结果,那么这次View就真的不能再偷懒了,只能乖乖地调用onMeasure()
方法去完成实际的测量工作,并且将尺寸限制条件widthMeasureSpec
和heightMeasureSpec
传递给onMeasure()
方法。关于onMeasure()
方法,我们会在下面详细介绍。 -
不论上面代码走了哪个判断的分支,最终View都会得到测量的结果,并且将结果保存到
mMeasuredWidth
和mMeasuredHeight
这两个成员变量中,同时缓存到成员变量mMeasureCache
中,以便下次执行measure()
方法时能够从其中读取缓存值。 -
需要说明的是,View有一个成员变量
mPrivateFlags
,用以保存View的各种状态位,在测量开始前,会将其设置为未测量状态,在测量完成后会将其设置为已测量状态。
onMeasure()
上面我们提到,View的measure()
方法在需要进行实际的测量工作时会调用onMeasure()
方法.看下源码:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
我们发现onMeasure()
方法中调用了setMeasuredDimension()
方法,setMeasuredDimension()
又调用了getDefaultSize()
方法.getDefaultSize()
又调用了getSuggestedMinimumWidth()
和getSuggestedMinimumHeight()
,那我们反向研究一下,先看下getSuggestedMinimumWidth()
方法(getSuggestedMinimumHeight()
原理getSuggestedMinimumWidth()
跟一样).
getSuggestedMinimumWidth()
该方法返回View推荐的最小宽度,源码如下:
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
源码很简单,如果View没有背景,就直接返回View本身的最小宽度mMinWidth
;如果给View设置了背景,就取View本身的最小宽度mMinWidth
和背景的最小宽度的最大值.
那么mMinWidth
是哪里来的?搜索下源码就可以知道,View的最小宽度mMinWidth
可以有两种方式进行设置:
- 第一种是在View的构造方法中进行赋值的,View通过读取XML文件中View设置的
minWidth
属性来为mMinWidth
赋值:
case R.styleable.View_minWidth:
mMinWidth = a.getDimensionPixelSize(attr, 0);
break;
- 第二种是在调用View的
setMinimumWidth
方法为mMinWidth
赋值:
public void setMinimumWidth(int minWidth) {
mMinWidth = minWidth;
requestLayout();
}
getDefaultSize()
知道了getSuggestedMinimumWidth()/getSuggestedMinimumHeight()
这两个方法返回的是View的最小宽度/高度之后,我们将得到的最小宽度/高度值作为参数传给getDefaultSize(int size, int measureSpec)
方法,看下源码:
public static int getDefaultSize(int size, int measureSpec) {
//size是传进来的View自己想要的最小宽度/高度
int result = size;
//measureSpec是父ViewGroup给View的限制条件,解析出specMode和specSize
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
//如果specMode为UNSPECIFIED,就表明父ViewGroup没有对该View尺寸进行限制,直接取View自己想要的宽高
case MeasureSpec.UNSPECIFIED:
result = size;
break;
//如果specMode为EXACTLY,表明父ViewGroup要求该View必须用父ViewGroup指定的尺寸(specSize),取父ViewGroup指定的宽高值
//如果specMode为AT_MOST,表明ViewGroup给该View指定了最大宽度/高度尺寸(specSize),取父ViewGroup指定的最大宽度/高度。
//这里肯定有人有疑问了?为什么specMode为AT_MOST是取View能到达的最大宽高值specSize,跟EXACTLY模式下的取值一模一样,联想到EXACTLY对应match_parent,AT_MOST对应wrap_content,那这样wrap_content不就跟match_parent一样的效果么?是的,调用这个方法在测量的时候,wrap_content确实跟match_parent一样的效果,这样做有可能是Android还没适配wrap_content而做的简单处理,就像Recyclerview早期的版本就没有适配wrap_content,导致wrap_content和match_parent一样的效果,直到23.2.0版本才将match_parent和wrap_content区分开来。那适配了wrap_content的测量方法在哪里呢?下文会讲到。
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
通过代码可以看到,父ViewGroup通过measureSpec
对View尺寸的限制作用已经体现出来了。最终通过该方法可以得到View在符合ViewGroup的限制条件下的默认尺寸,即
getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec)
:获得该View在符合ViewGroup的限制条件下的默认宽度值
getDefaultSize(getSuggestedMinimumHeight(), widthMeasureSpec)
:获得该View在符合ViewGroup的限制条件下的默认高度值
从注释可以看出,getDefaultSize()
这个测量方法并没有适配wrap_content
这一种布局模式,只是简单地将wrap_content
跟match_parent
等同起来。
到了这里,我们要注意一个问题,getDefaultSize()
方法中wrap_content
和match_parent
属性的效果是一样的,而该方法是View的onMeasure()
中默认调用的,也就是说,对于一个直接继承自View的自定义View来说,它的wrap_content和match_parent属性是一样的效果,因此如果要实现自定义View的wrap_content
,则要重写onMeasure()
方法,对wrap_content
属性进行处理。如何处理呢?也很简单,代码如下所示:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//取得父ViewGroup指定的宽高测量模式和尺寸
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
//如果宽高都是AT_MOST的话,即都是wrap_content布局模式,就用View自己想要的宽高值
setMeasuredDimension(mWidth, mHeight);
}else if (widthSpecMode == MeasureSpec.AT_MOST) {
//如果只有宽度都是AT_MOST的话,即只有宽度是wrap_content布局模式,宽度就用View自己想要的宽度值,高度就用父ViewGroup指定的高度值
setMeasuredDimension(mWidth, heightSpecSize);
}else if (heightSpecMode == MeasureSpec.AT_MOST) {
//如果只有高度都是AT_MOST的话,即只有高度是wrap_content布局模式,高度就用View自己想要的宽度值,宽度就用父ViewGroup指定的高度值
setMeasuredDimension(widthSpecSize, mHeight);
}
}
在上面的代码中,我们要给View指定一个默认的内部宽/高(mWidth
和mHeight
),并在wrap_content
时设置此宽/高即可。
setMeasuredDimension()
现在再来看下setMeasuredDimension()
这个方法,该方法将通过getDefaultSize()
得到的宽高值作为参数传进去,看下源码都干了些什么:
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int opticalWidth = insets.left + insets.right;
int opticalHeight = insets.top + insets.bottom;
measuredWidth += optical ? opticalWidth : -opticalWidth;
measuredHeight += optical ? opticalHeight : -opticalHeight;
}
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
该方法会在开始判断layoutMode是不是LAYOUT_MODE_OPTICAL_BOUNDS
的特殊情况,这种特例很少见,我们直接忽略掉。
setMeasuredDimension()
方法最后将宽高值传递给方法setMeasuredDimensionRaw()
,我们再研究一下setMeasuredDimensionRaw()
这方法。
setMeasuredDimensionRaw()
该方法接受两个参数,也就是测量完的宽度和高度,看源码:
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
看到了吧,这里就是把测量完的宽高值赋值给mMeasuredWidth
、mMeasuredHeight
这两个View的属性,然后将标志位置为已测量状态。
View宽高尺寸值的state
至此,由父ViewGroup发起的向它内部的每个子View发送measure命令,然后各个View根据ViewGroup给的限制条件测量出来的宽高尺寸已经存到View的mMeasuredWidth
、mMeasuredHeight
这两个属性当中。但ViewGroup怎么知道他的子View是多大呢?View提供了以下三组方法:
-
getMeasuredWidth()
和getMeasuredHeight()
-
getMeasuredWidthAndState()
和getMeasuredHeightAndState()
-
getMeasuredState()
通过方法名称可以猜出宽高的尺寸有state这个概念,我们再来研究View中保存测量结果的属性mMeasuredWidth
和mMeasuredHeight
,其实只要讨论mMeasuredWidth
就可以了,mMeasuredHeight
一样的道理。
mMeasuredWidth
是一个Int类型的值,其是由4个字节组成的。
Android为让其View的父控件获取更多的信息,就在mMeasuredWidth
上下了很大功夫,虽然是一个Int值,但是想让它存储更多信息,具体来说就是把mMeasuredWidth
分成两部分:
-
其高位的第一个字节为第一部分,用于标记测量完的尺寸是不是达到了View想要的宽度,我们称该信息为测量的state信息。
-
其低位的三个字节为第二部分,用于存储测量到的宽度。
一个变量能包含两个信息,这个有点类似于measureSpec
,但是二者又有不同:
-
measureSpec
是将限制条件从ViewGroup传递给其子View。 -
mMeasuredWidth
、mMeasuredHeight
是将带有测量结果的state标志位信息从View传递给其父ViewGroup。
那是在哪里有对mMeasuredWidth
的第一个字节进行处理呢?可以看到我们下面看一下View中的resolveSizeAndState()
方法。
resolveSizeAndState()
这是View一个很重要的测量方法,直接看源码:
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
final int specMode = MeasureSpec.getMode(measureSpec);
final int specSize = MeasureSpec.getSize(measureSpec);
final int result;
switch (specMode) {
case MeasureSpec.AT_MOST:
//当specMode为AT_MOST时,这时候specSize是父ViewGroup给该View指定的最大尺寸
if (specSize < size) {
//如果父ViewGroup指定的最大尺寸比View想要的尺寸还要小,这时候会使用MEASURED_STATE_TOO_SMALL这个掩码向已经测量出来的尺寸specSize加入尺寸太小的标志,然后将这个带有标志的specSize返回
result = specSize | MEASURED_STATE_TOO_SMALL;
} else {
//如果父控件指定最大尺寸没有比View想要的尺寸小,这时候就放弃之前已经给View赋值的specSize,用View自己想要的尺寸就可以了。
result = size;
}
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
case MeasureSpec.UNSPECIFIED:
default:
result = size;
}
return result | (childMeasuredState & MEASURED_STATE_MASK);
}
这个方法的代码结构跟前文提到的getDefaultSize()
方法很相似,主要的区别在于specMode
为AT_MOST的情况。我们当时说getDefaultSize()
方法是没有适配wrap_content
这种情况,而这个resolveSizeAndState()
方法是已经适配了wrap_content
的布局方式,那具体怎么实现AT_MOST测量逻辑的呢?有两种情况:
-
当父ViewGroup指定的最大尺寸比View想要的尺寸还要小时,会给这个父ViewGroup的指定的最大值
specSize
加入一个尺寸太小的标志MEASURED_STATE_TOO_SMALL,然后将这个带有标志的尺寸返回,父ViewGroup通过该标志就可以知道分配给View的空间太小了,在窗口协商测量的时候会根据这个标志位来做窗口大小的决策。 -
当父ViewGroup指定的最大尺寸比没有比View想要的尺寸小时(相等或者View想要的尺寸更小),直接取View想要的尺寸,然后返回该尺寸。
getDefaultSize()
方法只是onMeasure()
方法中获取最终尺寸的默认实现,其返回的信息比resolveSizeAndState()
要少,那么什么时候才会调用resolveSizeAndState()
方法呢? 主要有两种情况:
-
Android中的大部分layout类都调用了
resolveSizeAndState()
方法,比如LinearLayout在测量过程中会调用resolveSizeAndState()
方法而非getDefaultSize()
方法。 -
我们自己在实现自定义的View或ViewGroup时,我们可以重写
onMeasure()
方法,并在该方法内调用resolveSizeAndState()
方法。
getMeasureXXX系列方法
现在回过头来看下以下三组方法:
-
getMeasuredWidth()
和getMeasuredHeight()
该组方法只返回测量结果的尺寸信息,去除掉高位字节的state信息,以getMeasuredWidth()
为例,源码如下:
public final int getMeasuredWidth() {
// MEASURED_SIZE_MASK = 0x00ffffff,mMeasuredWidth与MEASURED_SIZE_MASK作与运算,
// 就能将mMeasuredWidth的最高字节全部置0,从而去掉state信息
return mMeasuredWidth & MEASURED_SIZE_MASK;
}
-
getMeasuredWidthAndState()
和getMeasuredHeightAndState()
该组方法返回测量结果同时包含尺寸和state信息,以getMeasuredWidthAndState()
为例,源码如下:
public final int getMeasuredWidthAndState() {
//由于mMeasuredWidth完整包含了尺寸和state信息,直接返回该信息
return mMeasuredWidth;
}
getMeasuredState()
该方法返回一个int值,该值同时包含宽度的state以及高度的state信息,不包含任何的尺寸信息,源码如下:
public final int getMeasuredState() {
//将宽度state信息保存到int值的第一个字节中
//将高度state信息保存到int值的第三个字节中
return (mMeasuredWidth&MEASURED_STATE_MASK)
| ((mMeasuredHeight>>MEASURED_HEIGHT_STATE_SHIFT)
& (MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT));
}
-
MEASURED_STATE_MASK的值为0xff000000,其高字节的8位全部为1,低字节的24位全部为0。
-
MEASURED_HEIGHT_STATE_SHIFT值为16。
-
将MEASURED_STATE_MASK与
mMeasuredWidth
做与操作之后就取出了存储在宽度首字节中的state信息,过滤掉低位三个字节的尺寸信息。 -
由于int有四个字节,首字节已经存了宽度的state信息,那么高度的state信息就不能存在首位字节。MEASURED_STATE_MASK向右移16位,变成了0x0000ff00,这个值与高度值
mMeasuredHeight
做与操作就取出了mMeasuredHeight
第三个字节中的信息。而mMeasuredHeight
的state信息是存在首字节中,所以也得对mMeasuredHeight
向右移相同的位置,这样就把state信息移到了第三个字节中。 -
最后,将得到的宽度state与高度state按位或操作,这样就拼接成一个int值,该值首个字节存储宽度的state信息,第三个字节存储高度的state信息。
ViewGroup的measure过程
通过上面的介绍已经知道了单个View的测量过程,现在看下ViewGroup是怎样测量的。
对于ViewGroup来说,除了完成自己的measure过程,还会遍历去调用所有子元素的measure()
方法,各个子元素再递归去执行这个过程。和View不同的是,ViewGroup是一个抽象类,它提供了一个叫measureChildren()
的方法用于测量子元素,源码如下:
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
//遍历每个子元素,如果该子元素不是GONE的话,就去测量该子元素
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
从上述代码来看,ViewGroup在measure时,会调用measureChild()
这个方法对每一个子元素进行测量,该方法源码如下:
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
//获取child自身的LayoutParams属性
final LayoutParams lp = child.getLayoutParams();
//根据父布局的MeasureSpec,父布局的padding和child的LayoutParams这三个参数,通过getChildMeasureSpec()方法计算出子元素的MeasureSpec
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
//调用measure()方法测量child,前文已经解释过这个方法,调用该方法之后会将view的宽高值保存在mMeasuredWidth和mMeasuredHeight这两个属性当中,这样child的尺寸就已经测量出来了
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
很显然,measureChild()
的思想就是取出子元素的LayoutParams,然后再通过getChildMeasureSpec()
方法来创建子元素的MeasureSpec,接着将MeasureSpec传给View的measure()
方法来完成对子元素的测量。重点看下getChildMeasureSpec()
这个方法。
getChildMeasureSpec()
该方法是根据父容器的MeasureSpec、padding和子元素的LayoutParams属性得到子元素的MeasureSpec,进而根据这个MeasureSpec来测量子元素。源码如下:
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//取得SpecMode和SpecSize
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
//子元素的可用大小为父容器的尺寸减去padding
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
//父容器是EXACTLY模式,表明父容器本身的尺寸已经是确定的了
case MeasureSpec.EXACTLY:
//childDimension是子元素的属性值,如果大于等于0,就说明该子元素是指定宽/高尺寸的(比如20dp),
//因为MATCH_PARENT的值为-1,WRAP_CONTENT的值为-2,都是小于0的,所以大于等于0肯定是指定固定尺寸的。
//既然子元素都指定固定大小了,就直接取指定的尺寸,
//然后将子元素的测量模式定为EXACTLY模式,表明子元素的尺寸也确定了
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// 如果子元素是MATCH_PARENT,也就是希望占满父容器的空间,那子元素的尺寸就取父容器的可用空间大小,模式也是EXACTLY,表明子元素的尺寸也确定了
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// 如果子元素是WRAP_CONTENT,也就是宽/高希望能包裹自身的内容就可以了,
//但由于这时子元素自身还没测量,无法知道自己想要多大的尺寸,
//所以这时就先取父容器给子元素留下的最大空间,模式为AT_MOST,表示子元素的宽/高不能超过该最大值
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 父容器的尺寸还没确定,但是不能超过最大值
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// 子元素指定了大小,就取子元素的尺寸,模式为EXACTLY,表明该子元素确定了尺寸
// 这时父容器的限制对子元素来说是不起作用的,子元素的尺寸是可以超出了父容器的大小,超出的部分是显示不出来的
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// 子元素是MATCH_PARENT,表明子元素希望占满父容器,
//但是父容器自身的大小还没确定,也无法给子元素确切的尺寸,
//这时就先取父容器给子元素留下的最大空间,模式为AT_MOST,表示子元素不能超过该最大值
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// 子元素的尺寸只希望能包裹自身的内容就可以了,这时子元素还没测量,无法知道具体尺寸,
// 就先取父容器给子元素留下的最大空间,模式为AT_MOST,表示子元素不能超过该最大值
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 父容器没有对子元素的大小进行约束
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// 子元素指定了大小,就取子元素的尺寸,模式为EXACTLY,表明该子元素确定了尺寸
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// 子元素想要占满父容器,先判断下子元素是否需要取0,
// 如果不需要取0,就先取父容器给子元素留下的最大空间,模式为UNSPECIFIED,表示子元素并没有受到约束
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// 子元素的尺寸只希望能包裹自身的内容就可以了,判断下需不需要取0,
// 如果不需要就先取父容器给子元素留下的最大空间,模式为UNSPECIFIED,表示子元素并没有受到约束
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//根据得到的大小和模式返回一个MeasureSpec
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
getChildMeasureSpec()
这个方法清楚展示了普通View的MeasureSpec的创建规则,每个View的MeasureSpec状态量由其直接父View的MeasureSpec和View自身的属性LayoutParams(LayoutParams有宽高尺寸值等信息)共同决定。总结为下表:
View的布局属性��ViewGroup的MeasurSpec | EXACTLY | AT_MOST | UNSPECIFIED |
---|---|---|---|
非负具体值 | EXACTLY childSize | EXACTLY childSize | EXACTLY childSize |
match_parent | EXACTLY parentSize | AT_MOST parentSize | UNSPECIFIED 0/parentSize |
wrap_content | AT_MOST parentSize | AT_MOST parentSize | UNSPECIFIED 0/parentSize |
在得到View的MeasureSpec状态后,将其与尺寸值通过makeMeasureSpec(int size,int mode)
方法结合在一起,就是最终传给View的onMeasure(int, int)
方法的MeasureSpec值了。
查看源码发现,ViewGroup并没有定义其测量的具体过程方法,这是因为ViewGroup是一个抽象类,其测量过程的onMeasure()
方法需要各个子类去实现,比如LinearLayout、RelativeLayout、ListView等。为什么ViewGroup不像View一样对其onMeasure()
方法做统一的实现呢?那是因为不同的ViewGroup子类有不同的布局特性,这导致它们的测量细节各不相同,因此ViewGroup无法做统一的实现。
需要注意的是,虽然View实现了onMeasure()
方法,但也只是一种默认实现,前面也提到过View的这种默认实现是不区分wrap_content
和match_parent
的,而View的子类如果需要支持区分实现这两种布局方式,就需要根据自身的特性自定义实现onMeasure()
方法,比如TextView、ImageView等就都实现了onMeasure()
方法,而且实现的方式各不相同,有兴趣的同学可以去看下源码,这里就不细讲了。
DecorView和ViewRootImpl
本来关于View的measure过程到这里已经介绍得七七八八了,但是为了更好的理解整个View树结构的测量过程,这里就先简单提下DecorView和ViewRootImpl这两个家伙。
我们知道,Android界面上的View其实是一个View树结构,而DecorView就是View树的顶端,是视图的顶级View,一般情况下它内部会包含一个竖直方向的LinearLayout,在这个LinearLayout里面有上下两个部分(具体情况和Android版本以及主题有关),上面是标题栏,下面是内容栏。我们在创建Activity时通过setContentView()
添加的布局文件其实就是被加到内容栏之中,而内容栏是一个id为content的FrameLayout,所以可以理解Activity指定布局的方法不叫setView()
而叫setContentView()
了吧。
每一个Activity组件都有一个关联的Window对象,用来描述一个应用程序窗口。每一个应用程序窗口内部又包含有一个View对象,用来描述应用程序窗口的视图。在Activity创建完毕后,DecorView会被添加到Window中,之后我们才能在屏幕上看到应用程序的视图效果。
而ViewRootImpl是连接WindowManager和DecorView的纽带,控件的测量、布局、绘制以及输入事件的分发处理都由ViewRootImpl触发。它是WindowManagerGlobal工作的实际实现者,因此它还需要负责与WMS交互通信以调整窗口的位置大小,以及对来自WMS的事件(如窗口尺寸改变等)作出相应的处理。它调用了一个performTraversals()
方法使得View树开始三大工作流程,然后使得View展现在我们面前。关键源码如下:
private void performTraversals() {
...
if (!mStopped || mReportNextDraw) {
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width); // 1
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
}
}
if (didLayout) {
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
...
}
if (!cancelDraw && !newSurface) {
if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
for (int i = 0; i < mPendingTransitions.size(); ++i) {
mPendingTransitions.get(i).startChangingAnimations();
}
mPendingTransitions.clear();
}
performDraw();
}
...
}
我们看到它里面执行了三个方法,分别是performMeasure()
、performLayout()
、performDraw()
这三个方法,这三个方法分别完成DecorView的measure、layout、和draw这三大流程,其中performMeasure()
中会调用measure()
方法,在measure()
方法中又会调用onMeasure()
方法,在onMeasure()
方法中会对所有子元素进行measure过程,这个时候measure流程就从父容器传递到子元素中了,这样就完成了一次measure过程。接着子元素会重复父容器的measure过程,如此反复就实现了从DecorView开始对整个View树的遍历测量,measure过程就这样完成了。同理,performLayout()
和performDraw()
也是类似的传递流程。针对performTraveals()
的大致流程,可以用以下流程图来表示。
在performTraversals()
方法中,其实对于View树的测量、布局、绘制不是简单地依次单次执行,以上的流程图只是一个为了便于理解而简化版的流程,真正的流程应该分为以下五个工作阶段:
-
预测量阶段:这是进入
performTraversals()
方法后的第一个阶段,它会对View树进行第一次测量。在此阶段中将会计算出View树为显示其内容所需的尺寸,即期望的窗口尺寸。(调用measureHierarchy()
) -
布局窗口阶段:根据预测量的结果,通过
IWindowSession.relayout()
方法向WMS请求调整窗口的尺寸等属性,这将引发WMS对窗口进行重新布局,并将布局结果返回给ViewRootImpl。(调用relayoutWindow()
) -
最终测量阶段:预测量的结果是View树所期望的窗口尺寸。然而由于在WMS中影响窗口布局的因素很多,WMS不一定会将窗口准确地布局为View树所要求的尺寸,而迫于WMS作为系统服务的强势地位,View树不得不接受WMS的布局结果。因此在这一阶段,
performTraversals()
将以窗口的实际尺寸对View树进行最终测量。(调用performMeasure()
) -
布局View树阶段:完成最终测量之后便可以对View树进行布局了。(调用
performLayout()
) -
绘制阶段:这是performTraversals()的最终阶段。确定了控件的位置与尺寸后,便可以对View树进行绘制了。(调用
performDraw()
)
也就是说,实际上多了预测量阶段和布局窗口阶段,这里面还有很多可以讲的,但本文主要是介绍View的measure过程,相关性不大的尽量少涉及,以免太过混乱。
预测量阶段和最终测量阶段都会至少完整测量一次View树,这两个阶段的区别也只是参数不同而已。预测量阶段用到了一个measureHierarchy()
方法,该方法传入的参数desiredWindowWidth与desiredWindowHeight是期望的窗口尺寸。View树本可以按照这两个参数完成测量,但是measureHierarchy()
有自己的考量,即如何将窗口布局地尽可能地优雅。
这是针对将LayoutParams.width设置为了WRAP_CONTENT的悬浮窗口而言。如前文所述,在设置为WRAP_CONTENT时,指定的desiredWindowWidth是应用可用的最大宽度,如此可能会产生下面左图所示的丑陋布局。这种情况较容易发生在AlertDialog中,当AlertDialog需要显示一条比较长的消息时,由于给予的宽度足够大,因此它有可能将这条消息以一行显示,并使得其窗口充满了整个屏幕宽度,在横屏模式下这种布局尤为丑陋。
倘若能够对可用宽度进行适当的限制,迫使AlertDialog将消息换行显示,则产生的布局结果将会优雅得多,如图下面右图所示。但是,倘若不分清红皂白地对宽度进行限制,当控件树真正需要足够的横向空间时,会导致内容无法显示完全,或者无法达到最佳的显示效果。例如当一个悬浮窗口希望尽可能大地显示一张照片时就会出现这样的情况。
image那么measureHierarchy()
如何解决这个问呢?它采取了与View树进行协商的办法,即先使用measureHierarchy()
所期望的宽度限制尝试对View树进行测量,然后通过测量结果来检查View树是否能够在此限制下满足其充分显示内容的要求。倘若没能满足,则measureHierarchy()
进行让步,放宽对宽度的限制,然后再次进行测量,再做检查。倘若仍不能满足则再度进行让步。
关键源码如下:
private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
int childWidthMeasureSpec;
int childHeightMeasureSpec;
// 表示测量结果是否可能导致窗口的尺寸发生变化
boolean windowSizeMayChange = false;
//goodMeasure表示了测量是否能满足View树充分显示内容的要求
boolean goodMeasure = false;
//测量协商仅发生在LayoutParams.width被指定为WRAP_CONTENT的情况下
if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
//第一次协商。measureHierarchy()使用它最期望的宽度限制进行测量。
//这一宽度限制定义为一个系统资源。
//可以在frameworks/base/core/res/res/values/config.xml找到它的定义
final DisplayMetrics packageMetrics = res.getDisplayMetrics();
res.getValue(com.android.internal.R.dimen.config_prefDialogWidth, mTmpValue, true);
// 宽度限制被存放在baseSize中
int baseSize = 0;
if (mTmpValue.type == TypedValue.TYPE_DIMENSION) {
baseSize = (int)mTmpValue.getDimension(packageMetrics);
}
if (baseSize != 0 && desiredWindowWidth > baseSize) {
childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
//第一次测量。调用performMeasure()进行测量
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
//View树的测量结果可以通过mView的getmeasuredWidthAndState()方法获取。
//View树对这个测量结果不满意,则会在返回值中添加MEASURED_STATE_TOO_SMALL位
if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
goodMeasure = true; // 控件树对测量结果满意,测量完成
} else {
//第二次协商。上次的测量结果表明View树认为measureHierarchy()给予的宽度太小,在此
//在此适当地放宽对宽度的限制,使用最大宽度与期望宽度的中间值作为宽度限制
baseSize = (baseSize+desiredWindowWidth)/2;
childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
//第二次测量
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
// 再次检查控件树是否满足此次测量
if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
// 控件树对测量结果满意,测量完成
goodMeasure = true;
}
}
}
}
if (!goodMeasure) {
//最终测量。当View树对上述两次协商的结果都不满意时,measureHierarchy()放弃所有限制
//做最终测量。这一次将不再检查控件树是否满意了,因为即便其不满意,measurehierarchy()也没
//有更多的空间供其使用了
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
//如果测量结果与ViewRootImpl中当前的窗口尺寸不一致,则表明随后可能有必要进行窗口尺寸的调整
if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
windowSizeMayChange = true;
}
}
// 返回窗口尺寸是否可能需要发生变化
return windowSizeMayChange;
}
可以看到,measureHierarchy()
方法最终也是调用了performMeasure()
方法对View树进行测量,只是多了协商测量的过程。
显然,对于非悬浮窗口,即当LayoutParams.width被设置为MATCH_PARENT时,不存在协商过程,直接使用给定的desiredWindowWidth/Height进行测量即可。而对于悬浮窗口,measureHierarchy()
可以连续进行两次让步,从而导致View的onMeasure()
方法多次被调用。
这里也看到,在View的measure过程中设置的MEASURED_STATE_TOO_SMALL标志位就在测量协商过程中起作用了。
image总结
看到了这里,我们发现Android中View的measure过程是很巧妙的,知道如何利用以前测量过的数据,如果情况有变,那么就调用onMeasure()
方法进行实际的测量工作。真正实现对View本身的测量就是在onMeasure()
中,在该方法中View要根据父ViewGroup给其传递进来的widthMeasureSpec和heightMeasureSpec,并结合View自身想要的尺寸,综合考虑,计算出最终的宽度和高度,并存储到相应的成员变量中,这才标志着该View测量有效的完成了,如果没有将值存入到成员变量中,View会抛出异常。而且在成员变量中还储藏着测量的状态信息state,该信息表示了View对此次测量的结果是否满意。而这个state信息有可能会在ViewRootImpl在做窗口大小决策的时候提供反馈,从而达到最佳的显示效果。
网友评论