View以及ViewGroup的度量

目录

  • MeasureSpec
  • LayoutParam
  • 度量过程

之前一直知道一个界面的测量过程是由Android系统便利ViewTree,递归调用各个View或者说ViewGroup的onMeasure()方法完成的,但是对于每一个组件具体的测量过程还是不求甚解,于是阅读以下源码,稍微学习了一下,特此记录。

MeasureSpec

MeasureSpecView的一个静态内部类,是用于辅助完成View的测量的工具类之一,从字面上来看MeasureSpec->测量规格,也就是说Measure会告诉Android系统该如何做,即如何测量
MeasureSpecs被实现为一个32位的int值,高两位用于表示SpecMode,低30位表示SpecSize
再看一下源码对这个类的注释说明是这样的:

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.

其中有两个关键点

  • MeasureSpec封装了从父级传递到子级的布局要求
  • MeasureSpec由大小模式组成,对应刚才说到的SpecModeSpecSize

然后再看一下MeasureSpec的内部构成

private static final int MODE_SHIFT = 30;
private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

 /**
* Measure specification mode: The parent has not imposed any constraint
* on the child. It can be whatever size it wants.
*/
public static final int UNSPECIFIED = 0 << MODE_SHIFT;

/**
* Measure specification mode: The parent has determined an exact size
* for the child. The child is going to be given those bounds regardless
* of how big it wants to be.
*/
public static final int EXACTLY     = 1 << MODE_SHIFT;

/**
* Measure specification mode: The child can be as large as it wants up
* to the specified size.
*/
public static final int AT_MOST     = 2 << MODE_SHIFT;

  • 可以看到类内一共定义了五个常量,首先是 MODE_SHIFTMODE_MASK。当我们拿到一个MeasureSpec的具体值时,就可以通过Measure.getMode(int measureSpec)Measure.getSize(int measureSpec)这两个方法对MeasureSpec具体值进行解包得到具体的SpecModeSpecSize了。这两个工具方法就是通过将 MODE_SHIFTMODE_MASK 和具体MeasureSpec的进行简单的位运算实现的。当然Measure内部也提供了makeMeasureSpec( int size, int mode)方法,可将SpecSize和SpecMode再次打包为一个MeasureSpec
@MeasureSpecMode
public static int getMode(int measureSpec) {
    //noinspection ResourceType
    return (measureSpec & MODE_MASK);
}

public static int getSize(int measureSpec) {
    return (measureSpec & ~MODE_MASK);
}

public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                          @MeasureSpecMode int mode) {
    if (sUseBrokenMakeMeasureSpec) {
        return size + mode;
    } else {
         return (size & ~MODE_MASK) | (mode & MODE_MASK);
    }
}
  • 剩下的三个常量UNSPECIFIEDEXACTLYAT_MOST对应上文SpecSize的具体实现
  1. UNSPECIFIED
    表示:父容器的测量模式为未指定模式。在这种情况下,父容器布局没有对子View施加任何约束。 可以是任何大小
  2. EXACTLY
    表示:父容器的测量模式为精确模式。在这种情况下,表明父容器已经有了一个确切值,子View的测量过程将受到这个值的限定
  3. AT_MOST
    表示: 父容器的测量模式为最大限定模式。在这种情况下,子View可以根据自己的具体需求分配尺寸大小,但是不可以超过父容器的尺寸

LayoutParams

LayoutParamsViewGroup的一个静态内部类,其中存储了View在XML定义的具体尺寸属性。用于告诉父容器,当前View想要如何被布局。
在看一下LayoutParams的内部构造

/**
* Special value for the height or width requested by a View.
* MATCH_PARENT means that the view wants to be as big as its parent,
* minus the parent's padding, if any. Introduced in API Level 8.
*/
public static final int MATCH_PARENT = -1;

/**
* Special value for the height or width requested by a View.
* WRAP_CONTENT means that the view wants to be just large enough to fit
* its own internal content, taking its own padding into account.
*/
public static final int WRAP_CONTENT = -2;

/**
* Information about how wide the view wants to be. Can be one of the
* constants FILL_PARENT (replaced by MATCH_PARENT
* in API Level 8) or WRAP_CONTENT, or an exact size.
*/
@ViewDebug.ExportedProperty(category = "layout", mapping = {
@ViewDebug.IntToString(from = MATCH_PARENT, to = "MATCH_PARENT"),
@ViewDebug.IntToString(from = WRAP_CONTENT, to = "WRAP_CONTENT")
})
public int width;

/**
* Information about how tall the view wants to be. Can be one of the
* constants FILL_PARENT (replaced by MATCH_PARENT
* in API Level 8) or WRAP_CONTENT, or an exact size.
*/
@ViewDebug.ExportedProperty(category = "layout", mapping = {
    @ViewDebug.IntToString(from = MATCH_PARENT, to = "MATCH_PARENT"),
    @ViewDebug.IntToString(from = WRAP_CONTENT, to = "WRAP_CONTENT")
})
public int height;

