技术控

    今日:0| 主题:63445
收藏本版 (1)
最新软件应用技术尽在掌握

[其他] Android 浅谈View的测量measure

[复制链接]
最后是我开了口 发表于 2016-10-10 01:21:02
325 2
本篇文章算是对 Android自定义控件学习笔记三 的补充和完善。一般一个View的呈现基本需要三大流程measure、layout、draw,measure作为View的三大工作流程之一,也是三大流程中第一个流程,主要用于确定View的测量宽/高,该流程的执行情况将直接影响后续的两个流程,可谓是重中之重,不可不察也。其余的两个流程layout用于确定View的最终宽高和四个顶点的位置,Draw则将View绘制到屏幕上。
   

Android 浅谈View的测量measure

Android 浅谈View的测量measure

  讲到View的measure测量,一般会涉及到两个方法和一个类,两个方法分别是measure和onMeasure,一个类是MeasureSpec。在自定义View中MeasureSpec在measure和onMeasure两个方法中都有使用,所以为了更好地理解View的测量过程,MeasureSpec是我们首先需要理解的东西。
  MeasureSpec

  MeasureSpec是View的一个静态内部类,MeasureSpec类封装了父View传递给子View的布局(layout)要求,每个MeasureSpec实例代表宽度或者高度(只能是其一)要求。MeasureSpec字面意思是测量规格或者测量属性,在measure方法中有两个参数widthMeasureSpec和heightMeasureSpec,如果使用widthMeasureSpec,我们就可以通过MeasureSpec计算出宽的模式Mode和宽度的实际值,当然了也可以通过模式Mode和宽度获得一个MeasureSpec,下面是MeasureSpec的部分核心逻辑。
  1. public class MeasureSpec {
  2. // 进位大小为2的30次方(int的大小为32位,所以进位30位就是要使用int的最高位和倒数第二位也就是32和31位做标志位)
  3.     private static final int MODE_SHIFT = 30;
  4.    
  5.     // 运算遮罩,0x3为16进制,10进制为3,二进制为11。3向左进位30,就是11 00000000000(11后跟30个0)
  6.     // (遮罩的作用是用1标注需要的值,0标注不要的值。因为1与任何数做与运算都得任何数,0与任何数做与运算都得0)
  7.     private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
  8.     // 0向左进位30,就是00 00000000000(00后跟30个0)
  9.     public static final int UNSPECIFIED = 0 << MODE_SHIFT;
  10.     // 1向左进位30,就是01 00000000000(01后跟30个0)
  11.     public static final int EXACTLY    = 1 << MODE_SHIFT;
  12.     // 2向左进位30,就是10 00000000000(10后跟30个0)
  13.     public static final int AT_MOST    = 2 << MODE_SHIFT;
  14.     /**
  15.      * 根据提供的size和mode得到一个详细的测量结果
  16.      */
  17.     // measureSpec = size + mode; (注意:二进制的加法,不是10进制的加法!)
  18.     // 这里设计的目的就是使用一个32位的二进制数,32和31位代表了mode的值,后30位代表size的值
  19.     // 例如size=100(4),mode=AT_MOST,则measureSpec=100+10000...00=10000..00100
  20.     public static int makeMeasureSpec(int size, int mode) {
  21.         return size + mode;
  22.     }
  23.     /**
  24.      * 通过详细测量结果获得mode
  25.      */
  26.     // mode = measureSpec & MODE_MASK;
  27.     // MODE_MASK = 11 00000000000(11后跟30个0),原理是用MODE_MASK后30位的0替换掉measureSpec后30位中的1,再保留32和31位的mode值。
  28.     // 例如10 00..00100 & 11 00..00(11后跟30个0) = 10 00..00(AT_MOST),这样就得到了mode的值
  29.     public static int getMode(int measureSpec) {
  30.         return (measureSpec & MODE_MASK);
  31.     }
  32.     /**
  33.      * 通过详细测量结果获得size
  34.      */
  35.     // size = measureSpec & ~MODE_MASK;
  36.     // 原理同上,不过这次是将MODE_MASK取反,也就是变成了00 111111(00后跟30个1),将32,31替换成0也就是去掉mode,保留后30位的size
  37.     public static int getSize(int measureSpec) {
  38.         return (measureSpec & ~MODE_MASK);
  39.     }
  40. }
复制代码
MeasureSpec实际上是对int类型的整数进行位运算的一个封装,其中前2位是Mode,后面30位是实际宽或高,Mode就三种情况:
  
       
  • UNSPECIFIED(未指定) 父元素不会对子元素施加任何束缚,子元素可以得到任意想要的大小;   
  • EXACTLY(完全) 父元素决定子元素的确切大小,子元素将被限定在给定的边界里而忽略它本身大小;   
  • AT_MOST(至多) 子元素至多达到指定大小的值。  
  三种模式中最常用的是EXACTLY和AT_MOST两种模式,这两种模式分别对应layout布局文件中的match_parent和wrap_content,而布局文件会转化为中layout相关属性会转换为LayoutParams,接下来我们看一下LayoutParams是如何与MeasureSpec进行逻辑交互的。
  LayoutParams与MeasureSpec关系

   系统内部通过MeasureSpec对View进行测量,但是我们可以通过给View设置LayoutParams来影响MeasureSpec,有关LayoutParams的更多内容可以查看 Android浅谈LayoutParams 。在View测量的时候,系统会将LayoutParams在父ViewGroup的作用下转化为MeasureSpec,这里需要注意一点子View的MeasureSpec不是唯一有LayoutParams决定而是与父ViewGroup的MeasureSpec一起决定。在ViewGroup中无论是measureChild还是measureChildWithMargins方法中都有一个getChildMeasureSpec方法,代码如下:
  1. protected void measureChild(Viewchild, int parentWidthMeasureSpec,
  2. int parentHeightMeasureSpec) {
  3. final LayoutParamslp = child.getLayoutParams();
  4. final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
  5. mPaddingLeft + mPaddingRight, lp.width);
  6. final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
  7. mPaddingTop + mPaddingBottom, lp.height);
  8. child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
  9. }
  10. public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
  11. int specMode = MeasureSpec.getMode(spec);
  12. int specSize = MeasureSpec.getSize(spec);
  13. int size = Math.max(0, specSize - padding);
  14. int resultSize = 0;
  15. int resultMode = 0;
  16. switch (specMode) {
  17. // Parent has imposed an exact size on us
  18. case MeasureSpec.EXACTLY:
  19. if (childDimension >= 0) {
  20. resultSize = childDimension;
  21. resultMode = MeasureSpec.EXACTLY;
  22. } else if (childDimension == LayoutParams.MATCH_PARENT) {
  23. // Child wants to be our size. So be it.
  24. resultSize = size;
  25. resultMode = MeasureSpec.EXACTLY;
  26. } else if (childDimension == LayoutParams.WRAP_CONTENT) {
  27. // Child wants to determine its own size. It can't be
  28. // bigger than us.
  29. resultSize = size;
  30. resultMode = MeasureSpec.AT_MOST;
  31. }
  32. break;
  33. // Parent has imposed a maximum size on us
  34. case MeasureSpec.AT_MOST:
  35. if (childDimension >= 0) {
  36. // Child wants a specific size... so be it
  37. resultSize = childDimension;
  38. resultMode = MeasureSpec.EXACTLY;
  39. } else if (childDimension == LayoutParams.MATCH_PARENT) {
  40. // Child wants to be our size, but our size is not fixed.
  41. // Constrain child to not be bigger than us.
  42. resultSize = size;
  43. resultMode = MeasureSpec.AT_MOST;
  44. } else if (childDimension == LayoutParams.WRAP_CONTENT) {
  45. // Child wants to determine its own size. It can't be
  46. // bigger than us.
  47. resultSize = size;
  48. resultMode = MeasureSpec.AT_MOST;
  49. }
  50. break;
  51. // Parent asked to see how big we want to be
  52. case MeasureSpec.UNSPECIFIED:
  53. if (childDimension >= 0) {
  54. // Child wants a specific size... let him have it
  55. resultSize = childDimension;
  56. resultMode = MeasureSpec.EXACTLY;
  57. } else if (childDimension == LayoutParams.MATCH_PARENT) {
  58. // Child wants to be our size... find out how big it should
  59. // be
  60. resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
  61. resultMode = MeasureSpec.UNSPECIFIED;
  62. } else if (childDimension == LayoutParams.WRAP_CONTENT) {
  63. // Child wants to determine its own size.... find out how
  64. // big it should be
  65. resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
  66. resultMode = MeasureSpec.UNSPECIFIED;
  67. }
  68. break;
  69. }
  70. return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
  71. }
