欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android View 测量流程(Measure)全面解析

程序员文章站 2023-12-20 11:21:52
前言 文章,笔者主要讲述了decorview以及viewrootimpl相关的作用,这里回顾一下上一章所说的内容:decorview是视图的*view,我们添加的布局文...

前言

文章,笔者主要讲述了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 测量流程(Measure)全面解析

(注:该表格呈现形式参考自《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) 完全解析

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。

上一篇:

下一篇: