Android中的Veiw从内存中到呈现在UI界面上需要依次经历三个阶段:量算 -> 布局 -> 绘图,关于View的量算、布局、绘图的总体机制可参见博文《 Android中View的布局及绘图机制》。量算是布局和绘图的基础,所以量算是很重要的一个环节。本文将从源码角度解析View的量算过程,这其中会涉及某些关键类以及关键方法。
对View进行量算的目的是让View的父控件知道View想要多大的尺寸。
量算过程概述
如果要进行量算的View是ViewGroup类型,那么ViewGroup会在onMeasure方法内会遍历子View依次进行量算,本文重点说明非ViewGroup的View的量算过程,因为我们一旦了解了非ViewGroup的View的量算过程,ViewGroup的量算理解起来就要简单许多,主要是ViewGroup在其内部对子View再依次执行量算。
整个应用量算的起点是ViewRootImpl类,从它开始依次对子View进行量算,如果子View是一个ViewGroup,那么又会遍历该ViewGroup的子View依次进行量算。也就是说,量算会从View树的根结点,纵向递归进行,从而实现自上而下对View树进行量算,直至完成对叶子节点View的量算。
那么到底如何对一个View进行量算呢?Android通过调用View的measure()方法对View进行量算,让该View的父控件知道该View想要多大的尺寸空间。
具体来说,View的父控件ViewGroup会调用View的measure方法,ViewGroup会将一些宽度和高度的限制条件传递给View的measure方法。
在View的measure方法会首先从成员变量中读取以前缓存过的量算结果,如果能找到该缓存值,那么就基本完事了,如果没有找到缓存值,那么measure方法会执行onMeasure回调方法,measure方法会将上述的宽度和高度的限制条件依次传递给onMeasure方法。onMeasure方法会完成具体的量算工作,并将量算的结果通过调用View的setMeasuredDimension方法保存到View的成员变量mMeasuredWidth 和mMeasuredHeight中。
量算完成之后,View的父控件就可以通过调用getMeasuredWidth、getMeasuredState、getMeasuredWidthAndState这三个方法获取View的量算结果。
以上就是非ViewGroup类型的View量算的总体过程。
MeasureSpec简介
上面我们提到ViewGroup在调用View的measure方法时,会传入ViewGroup对View的宽度及高度的限制条件,这是合理的,例如ViewGroup的空间有限,它需要告诉子View要量算的尺寸的上限。
上面提到的尺寸的限制条件就是MeasureSpec,它可以通过一个Int类型的值来表示的,该Int值会同时包含两种信息:mode和size,即模式和尺寸。我们知道Java中Int类型的值是4个字节的,Android会用第一个高位字节存储mode,然后用剩余的三个字节存储size。
View有一个静态内部类MeasureSpec,该类有几个静态方法以及静态常量,我们可以用这些方法将mode和size打包成一个Int值或者是从一个Int值中解析出mode和size。
假设我们已有了一个包含MeasureSpec信息的Int值measureSpec,那么
通过调用MeasureSpec.getSize(int measureSpec)即可从measureSpec解析出三个字节所包含的尺寸size信息,该方法返回Int类型,也就是说我们得到的size实际上就是对原有的measureSpec的高位字节的8个二进制位都设置为0,该方法的返回值size虽然也是4个字节的Int值,但是已经完全不包含mode信息。
通过调用MeasureSpec.getMode(int measureSpec)即可从measureSpec解析出高位字节所包含的模式mode信息,该方法返回Int类型,也就是说我们得到的mode实际上对原有的measureSpec的低位的三个字节的24个二进制码都设置为0,该方法的返回值mode虽然也是4个字节的Int值,但是已经完全不包含size信息。
对于尺寸size,我们很好理解,比如表示某个宽度值或者表示某个高度值。那么mode是什么呢?
mode的取值有三种,分别是:
MeasureSpec.AT_MOST,即0x80000000,该值表示View最大可以取其父ViewGroup给其指定的尺寸,例如现在有个Int值widthMeasureSpec,ViewGroup将其传递给了View的measure方法,如果widthMeasureSpec中的mode值是AT_MOST,size是200,那么表示View能取的最大的宽度是200。
MeasureSpec.EXACTLY,即0x40000000,该值表示View必须使用其父ViewGroup指定的尺寸,还是以widthMeasureSpec为例,如果其mode值是EXACTLY,size是200,那么表示View的宽度必须是200,不多不少才行。
MeasureSpec.UNSPECIFIED,即0x00000000,该值表示View的父ViewGroup没有给View在尺寸上设置限制条件,这种情况下View可以忽略measureSpec中的size,View可以取自己想要的值作为量算的尺寸。
更多信息可参考API文档 android/view/View.MeasureSpec。
measure方法
measure()的方法签名是public final void measure(int widthMeasureSpec, int heightMeasureSpec)
。
当View的父控件ViewGroup对View进行量算时,会调用View的measure方法,ViewGroup会传入widthMeasureSpec和heightMeasureSpec,分别表示父控件对View的宽度和高度的一些限制条件。
measure方法的源码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 | <code class = "hljs" cs= "" > 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下量算过的结果 //如果mMeasureCache为空,我们就新new一个对象赋值给mMeasureCache 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。 int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? - 1 : mMeasureCache.indexOfKey(key); //sIgnoreMeasureCache是一个boolean类型的成员变量,其值是在View的构造函数中计算的,而且只计算一次 //一些老的App希望在一次layou过程中,onMeasure方法总是被调用, //具体来说其值是通过如下计算的: sIgnoreMeasureCache = targetSdkVersion < KITKAT; //也就是说如果targetSdkVersion的API版本低于KITKAT,即API level小于19,那么sIgnoreMeasureCache为true if (cacheIndex < 0 || sIgnoreMeasureCache) { //如果运行到此处,表示我们没有从缓存中找到量算过的尺寸或者是sIgnoreMeasureCache为true导致我们要忽略缓存结果 //此处调用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()); } mPrivateFlags |= PFLAG_LAYOUT_REQUIRED; } //mOldWidthMeasureSpec和mOldHeightMeasureSpec保存着最近一次量算时的MeasureSpec, //在量算完成后将这次新传入的MeasureSpec赋值给它们 mOldWidthMeasureSpec = widthMeasureSpec; mOldHeightMeasureSpec = heightMeasureSpec; //最后用上面计算出的key作为键,量算结果作为值,将该键值对放入成员变量mMeasureCache中, //这样就实现了对本次量算结果的缓存,以便在下次measure方法执行的时候,有可能将其从中直接读出, //从而省去实际量算的步骤 mMeasureCache.put(key, (( long ) mMeasuredWidth) << 32 | ( long ) mMeasuredHeight & 0xffffffffL); }</code> |
上面的注释对每行代码都进行了详细的说明,如果大家仔细读了的话,相信能一目了然,这里根据上面的注释简单总结一下measure方法都干了什么事:
首先,我们要知道并不是只要View的measure方法执行的时候View就一定要傻傻的真的去做量算工作,View也喜欢偷懒,如果View发现没有必要去量算的话,那它就不会真的去做量算的工作。
具体来说,View先查看是不是要强制量算以及这次measure中传入的MeasureSpec与上次量算的MeasureSpec是否相同,如果不是强制量算或者MeasureSpec与上次的量算的MeasureSpec相同,那么View就不必真的去量算了。
如果不满足上述条件,View就考虑去做量算工作。但是在量算之前,View还想偷懒,它会以MeasureSpec计算出的key值作为键,去成员变量mMeasureCache中查找是否缓存过对应key的量算结果,如果能找到,那么就简单调用一下setMeasuredDimensionRaw方法,将从缓存中读到的量算结果保存到成员变量mMeasuredWidth和mMeasuredHeight中。
如果不能从mMeasureCache中读到缓存过的量算结果,那么这次View就真的不能再偷懒了,只能乖乖地调用onMeasure方法去完成实际的量算工作,并且将尺寸限制条件widthMeasureSpec和heightMeasureSpec传递给onMeasure方法。关于onMeasure方法,我们会在下面详细介绍。
不论上面代码走了哪个判断的分支,最终View都会得到量算的结果,并且将结果缓存到成员变量mMeasureCache中,以便下次执行measure方法时能够从其中读取缓存值。
需要说明的是,View有一个成员变量mPrivateFlags,用以保存View的各种状态位,在量算开始前,会将其设置为未量算状态,在量算完成后会将其设置为已量算状态。
onMeasure方法
我们在上面提到,当View在measure方法中发现不得不进行实际的量算工作时,将会调用onMeasure方法,并且将尺寸限制条件widthMeasureSpec和heightMeasureSpec作为参数传递给onMeasure方法。View的onMeasure方法不是空方法,它提供了一个默认的具体实现。
onMeasure方法的代码如下:
1 2 3 4 5 6 7 | <code class = "hljs" cs= "" > protected void onMeasure( int widthMeasureSpec, int heightMeasureSpec) { //onMeasure调用了setMeasuredDimension方法, //setMeasuredDimension又需要调用getDefaultSize方法, //getDefaultSize又需要调用getSuggestedMinimumWidth和getSuggestedMinimumHeight方法 setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); }</code> |
我们发现onMeasure方法中会调用setMeasuredDimension方法,setMeasuredDimension又需要调用getDefaultSize方法,getDefaultSize又需要调用getSuggestedMinimumWidth和getSuggestedMinimumHeight方法,即
setMeasuredDimension -> getDefaultSize -> getSuggestedMinimumWidth/Height
那我们就先研究getSuggestedMinimumWidth/Height,然后再依次研究getDefaultSize和setMeasuredDimension,这样就能把onMeasure方法搞明白了。其实getSuggestedMinimumWidth和getSuggestedMinimumHeight的实现逻辑基本一样,我们此处只研究getSuggestedMinimumWidth方法即可。
getSuggestedMinimumWidth方法
getSuggestedMinimumWidth用于返回View推荐的最小宽度,其代码如下所示:
1 2 3 4 5 | <code class = "hljs" cs= "" > protected int getSuggestedMinimumWidth() { //如果没有给View设置背景,那么就返回View本身的最小宽度mMinWidth //如果给View设置了背景,那么就取View本身最小宽度mMinWidth和背景的最小宽度的最大值 return (mBackground == null ) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth()); }</code> |
如果没有给View设置背景,那么就返回View本身的最小宽度mMinWidth
如果给View设置了背景,那么就取View本身最小宽度mMinWidth和背景的最小宽度的最大值
那你可能有疑问,View中保存的最小宽度mMinWidth的值是从哪来的呢?实际上有两种办法给View设置最小宽度。
第一种情况是,mMinWidth是在View的构造函数中被赋值的,View通过读取XML中定义的minWidth的值来设置View的最小宽度mMinWidth,以下代码片段是View构造函数中解析minWidth的部分:
1 2 3 4 5 | <code bash= "" class = "hljs" > //遍历到XML中定义的minWith属性 case R.styleable.View_minWidth: //读取XML中定义的属性值作为mMinWidth,如果XML中未定义,则设置为0 mMinWidth = a.getDimensionPixelSize(attr, 0 ); break ;</code> |
第二种情况是调用View的setMinimumWidth方法给View的最小宽度mMinWidth赋值,setMinimumWidth方法的代码如下所示:
1 2 3 4 | <code class = "hljs" cs= "" > public void setMinimumWidth( int minWidth) { mMinWidth = minWidth; requestLayout(); }</code> |
这样我们就搞明白了getSuggestedMinimumWidth方法是怎么执行的了,getSuggestedMinimumHeight方法与其逻辑完全一致,只不过是把宽度换成了高度,在此就不再赘述了。
getDefaultSize
我们在onMeasure方法中发现,onMeasure会执行以下两行代码:getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec)
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)
我们已经研究了getSuggestedMinimumWidth/Height,知道其会返回View的最小宽度和高度,现在我们开始研究getDefaultSize方法。
Android会将View想要的尺寸以及其父控件对其尺寸限制信息measureSpec传递给getDefaultSize方法,该方法要根据这些综合信息计算最终的量算的尺寸。
其源码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | <code class = "hljs" cs= "" > public static int getDefaultSize( int size, int measureSpec) { //size表示的是View想要的尺寸信息,比如最小宽度或最小高度 int result = size; //从measureSpec中解析出specMode信息 int specMode = MeasureSpec.getMode(measureSpec); //从measureSpec中解析出specSize信息,不要将specSize与上面的size变量搞混 int specSize = MeasureSpec.getSize(measureSpec); switch (specMode) { //如果mode是UNSPECIFIED,表示View的父ViewGroup没有给View在尺寸上设置限制条件 case MeasureSpec.UNSPECIFIED: //此处当mode是UNSPECIFIED时,View就直接用自己想要的尺寸size作为量算的结果 result = size; break ; //如果mode是UNSPECIFIED,那么表示View最大可以取其父ViewGroup给其指定的尺寸 //如果mode是EXACTLY,那么表示View必须使用其父ViewGroup指定的尺寸 case MeasureSpec.AT_MOST: case MeasureSpec.EXACTLY: //此处mode是UNSPECIFIED或EXACTLY时,View就用其父ViewGroup指定的尺寸作为量算的结果 result = specSize; break ; } return result; }</code> |
通过以上代码,我们就会发现View的父ViewGroup传递给View的限制条件measureSpec的作用在该方法中体现的淋漓尽致。
首先根据measuredSpec解析出对应的specMode和specSize
当mode是UNSPECIFIED时,View就直接用自己想要的尺寸size作为量算的结果
当mode是UNSPECIFIED或EXACTLY时,View就用其父ViewGroup指定的尺寸作为量算的结果
最终,View会根据measuredSpec限制条件,得到最终的量算的尺寸。
这样在onMeasure方法中,
当执行getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec)时,我们就得到了最终量算到的宽度值;
当执行getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)时,我们就得到了最终量算到的高度值。
setMeasuredDimension
在前面我们研究onMeasure方法时就已经看到setMeasuredDimension会调用getDefaultSize方法,会将已经量算到的宽度值和高度值作为参数传递给setMeasuredDimension方法,我们研究一下该方法。
其源码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <code class = "hljs" java= "" > protected final void setMeasuredDimension( int measuredWidth, int measuredHeight) { boolean optical = isLayoutModeOptical( this ); if (optical != isLayoutModeOptical(mParent)) { //layoutMode是LAYOUT_MODE_OPTICAL_BOUNDS的特殊情况,我们不考虑 Insets insets = getOpticalInsets(); int opticalWidth = insets.left + insets.right; int opticalHeight = insets.top + insets.bottom; measuredWidth += optical ? opticalWidth : -opticalWidth; measuredHeight += optical ? opticalHeight : -opticalHeight; } //最终调用setMeasuredDimensionRaw方法,将量算结果传入进去 setMeasuredDimensionRaw(measuredWidth, measuredHeight); }</code> |
该方法会在开始判断layoutMode是不是LAYOUT_MODE_OPTICAL_BOUNDS的特殊情况,这种特例很少见,我们直接忽略掉。
setMeasuredDimension方法最后将量算的结果传递给方法setMeasuredDimensionRaw,我们再研究一下setMeasuredDimensionRaw这方法。
setMeasuredDimensionRaw
setMeasuredDimensionRaw接收两个参数,分别是已经量算完成的宽度和高度。
其源码如下所示:
1 2 3 4 5 6 7 8 | <code class = "hljs" cs= "" > private void setMeasuredDimensionRaw( int measuredWidth, int measuredHeight) { //将量算完成的宽度measuredWidth保存到View的成员变量mMeasuredWidth中 mMeasuredWidth = measuredWidth; //将量算完成的高度measuredHeight保存到View的成员变量mMeasuredHeight中 mMeasuredHeight = measuredHeight; //最后将View的状态位mPrivateFlags设置为已量算状态 mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET; }</code> |
我们发现,在该方法中做了三件事:
将量算完成的宽度measuredWidth保存到View的成员变量mMeasuredWidth中
将量算完成的高度measuredHeight保存到View的成员变量mMeasuredHeight中
最后将View的状态位mPrivateFlags设置为已量算状态
量算完成的尺寸的state
至此,View的量算过程就完成了,但是View的父ViewGroup如何读取到View量算的结果呢?
为此,View提供了三组方法,分别是:
1. getMeasuredWidth和getMeasuredHeight方法
2. getMeasuredWidthAndState和getMeasuredHeightAndState方法
3. getMeasuredState方法
有些人可能会纳闷,只要有了第一组方法不就行了吗?后面那两组方法有啥用?
此处我们要再仔细研究一下View中保存量算结果的成员变量mMeasuredWidth和mMeasuredHeight,下面的讨论我们都只讨论宽度,理解了宽度的处理方式,高度也是完全一样的。
mMeasuredWidth是一个Int类型的值,其是由4个字节组成的。
我们先假设mMeasuredWidth只存储了量算完成的宽度信息,而且View的父ViewGroup可以通过相关方法得到该值。但是存在这样一种情况:View在量算时,父ViewGroup给其传递的widthMeasureSpec中的specMode的值是AT_MOST,specSize是100,但是View的最小宽度是200,显然父ViewGroup指定的specSize不能满足View的大小,但是由于specMode的值是AT_MOST,View在getDefaultSize方法中不得不妥协,只能含泪将量算的最终宽度设置为100。然后其父ViewGroup通过某些方法获取到该View的量算宽度为100时,ViewGroup以为子View只需要100就够了,最终给了子View宽度为100的空间,这就导致了在UI界面上View特别窄,用户体验也就不好。
Android为让其View的父控件获取更多的信息,就在mMeasuredWidth上下了很大功夫,虽然是一个Int值,但是想让它存储更多信息,具体来说就是把mMeasuredWidth分成两部分:
其高位的第一个字节为第一部分,用于标记量算完的尺寸是不是达到了View想要的宽度,我们称该信息为量算的state信息。 其低位的三个字节为第二部分,用于存储实际的量算到的宽度。
由此我们可以看出Android真是物尽其用,一个变量能包含两个信息,这个有点类似于measureSpec的道理,但是二者又有不同:
measureSpec是将限制条件mode从ViewGroup传递给其子View。 mMeasuredWidth、mMeasuredHeight是将带有量算结果的state标志位信息从View传递给其父ViewGroup。
那么你可能会问,在本文中我们没看到对mMeasuredWidth的高位字节进行特殊处理啊?我们下面看一下View中的resolveSizeAndState方法。
resolveSizeAndState
resolveSizeAndState方法与getDefaultSize方法类似,其内部实现的逻辑是一样的,但是又有区别,getDefaultSize仅仅返回最终量算的尺寸信息,但resolveSizeAndState除了返回最终尺寸信息还会有可能返回量算的state标志位信息。
resolveSizeAndState方法的源码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | <code class = "hljs" java= "" > 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: if (specSize < size) { //当specMode为AT_MOST,并且父控件指定的尺寸specSize小于View自己想要的尺寸时, //我们就会用掩码MEASURED_STATE_TOO_SMALL向量算结果加入尺寸太小的标记 //这样其父ViewGroup就可以通过该标记其给子View的尺寸太小了, //然后可能分配更大一点的尺寸给子View result = specSize | MEASURED_STATE_TOO_SMALL; } else { result = size; } break ; case MeasureSpec.EXACTLY: result = specSize; break ; case MeasureSpec.UNSPECIFIED: default : result = size; } return result | (childMeasuredState & MEASURED_STATE_MASK); }</code> |
当specMode为AT_MOST,并且父控件指定的尺寸specSize小于View自己想要的尺寸时,我们就会用掩码MEASURED_STATE_TOO_SMALL向量算结果加入尺寸太小的标记,这样其父ViewGroup就可以通过该标记其给子View的尺寸太小了,然后可能分配更大一点的尺寸给子View。
getDefaultSize方法只是onMeasure方法中获取最终尺寸的默认实现,其返回的信息比resolveSizeAndState要少,那么什么时候才会调用resolveSizeAndState方法呢? 主要有两种情况:
Android中的许多layout类都调用了resolveSizeAndState方法,比如LinearLayout在量算过程中会调用resolveSizeAndState方法而非getDefaultSize方法。 我们自己在实现自定义的View或ViewGroup时,我们可以重写onMeasure方法,并在该方法内调用resolveSizeAndState方法。
getMeasuredXXX系列方法
现在我们再回过头来看以下三组方法:
getMeasuredWidth和getMeasuredHeight方法
该组方法只返回量算结果中的的尺寸信息,去掉了高位字节的state信息,以getMeasuredWidth方法为例,其源码如下:
1 2 3 4 5 | <code class = "hljs" java= "" > public final int getMeasuredWidth() { //MEASURED_SIZE_MASK的值为0x00ffffff,用mMeasuredWidth与掩码MEASURED_SIZE_MASK进行按位与运算, //可以将返回值中的高位字节的8个bit位全置为0,从而去掉了高位字节的state信息 return mMeasuredWidth & MEASURED_SIZE_MASK; }</code> |
MEASURED_SIZE_MASK的值为0x00ffffff,用mMeasuredWidth与掩码MEASURED_SIZE_MASK进行按位与运算,可以将返回值中的高位字节的8个bit位全置为0,从而去掉了高位字节的state信息
getMeasuredWidthAndState和getMeasuredHeightAndState方法
该组方法返回的量算结果中同时包含尺寸和state信息(如果state存在的话),以getMeasuredWidthAndState方法为例,其源码如下所示:
1 2 3 4 | <code class = "hljs" java= "" > public final int getMeasuredWidthAndState() { //该方法直接返回成员变量mMeasuredWidth,因为mMeasuredWidth本身已经包含了尺寸以及可能的state信息 return mMeasuredWidth; }</code> |
该方法直接返回成员变量mMeasuredWidth,因为mMeasuredWidth本身已经包含了尺寸以及可能的state信息
getMeasuredState方法
该方法返回的Int值中同时包含宽度量算的state以及高度量算的state,不包含任何的尺寸信息,其源码如下所示:
1 2 3 4 5 6 7 | <code class = "hljs" java= "" > 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)); }</code> |
我们简单分析一下以上代码:
掩码MEASURED_STATE_MASK的值为常量0xff000000,其高位字节的8个bit位全为1,剩余低位字节的三个字节的24个bit位全为0
MEASURED_HEIGHT_STATE_SHIFT的值为常量16
当执行(mMeasuredWidth&MEASURED_STATE_MASK)时,将mMeasuredWidth与MEASURED_STATE_MASK进行按位与操作,该表达式的值高位字节保留了量算后宽度的state,过滤掉了其低位三个字节所存储的宽度size
由于我们已经用高位首字节存储了量算后宽度的state,所以高度的state就不能存储在高位首字节了。Android打算把它存储在第三个字节中。(mMeasuredHeight>>MEASURED_HEIGHT_STATE_SHIFT)表示将mMeasuredHeight向右移16位,这样高度的state字节就从原来的第一个字节右移动到了第三个字节,由于高度的state向右移动了,所以其对应的掩码也有相应移动。(MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT)表示state的掩码也从第一个字节右移16位到了第三个字节,即掩码从0xff000000变成了0x0000ff00。然后用右移后的state与右移后的掩码执行按位与操作,这样就在第三个字节保留了高度的state信息,并且过滤掉了第1、2、4字节中的信息,即将这三个字节中的24个bit位置为0。
最后,将我们得到的宽度的state与高度的state进行按位或操作,这样就将宽度和高度的state都保存在一个Int值中:第一个字节存储宽度的state,第三个字节存储高度的state。