Android View 测量流程(Measure)全面解析
前言
文章,笔者主要讲述了decorview以及viewrootimpl相关的作用,这里回顾一下上一章所说的内容:decorview是视图的*view,我们添加的布局文件是它的一个子布局,而viewrootimpl则负责渲染视图,它调用了一个performtraveals方法使得viewtree开始三大工作流程,然后使得view展现在我们面前。本篇文章主要内容是:详细讲述view的测量(measure)流程,主要以源码的形式呈现,源码均取自android api 21.
从viewrootimpl#performtraveals说起
我们直接从这个方法说起,因为它是整个工作流程的核心,我们看看它的源码:
private void performtraversals() { ... if (!mstopped) { 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 (!skipdraw || mreportnextdraw) { if (mpendingtransitions != null && mpendingtransitions.size() > 0) { for (int i = 0; i < mpendingtransitions.size(); ++i) { mpendingtransitions.get(i).startchanginganimations(); } mpendingtransitions.clear(); } performdraw(); } } ... }
方法非常长,这里做了精简,我们看到它里面主要执行了三个方法,分别是performmeasure、performlayout、performdraw这三个方法,在这三个方法内部又会分别调用measure、layout、draw这三个方法来进行不同的流程。我们先来看看performmeasure(childwidthmeasurespec, childheightmeasurespec)这个方法,它传入两个参数,分别是childwidthmeasurespec和childheightmeasure,那么这两个参数代表什么意思呢?要想了解这两个参数的意思,我们就要先了解measurespec。
理解measurespec
measurespec是view类的一个内部类,我们先看看官方文档对measurespec类的描述:a measurespec encapsulates the layout requirements passed from parent to child. each measurespec represents a requirement for either the width or the height. a measurespec is comprised of a size and a mode.它的意思就是说,该类封装了一个view的规格尺寸,包括view的宽和高的信息,但是要注意,measurespec并不是指view的测量宽高,这是不同的,是根据measuespec而测出测量宽高。
measurespec的作用在于:在measure流程中,系统会将view的layoutparams根据父容器所施加的规则转换成对应的measurespec,然后在onmeasure方法中根据这个measurespec来确定view的测量宽高。
我们来看看这个类的源码:
public static class measurespec { private static final int mode_shift = 30; private static final int mode_mask = 0x3 << mode_shift; /** * unspecified 模式: * 父view不对子view有任何限制,子view需要多大就多大 */ public static final int unspecified = 0 << mode_shift; /** * exactyly 模式: * 父view已经测量出子viwe所需要的精确大小,这时候view的最终大小 * 就是specsize所指定的值。对应于match_parent和精确数值这两种模式 */ public static final int exactly = 1 << mode_shift; /** * at_most 模式: * 子view的最终大小是父view指定的specsize值,并且子view的大小不能大于这个值, * 即对应wrap_content这种模式 */ public static final int at_most = 2 << mode_shift; //将size和mode打包成一个32位的int型数值 //高2位表示specmode,测量模式,低30位表示specsize,某种测量模式下的规格大小 public static int makemeasurespec(int size, int mode) { if (susebrokenmakemeasurespec) { return size + mode; } else { return (size & ~mode_mask) | (mode & mode_mask); } } //将32位的measurespec解包,返回specmode,测量模式 public static int getmode(int measurespec) { return (measurespec & mode_mask); } //将32位的measurespec解包,返回specsize,某种测量模式下的规格大小 public static int getsize(int measurespec) { return (measurespec & ~mode_mask); } //... }
可以看出,该类的思路是相当清晰的,对于每一个view,包括decorview,都持有一个measurespec,而该measurespec则保存了该view的尺寸规格。在view的测量流程中,通过makemeasurespec来保存宽高信息,在其他流程通过getmode或getsize得到模式和宽高。那么问题来了,上面提到measurespec是layoutparams和父容器的模式所共同影响的,那么,对于decorview来说,它已经是顶层view了,没有父容器,那么它的measurespec怎么来的呢?
为了解决这个疑问,我们回到viewrootimpl#performtraveals方法,看①号代码处,调用了getrootmeasurespec(desiredwindowwidth,lp.width)方法,其中desiredwindowwidth就是屏幕的尺寸,并把返回结果赋值给childwidthmeasurespec成员变量(childheightmeasurespec同理),因此childwidthmeasurespec(childheightmeasurespec)应该保存了decorview的measurespec,那么我们看一下viewrootimpl#getrootmeasurespec方法的实现:
/** * @param windowsize * the available width or height of the window * * @param rootdimension * the layout params for one dimension (width or height) of the * window. * * @return the measure spec to use to measure the root view. */ private static int getrootmeasurespec(int windowsize, int rootdimension) { int measurespec; switch (rootdimension) { case viewgroup.layoutparams.match_parent: // window can't resize. force root view to be windowsize. measurespec = measurespec.makemeasurespec(windowsize, measurespec.exactly); break; //省略... } return measurespec; }
思路也很清晰,根据不同的模式来设置measurespec,如果是layoutparams.match_parent模式,则是窗口的大小,wrap_content模式则是大小不确定,但是不能超过窗口的大小等等。
那么到目前为止,就已经获得了一份decorview的measurespec,它代表着根view的规格、尺寸,在接下来的measure流程中,就是根据已获得的根view的measurespec来逐层测量各个子view。我们顺着①号代码往下走,来到performmeasure方法,看看它做了什么工作,viewrootimpl#performmeasure:
private void performmeasure(int childwidthmeasurespec, int childheightmeasurespec) { trace.tracebegin(trace.trace_tag_view, "measure"); try { mview.measure(childwidthmeasurespec, childheightmeasurespec); } finally { trace.traceend(trace.trace_tag_view); } }
方法很简单,直接调用了mview.measure,这里的mview就是decorview,也就是说,从*view开始了测量流程,那么我们直接进入measure流程。
measure 测量流程
viewgroup的测量流程
由于decorview继承自framelayout,是phonewindow的一个内部类,而framelayout没有measure方法,因此调用的是父类view的measure方法,我们直接看它的源码,view#measure:
public final void measure(int widthmeasurespec, int heightmeasurespec) { boolean optical = islayoutmodeoptical(this); if (optical != islayoutmodeoptical(mparent)) { ... if ((mprivateflags & pflag_force_layout) == pflag_force_layout || widthmeasurespec != moldwidthmeasurespec || heightmeasurespec != moldheightmeasurespec) { ... if (cacheindex < 0 || signoremeasurecache) { // measure ourselves, this should set the measured dimension flag back onmeasure(widthmeasurespec, heightmeasurespec); mprivateflags3 &= ~pflag3_measure_needed_before_layout; } ... }
可以看到,它在内部调用了onmeasure方法,由于decorview是framelayout子类,因此它实际上调用的是decorview#onmeasure方法。在该方法内部,主要是进行了一些判断,这里不展开来看了,到最后会调用到super.onmeasure方法,即framelayout#onmeasure方法。
由于不同的viewgroup有着不同的性质,那么它们的onmeasure必然是不同的,因此这里不可能把所有布局方式的onmeasure方法都分析一遍,因此这里选择了framelayout的onmeasure方法来进行分析,其它的布局方式读者可以自行分析。那么我们继续来看看这个方法:
@override protected void onmeasure(int widthmeasurespec, int heightmeasurespec) { //获取当前布局内的子view数量 int count = getchildcount(); //判断当前布局的宽高是否是match_parent模式或者指定一个精确的大小,如果是则置measurematchparent为false. final boolean measurematchparentchildren = measurespec.getmode(widthmeasurespec) != measurespec.exactly || measurespec.getmode(heightmeasurespec) != measurespec.exactly; mmatchparentchildren.clear(); int maxheight = 0; int maxwidth = 0; int childstate = 0; //遍历所有类型不为gone的子view for (int i = 0; i < count; i++) { final view child = getchildat(i); if (mmeasureallchildren || child.getvisibility() != gone) { //对每一个子view进行测量 measurechildwithmargins(child, widthmeasurespec, 0, heightmeasurespec, 0); final layoutparams lp = (layoutparams) child.getlayoutparams(); //寻找子view中宽高的最大者,因为如果framelayout是wrap_content属性 //那么它的大小取决于子view中的最大者 maxwidth = math.max(maxwidth, child.getmeasuredwidth() + lp.leftmargin + lp.rightmargin); maxheight = math.max(maxheight, child.getmeasuredheight() + lp.topmargin + lp.bottommargin); childstate = combinemeasuredstates(childstate, child.getmeasuredstate()); //如果framelayout是wrap_content模式,那么往mmatchparentchildren中添加 //宽或者高为match_parent的子view,因为该子view的最终测量大小会受到framelayout的最终测量大小影响 if (measurematchparentchildren) { if (lp.width == layoutparams.match_parent || lp.height == layoutparams.match_parent) { mmatchparentchildren.add(child); } } } } // account for padding too maxwidth += getpaddingleftwithforeground() + getpaddingrightwithforeground(); maxheight += getpaddingtopwithforeground() + getpaddingbottomwithforeground(); // check against our minimum height and width maxheight = math.max(maxheight, getsuggestedminimumheight()); maxwidth = math.max(maxwidth, getsuggestedminimumwidth()); // check against our foreground's minimum height and width final drawable drawable = getforeground(); if (drawable != null) { maxheight = math.max(maxheight, drawable.getminimumheight()); maxwidth = math.max(maxwidth, drawable.getminimumwidth()); } //保存测量结果 setmeasureddimension(resolvesizeandstate(maxwidth, widthmeasurespec, childstate), resolvesizeandstate(maxheight, heightmeasurespec, childstate << measured_height_state_shift)); //子view中设置为match_parent的个数 count = mmatchparentchildren.size(); //只有framelayout的模式为wrap_content的时候才会执行下列语句 if (count > 1) { for (int i = 0; i < count; i++) { final view child = mmatchparentchildren.get(i); final marginlayoutparams lp = (marginlayoutparams) child.getlayoutparams(); //对framelayout的宽度规格设置,因为这会影响子view的测量 final int childwidthmeasurespec; /** * 如果子view的宽度是match_parent属性,那么对当前framelayout的measurespec修改: * 把widthmeasurespec的宽度规格修改为:总宽度 - padding - margin,这样做的意思是: * 对于子viw来说,如果要match_parent,那么它可以覆盖的范围是framelayout的测量宽度 * 减去padding和margin后剩下的空间。 * * 以下两点的结论,可以查看getchildmeasurespec()方法: * * 如果子view的宽度是一个确定的值,比如50dp,那么framelayout的widthmeasurespec的宽度规格修改为: * specsize为子view的宽度,即50dp,specmode为exactly模式 * * 如果子view的宽度是wrap_content属性,那么framelayout的widthmeasurespec的宽度规格修改为: * specsize为子view的宽度减去padding减去margin,specmode为at_most模式 */ if (lp.width == layoutparams.match_parent) { final int width = math.max(0, getmeasuredwidth() - getpaddingleftwithforeground() - getpaddingrightwithforeground() - lp.leftmargin - lp.rightmargin); childwidthmeasurespec = measurespec.makemeasurespec( width, measurespec.exactly); } else { childwidthmeasurespec = getchildmeasurespec(widthmeasurespec, getpaddingleftwithforeground() + getpaddingrightwithforeground() + lp.leftmargin + lp.rightmargin, lp.width); } //同理对高度进行相同的处理,这里省略... //对于这部分的子view需要重新进行measure过程 child.measure(childwidthmeasurespec, childheightmeasurespec); } } }
由以上的framelayout的onmeasure过程可以看出,它还是做了相当多的工作的,这里简单总结一下:首先,framelayout根据它的measurespec来对每一个子view进行测量,即调用measurechildwithmargin方法,这个方法下面会详细说明;对于每一个测量完成的子view,会寻找其中最大的宽高,那么framelayout的测量宽高会受到这个子view的最大宽高的影响(wrap_content模式),接着调用setmeasuredimension方法,把framelayout的测量宽高保存。最后则是特殊情况的处理,即当framelayout为wrap_content属性时,如果其子view是match_parent属性的话,则要重新设置framelayout的测量规格,然后重新对该部分view测量。
在上面提到setmeasuredimension方法,该方法用于保存测量结果,在上面的源码里面,该方法的参数接收的是resolvesizeandstate方法的返回值,那么我们直接看view#resolvesizeandstate方法:
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) { 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); }
可以看到该方法的思路是相当清晰的,当specmode是exactly时,那么直接返回measurespec里面的宽高规格,作为最终的测量宽高;当specmode时at_most时,那么取measurespec的宽高规格和size的最小值。(注:这里的size,对于framelayout来说,是其最大子view的测量宽高)。
小结:那么到目前为止,以decorview为切入点,把viewgroup的测量流程详细地分析了一遍,在viewrootimpl#performtraversals中获得decorview的尺寸,然后在performmeasure方法中开始测量流程,对于不同的layout布局有着不同的实现方式,但大体上是在onmeasure方法中,对每一个子view进行遍历,根据viewgroup的measurespec及子view的layoutparams来确定自身的测量宽高,然后最后根据所有子view的测量宽高信息再确定父容器的测量宽高。
那么接下来,我们继续分析对于一个子view来说,是怎么进行测量的。
view的测量流程
还记得我们上面在framelayout测量内提到的measurechildwithmargin方法,它接收的主要参数是子view以及父容器的measurespec,所以它的作用就是对子view进行测量,那么我们直接看这个方法,viewgroup#measurechildwithmargins:
protected void measurechildwithmargins(view child, int parentwidthmeasurespec, int widthused, int parentheightmeasurespec, int heightused) { final marginlayoutparams lp = (marginlayoutparams) child.getlayoutparams(); final int childwidthmeasurespec = getchildmeasurespec(parentwidthmeasurespec, mpaddingleft + mpaddingright + lp.leftmargin + lp.rightmargin + widthused, lp.width); final int childheightmeasurespec = getchildmeasurespec(parentheightmeasurespec, mpaddingtop + mpaddingbottom + lp.topmargin + lp.bottommargin + heightused, lp.height); child.measure(childwidthmeasurespec, childheightmeasurespec); // 1 }
由源码可知,里面调用了getchildmeasurespec方法,把父容器的measurespec以及自身的layoutparams属性传递进去来获取子view的measurespec,这也印证了“子view的measurespec由父容器的measurespec和自身的layoutparams共同决定”这个结论。那么,我们一起来看看viewgroup#getchildmeasurespec方法:
public static int getchildmeasurespec(int spec, int padding, int childdimension) { int specmode = measurespec.getmode(spec); int specsize = measurespec.getsize(spec); //size表示子view可用空间:父容器尺寸减去padding int size = math.max(0, specsize - padding); int resultsize = 0; int resultmode = 0; switch (specmode) { // parent has imposed an exact size on us case measurespec.exactly: if (childdimension >= 0) { resultsize = childdimension; resultmode = measurespec.exactly; } else if (childdimension == layoutparams.match_parent) { // child wants to be our size. so be it. resultsize = size; resultmode = measurespec.exactly; } else if (childdimension == layoutparams.wrap_content) { // child wants to determine its own size. it can't be // bigger than us. resultsize = size; resultmode = measurespec.at_most; } break; // parent has imposed a maximum size on us case measurespec.at_most: //省略..具体可自行参考源码 break; // parent asked to see how big we want to be case measurespec.unspecified: //省略...具体可自行参考源码 break; } return measurespec.makemeasurespec(resultsize, resultmode); }
上面方法也非常容易理解,大概是根据不同的父容器的模式及子view的layoutparams来决定子view的规格尺寸模式等。那么,这里根据上面的逻辑,列出不同的父容器的measurespec和子view的layoutparams的组合情况下所出现的不同的子view的measurespec:
(注:该表格呈现形式参考自《android 开发艺术探索》 任玉刚 著)
当子view的measurespec获得后,我们返回measurechildwithmargins方法,接着就会执行①号代码:child.measure方法,意味着,绘制流程已经从viewgroup转移到子view中了,可以看到传递的参数正是我们刚才获取的子view的measurespec,接着会调用view#measure,这在上面说过了,这里不再赘述,然后在measure方法,会调用onmeasure方法,当然了,对于不同类型的view,其onmeasure方法是不同的,但是对于不同的view,即使是自定义view,我们在重写的onmeasure方法内,也一定会调用到view#onmeasure方法的,因此我们看看它的源码:
protected void onmeasure(int widthmeasurespec, int heightmeasurespec) { setmeasureddimension(getdefaultsize(getsuggestedminimumwidth(), widthmeasurespec), getdefaultsize(getsuggestedminimumheight(), heightmeasurespec)); }
显然,这里调用了setmeasuredimension方法,上面说过该方法的作用是设置测量宽高,而测量宽高则是从getdefaultsize中获取,我们继续看看这个方法view#getdefaultsize:
public static int getdefaultsize(int size, int measurespec) { int result = size; int specmode = measurespec.getmode(measurespec); int specsize = measurespec.getsize(measurespec); switch (specmode) { case measurespec.unspecified: result = size; break; case measurespec.at_most: case measurespec.exactly: result = specsize; break; } return result; }
好吧,又是类似的代码,根据不同模式来设置不同的测量宽高,我们直接看at_most和exactly模式,它直接把specsize返回了,即view在这两种模式下的测量宽高直接取决于specsize规格。也即是说,对于一个直接继承自view的自定义view来说,它的wrap_content和match_parent属性的效果是一样的,因此如果要实现自定义view的wrap_content,则要重写onmeasure方法,对wrap_content属性进行处理。
接着,我们看unspecified模式,这个模式可能比较少见,一般用于系统内部测量,它直接返回的是size,而不是specsize,那么size从哪里来的呢?再往上看一层,它来自于getsuggestedminimumwidth()或getsuggestedminimumheight(),我们选取其中一个方法,看看源码,view#getsuggestedminimumwidth:
protected int getsuggestedminimumwidth() { return (mbackground == null) ? mminwidth : max(mminwidth, mbackground.getminimumwidth()); }
从以上逻辑可以看出,当view没有设置背景的时候,返回mminwidth,该值对应于android:minwidth属性;如果设置了背景,那么返回mminwidth和mbackground.getminimumwidth中的最大值。那么mbackground.getminimumwidth又是什么呢?其实它代表了背景的原始宽度,比如对于一个bitmap来说,它的原始宽度就是图片的尺寸。到此,子view的测量流程也完成了。
总结
这里简单概括一下整个流程:测量始于decorview,通过不断的遍历子view的measure方法,根据viewgroup的measurespec及子view的layoutparams来决定子view的measurespec,进一步获取子view的测量宽高,然后逐层返回,不断保存viewgroup的测量宽高。
从文章开始到现在,view的测量流程已经全部分析完毕,view的measure流程是三大流程中最复杂的一个流程,其中的measurespec贯穿了整个测量流程,占有非常重要的地位,希望读者仔细体会这个流程,最后希望这篇文章能帮助你对view的测量流程有进一步的了解,谢谢阅读。
更多阅读
android view 布局流程(layout)完全解析
android view 绘制流程(draw) 完全解析
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
推荐阅读
-
Android View 绘制流程(Draw)全面解析
-
Android View 布局流程(Layout)全面解析
-
Android View 测量流程(Measure)全面解析
-
Android View measure layout draw 过程解析
-
全面解析Android系统指纹启动流程
-
带你全面解析Android框架体系架构view篇 android架构view
-
Android View measure layout draw 过程解析
-
Android开发学习笔记——自定义View(一)View三大工作流程measure、layout和draw
-
带你全面解析Android框架体系架构view篇 android架构view
-
全面解析Android系统指纹启动流程