这里的widthheight分别对应我们在XML中使用android:layout_width=""android:layout_height=""标签分配给View的宽高的期望值
widthheight为大于0的具体值时,对应我们在XML给View指定的具体期望值,例如android:layout_width="50dp"
widthheight的值小于0,例,width = -1width = -2分别对应XML中的 android:layout_width="match_parent"android:layout_height="wrap_content"

度量过程

首先看一下ViewGroup.java中的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];
         if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
          }
    }
}

显而易见,这里通过遍历所有子View,然后调用measureChild()方法,对ViewGroup内的所有子View进行了度量。
这里需要说明一下的是,这个方法其实只被NumPadKeyAbsoluteLayout在其各自的onMeasure方法中调用了,我们所熟悉的一些ViewGroup通常都会在自己的onMeasure方法中根据业务需求遍历所有子View,进而调用measureChild()方法完成ViewGroup中的子View度量。
那么我们跟进去看一下measureChild()方法

protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        final LayoutParams lp = child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

这个方法也很简单,一共做了三件事

  1. 首先拿到子View的LayoutParams属性。
  2. 然后分别结合ViewGroup的 parentWidthMeasureSpecparentHeightMeasureSpec属性调用getChildMeasureSpec得到子View在宽维度和高维度上的MeasureSpec属性。
  3. 最后调用子View的measure方法,这个方法最终又会调用到子View的onMeasure()方法,进而完成子View的内部度量。如此循环,最终完成ViewTree的递归度量。

这里需要再看一下getChildMeasureSpec()方法的实现细节

/**
* @param spec The requirements for this view
* @param padding The padding of this view for the current dimension and
*        margins, if applicable
* @param childDimension How big the child wants to be in the current
*        dimension
* @return a MeasureSpec integer for the child
*/
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

        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:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } 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 asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

首先看一下这个方法的三个入参,从上文的介绍中可以知道:

  • int spec :
    当前测量维度(宽或者高)上ViewVroup的MeasureSpec属性。
  • int padding:
    这里可以理解为父ViewGroup在当前测量维度上已经用掉的空间所占大小。这里需要特别特别特别说明一下,padding直译就是填充的意思。我第一次看这里的时候,总把这里的padding和XML中所定义的padding属性想当然的联系起来,导致我一度认为如果每次传进来都是固定的padding,那么每次度量子View时可用的剩余空间都是一致的,如果ViewGroup里边有很多子View的情况下,如何实现每次度量,ViewGroup可用空间就要减去相应的子View所占去的空间的逻辑,就没办法合理的限制超出ViewGroup可使用空间的子View了呀? 进而导致在这里困惑了好久,直到看LinearLayout的测量过程才突然惊醒。。。。惯性思维真的害死人。即,当当前ViewGroup里边有N个子View控件时,当我们完成了第N-1个子View的度量后,再去度量第N个View的时候,这个padding完全可以用来表示之前N-1个子View加上父容器的padding属性后所占掉的空间,即你可以理解为这N-1个View已经被不重叠的(如果重叠,只会用去最大的一个View的空间嘛)填充进了这个ViewGroup内(当然在Layout之后,各个View才会被以此摆放在ViewGroup内)。
  • int childDimension:
    被测量子View在当前测量维度上的LayoutParam属性,即LayoutParams.widthLayoutParams.height

这个方法一共分为三个关键步骤

  1. 将当前ViewGroup的MeasureSpec进行解包,分别得到当前测量维度上的specModespecSize
  2. 根据当前ViewGroup的specModespecSize属性结合当前被测量View的LayoutParam属性确定当前被测量View的MeasureSpec属性
  3. 最终将specModespecSize打包成完整的MeasureSpec返回

关于第一步,稍微说一下就是,得到ViewGroup的specSize之后,再通过

int size = Math.max(0, specSize - padding);

就得到了当前ViewGroup还剩多少空间可以使用。也就是我当初绊倒的地方。。。

OK, 然后重点看一下剩下的switch块内的逻辑,首先看一下第一种情况,即ViewGroup的specModeMeasureSpec.EXACTLY为精确模式时,这里又分为三种情况:

  1. 如果子View已经定义了明确的尺寸,那么View的大小就是这个值。这时子View的SpecMode自然也就被确定为了MeasureSpec.EXACTLY模式。
  2. 如果子View在当前被测量维度的属性为MATCH_PARENT时,这时候子View在当前测量维度的SpecSize就为ViewGroup剩下的可支配大小。因为大小已经确定,所以子View的SpecMode自然而然的也就被确定为了MeasureSpec.EXACTLY
  3. 如果子View在当前被测量维度的属性为WRAP_CONTENT时,这时候,子View在当前测量维度的最大尺寸也只能为ViewGroup所剩下的空间的最大值,故此时子View的SpecSize被确定为了ViewGroup所剩的SpecSize的值,SpecMode也相应被确定为了MATCH_PARENT模式