复制代码
从上面可以看出,只要给出父ViewGroup的MeasureSpec和子View的LayoutParams就可以很快的确定出子View的MeasureSpec,有了MeasureSpec就可以很快确定出子View测量后的大小了。讲到这里会发现还有一种模式没有说明呢,UNSPECIFIED这种模式在下文结合代码再继续讲解,该模式主要用于系统内部多次measure的情形。
  measure方法

  如果只是一个View直接通过measure就可以完成测量过程,但是如果是一个ViewGroup,除了完成自己的测量外,还需要遍历测量自己的所有孩子,各个子元素都需要递归调用该过程直至所有孩子都测量完毕。
  在直接继承自ViewGroup中自定义View中,一般我们都需要重写一个onMeasure方法,但是该方法不是必须的,通过代码可以很容易发现,因为需要我们强制重写的方法中并没有onMeasure方法,这是因为如果我们的自定义ViewGroup中子View的大小是ViewGroup直接分配的,并没有考虑子View自身大小因素,比如我们需要自定义一个相册View,每一行显示三个图片,这时候只要三个图片平均分配占满一行就可以了,不用考虑子View大小,由父ViewGroup直接赋值就可以了。但是在自定义ViewGroup时,如果想要测量子View,都是直接调用的measure方法,但是当前类中需要重写的确是onMeasure方法,这是为什么呢?先看一下View中measure方法的定义:
  1. public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
  2. if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
  3.                 widthMeasureSpec != mOldWidthMeasureSpec ||
  4.                 heightMeasureSpec != mOldHeightMeasureSpec) {
  5. //...
  6.             int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
  7.                     mMeasureCache.indexOfKey(key);
  8.             if (cacheIndex < 0 || sIgnoreMeasureCache) {
  9.                 // measure ourselves, this should set the measured dimension flag back
  10.                 onMeasure(widthMeasureSpec, heightMeasureSpec);
  11.             } else {
  12.                 long value = mMeasureCache.valueAt(cacheIndex);
  13.                 // Casting a long to int drops the high 32 bits, no mask needed
  14.                 setMeasuredDimensionRaw((int) (value >> 32), (int) value);
  15.             }
  16.         }
  17. //...
  18.         mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
  19.                 (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
  20.     }
  21. }
复制代码
  从measure方法定义格式就可以知道,我们想重写该方法都不行因为是最终方法。再看修饰符是 public ,这也就意味着我们可以在任意的地方View都可以直接调用measure方法,这也是为什么有时候在一些demo代码会看到measure(0,0)这种奇怪的调用方式了,因为View只有被测量过可以知道其大小,还没被测量之前如果想知道View大小怎么办呢,那么手动测量一下就可以了,那么如果我多次调用measure方法会不会测量多次呢,这个不一定,有上面代码可以知道,当测量完成以后View的宽高值会存入一个mMeasureCache的变量中,当我们再次传入的MeasureSpec相同,,此时变回直接从mMeasureCache中将上一次存入的值直接取出来赋值到View中。
  measure(0,0)中0代表的是什么?从measure方法的传参类型可以知晓0其实就是一个值为0的MeasureSpec,该MeasureSpec对应的模式就是UNSPECIFIED,上面说了该模式父View不会对子View添加任何限制,子View可以任意大小,这个任意大小就是子View不受父View空间约束的实际大小。下面我们通过onMeasure方法中的逻辑梳理一下measure(0,0)。
  onMeasure方法

  1. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  2. setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
  3. getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
  4. }
  5. protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
  6. boolean optical = isLayoutModeOptical(this);
  7. if (optical != isLayoutModeOptical(mParent)) {
  8. Insetsinsets = getOpticalInsets();
  9. int opticalWidth  = insets.left + insets.right;
  10. int opticalHeight = insets.top  + insets.bottom;
  11. measuredWidth  += optical ? opticalWidth  : -opticalWidth;
  12. measuredHeight += optical ? opticalHeight : -opticalHeight;
  13. }
  14. setMeasuredDimensionRaw(measuredWidth, measuredHeight);
  15. }
  16. private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
  17. mMeasuredWidth = measuredWidth;
  18. mMeasuredHeight = measuredHeight;
  19. mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
  20. }