剩下的两种情况和第一种情况类似,无非是根据ViewGroup的SpecMode得到子View相应的SpecModeSpecSize而已,也就不再赘述。

需要特殊说明的是,当ViewGroup在当前测量维度的MeasureSpec的模式为MeasureSpec.UNSPECIFIED模式这种情况,大多发生在系统内部测量以及可滚动ViewGroup的度量过程中,这里不再展开。但是这里有个地方特殊说明一下这里:

case MeasureSpec.UNSPECIFIED:
    if (childDimension >= 0) {
        resultSize = childDimension;
        resultMode = MeasureSpec.EXACTLY;
    } else if (childDimension == LayoutParams.MATCH_PARENT) {
         resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
         resultMode = MeasureSpec.UNSPECIFIED;
    } else if (childDimension == LayoutParams.WRAP_CONTENT) {
         resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
         resultMode = MeasureSpec.UNSPECIFIED;
    }
    break;

可以看到当ViewGroup的MeasureSpec为MeasureSpec.UNSPECIFIED时,且子View在当前测量维度未定义明确的尺寸时,子View最终的MeasureSpec的size属性是由View.sUseZeroUnspecifiedMeasureSpec这个属性决定的。不过这个地方稍微跟一下就可以确定啦。sUseZeroUnspecifiedMeasureSpec是View.java类中一个静态变量初始值为flase,且这个变量只在View的构造方法被赋值过,赋值逻辑如下:

// In M and newer, our widgets can pass a "hint" value in the size
// for UNSPECIFIED MeasureSpecs. This lets child views of scrolling containers
// know what the expected parent size is going to be, so e.g. list items can size
// themselves at 1/3 the size of their container. It breaks older apps though,
// specifically apps that use some popular open source libraries.
sUseZeroUnspecifiedMeasureSpec = targetSdkVersion < Build.VERSION_CODES.M;

所以这种情况下子View的MeasureSpec的size属性为

  1. ViewGroup在当前测量维度剩余值,对应-> targetSdkVersion >= Build.VERSION_CODES.M
  2. 0, 对应 targetSdkVersion < Build.VERSION_CODES.M

最终根据这个switch块,假定:

  1. 通过getChildMeasureSpec()中传入的第一个参数解包得到ViewGroup的SpecSize和SpecMode分别为specModespecSize
  2. 通过getChildMeasureSpec()中第二个参数padding得到的ViewGroup在当前测量维度上的剩余空间为
    parentSize( parentSize = Math.max(0, specSize - padding) )
  3. 通过第三个参数得到子View在当前测量维度上的Layoutparams属性为childDimension
  4. 此时若将子View的SpecSize和SpecMode分别参照代码命名为resultSizeresultMode

我们将到的下面这张在网上以及任玉刚大佬书里边可以看到的表格啦


View MeasureSpec获取参照表

说到这里ViewGroup的度量过程也就基本完成了,为什么说基本呢?毕竟我们现在只拿到了两样东西

  1. ViewGroup的MeasureSpec属性
  2. ViewGroup内各个View的MeasureSpec属性

MeasureSpec说到底也只是参考规范,而不是具体值,也就是我们还是不知道这个ViewGroup具体应该是占用多少空间,具体宽高是多少呢。其实我们看一下View的onMeasure()方法就知道答案了

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
   setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

可以看出,这个方法只调用了setMeasuredDimension()一个方法。其实也就是View最终通过自己的(注意这里是自己的了)widthMeasureSpecheightMeasureSpec稍微再结合一下自身其他方面对尺寸进行约束的属性,如getSuggestedMinimumWidth(),就得到了这个View的最终的mMeasuredWidthmMeasuredHeight了。也就是为什么在onMeasure()方法之后,我们就可以通过getMeasuredWidth()getMeasuredHeight()得到对应View的具体尺寸值了。

所以,在ViewGroup对所有子View完成遍历度量后,得到的不仅仅有所有子View的MeasureSpec,还有各个子View的具体尺寸值。这时候再根据

  1. 自身的MeasureSpec
  2. 各个子View的具体尺寸值
  3. 业务逻辑

进行最后的约束处理,就可以确定ViewGroup自身实际大小了,最终调用setMeasuredDimension(int measuredWidth, int measuredHeight)将最终尺寸进行保存就可以了。同样,这之后在对应的ViewGroup上调用getMeasuredWidth()getMeasuredHeight()就可以了得到这个ViewGroup的实际大小了。

总结:也就是说,当需要自定义个ViewGroup,可以分以下几步进行(度量方面)就可以了

  1. 继承ViewGroup
  2. 重写onMeasure()方法
  3. 遍历所有子View,对子View进行度量
  4. 调用setMeasuredDimension(int measuredWidth, int measuredHeight)进行自身尺寸的保存
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,142评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,298评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,068评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,081评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,099评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,071评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,990评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,832评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,274评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,488评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,649评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,378评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,979评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,625评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,643评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,545评论 2 352