复制代码
setMeasuredDimension就是设置View的宽高值,核心还是看getDefaultSize方法。
  1. public static int getDefaultSize(int size, int measureSpec) {
  2. int result = size;
  3. int specMode = MeasureSpec.getMode(measureSpec);
  4. int specSize = MeasureSpec.getSize(measureSpec);
  5. switch (specMode) {
  6. case MeasureSpec.UNSPECIFIED:
  7. result = size;
  8. break;
  9. case MeasureSpec.AT_MOST:
  10. case MeasureSpec.EXACTLY:
  11. result = specSize;
  12. break;
  13. }
  14. return result;
  15. }
复制代码
通过getDefaultSize代码可以知道,如果传入的measureSpec的模式是UNSPECIFIED,那么View的大小就是传入值size的大小,计算size代码如下:
  1. protected int getSuggestedMinimumWidth() {
  2. return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
  3. }
复制代码
从代码可以看出如果View没有设置背景,那么View的大小就是mMinWidth,mMinWidth是在layout布局文件中设置的android:minWidth指定的值,如果这个值没有指定,则最总返回0。
  通过getDefaultSize的实现可以知道,View的宽高由specSize决定,我们可以得出结论:直接继承View的自定义控件需要重写onMeasure方法并设置wrap_content时的自身大小,否则在布局中使用wrap_content就相当于使用match_parent,为什么是这样呢?当我们在布局中使用wrap_content时,那么它的specMode相当于AT_MOST,而在这种模式下它的宽高等于specSize,而这个specSize是通过父View传入的MeasureSpec获取到的,事实上就是父View的可以使用的大小,也是父View剩余空间的大小。很显然这种情况下View的宽高等于父View剩余空间的大小,跟在布局中使用match_parent效果完全一致。这个问题也容易解决,通过效仿getSuggestedMinimumWidth方法,给View设置一个内部的默认的宽高,当设置为wrap_content直接使用设置的默认宽高即可。对于非wrap_content,我们仍然使用系统内部的测量值即可。处理wrap_content时示例代码如下:
  1. public void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
  2. int widthSize = MeasureSpec.getSize(widthMeasureSpec);
  3. int widthMode = MeasureSpec.getMode(widthMeasureSpec);
  4. int heightSize = MeasureSpec.getSize(heightMeasureSpec);
  5. int heightMode = MeasureSpec.getMode(heightMeasureSpec);
  6. if(widthMode== MeasureSpec.AT_MOST&&heightMode==MeasureSpec.AT_MOST){
  7. setMeasuredDimension(mWidth,mHeight);
  8. }else if(widthMode== MeasureSpec.AT_MOST){
  9. setMeasuredDimension(mWidth,heightSize);
  10. }else if(heightMode==MeasureSpec.AT_MOST){
  11. setMeasuredDimension(widthSize,mHeight);
  12. }
  13. }
复制代码
CliftonMl 发表于 2016-10-21 09:43:26
灌,是一种美德。
回复 支持 反对

使用道具 举报

duck1082 发表于 2016-11-21 18:47:16
这是一个KB的故事,当你在半夜12点的时候穿着黑色的衣服对着镜子用梳子梳下就会看到…头皮…屑!
回复 支持 反对

使用道具 举报

我要投稿

推荐阅读


回页顶回复上一篇下一篇回列表
手机版/c.CoLaBug.com ( 粤ICP备05003221号 | 粤公网安备 44010402000842号 )

© 2001-2017 Comsenz Inc.

返回顶部 返回列表