【Android - 自定义View】之MeasureSpec简介

  MeasureSpec是View测量过程中的一个重要的类,它被用来将View的尺寸规格(SpecSize)尺寸模式(SpecMode)封装在一起,并提供打包和解包的方法。

  MeasureSpec虽然是一个工具类,在View测量的时候会用这个类来解析View的规格和模式,因此很多人都用MeasureSpec兼代这个尺寸和模式的打包值。

  MeasureSpec是一个 32 位的 int 值,其高2位代表SpecMode,即测量模式;低30位代表SpecSize,即在某种模式下的规格大小。MeasureSpec用这种方式将这两个值打包在一起,并提供了获取两个值得方法 getMode(int measureSpec) 和 getSize(int measureSpec) 。

  简单地说,一个View的MeasureSpec是这个View的父布局传递给这个View的,代表父布局能够提供给这个View多大的空间(在一定程度上由其父布局的尺寸决定)。

  MeasureSpec中的SpecMode有三个可选值,分别是 UNSPECIFIED 、 EXACTLY 、 AT_MOST 。通常,一个View在父容器中的尺寸声明(即这个View的LayoutParams的值)可能有三种:match_parent、wrap_content和固定值(如100dip)。

  一个View的MeasureSpec是由这个View的LayoutParams的值和这个View所在父布局传递过来的MeasureSpec一起决定的,而大多数情况下,View的父容器都是ViewGroup。那么我们就先从ViewGroup开始,看它的 measureChildWithMargin() 方法,源码如下:

/**
 * 让这个ViewGroup中的某个View自行进行测量,将这个View的MeasureSpec的宽高需求和padding、margin都计算在内
* *
@param child 要测量的子View * @param parentWidthMeasureSpec 对这个View的宽度需求 * @param widthUsed 由于其他子控件占用等原因,已经使用的宽度值 * @param parentHeightMeasureSpec 对这个View的高度需求 * @param heightUsed 由于其他子控件占用等原因,已经使用的高度值 */ 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); }

  从源码中可以看到,在measureChildWidthMargins()方法中,调用了 getChildMeasureSpec() 方法来测量当前ViewGroup中某个View的最佳布局的MeasureSpec,然后将宽度的MeasureSpec和高度的MeasureSpec重新赋值给这个View。getChildMeasureSpec()方法的源码如下:

/**
 * 这个方法用来计算并返回当前ViewGroup中某个View的某个MeasureSpec(宽度或高度中的一个)。
 * 这个方法的目的是结合当前ViewGroup的MeasureSpec和某个子View的LayoutParams,
 * 得到最适合这个子View的MeasureSpec并返回
 *
 * @param spec           当前ViewGroup的MeasureSpec(宽度或高度中的一个)
 * @param padding        子View在某个尺寸(宽度或高度中的一个)上的padding和margin的和
 * @param childDimension 子View希望得到的尺寸(宽度或高度中的一个)大小
 * @return               返回计算得到的这个子View布局的最佳MeasureSpec
 */
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    int specMode = View.MeasureSpec.getMode(spec);
    int specSize = View.MeasureSpec.getSize(spec);

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

    int resultSize = 0;
    int resultMode = 0;

    switch (specMode) {
        // 父容器在这个尺寸方向(宽度或高度中的一个)上的尺寸是固定值或MATCH_PARENT
        case View.MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                // 如果childDimension不小于0,则表示子View的尺寸有具体的值,如100dip
                resultSize = childDimension;
                resultMode = View.MeasureSpec.EXACTLY;
            } else if (childDimension == ViewGroup.LayoutParams.MATCH_PARENT) {
                // ViewGroup.LayoutParams.MATCH_PARENT = -1
                resultSize = size;
                resultMode = View.MeasureSpec.EXACTLY;
            } else if (childDimension == ViewGroup.LayoutParams.WRAP_CONTENT) {
                // ViewGroup.LayoutParams.WRAP_CONTENT = -2
                resultSize = size;
                resultMode = View.MeasureSpec.AT_MOST;
            }
            break;

        // 父容器在这个尺寸方向(宽度或高度中的一个)上的尺寸是WRAP_CONTENT
        case View.MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = View.MeasureSpec.EXACTLY;
            } else if (childDimension == ViewGroup.LayoutParams.MATCH_PARENT) {
                resultSize = size;
                resultMode = View.MeasureSpec.AT_MOST;
            } else if (childDimension == ViewGroup.LayoutParams.WRAP_CONTENT) {
                resultSize = size;
                resultMode = View.MeasureSpec.AT_MOST;
            }
            break;

        // 父容器没有指定在这个方向上的尺寸,子View想要多大就给多大
        case View.MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = View.MeasureSpec.EXACTLY;
            } else if (childDimension == ViewGroup.LayoutParams.MATCH_PARENT) {
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = View.MeasureSpec.UNSPECIFIED;
            } else if (childDimension == ViewGroup.LayoutParams.WRAP_CONTENT) {
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = View.MeasureSpec.UNSPECIFIED;
            }
            break;
    }
    // 重新包装这个View在当前尺寸方向上的尺寸规格和尺寸模式
    return View.MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

  可以看到,在这个方法中枚举了父容器的MeasureSpec和子View的LayoutParams的所有可能的情况,并一一给出了解决方案。针对不同的父容器和View本身不同的LayoutParams,View就可以有多种MeasureSpec,这里对所有情况进行了梳理,如下表所示:

  对照上表举个例子:当父容器的MeasureSpec的SpecMode是EXACTLY,并且子View的LayoutParams是固定值的时候,通过上面的代码得到的子View的MeasureSpec的SpecMode是EXACTLY,SpecSize就是子View的宽度或高度值。以此类推。

  在自定义View的时候,父控件的尺寸大多数都是MATCH_PARENT或具体的尺寸值,因此,我们可以粗略的将SpecMode与子View的LayoutParams的关系总结如下:

  • UNSPECIFIED:父容器不对子View有任何限制,要多大给多大。这种情况不常见;
  • EXACTLY:父容器已经检测出子View所需要的精确大小,这个时候子View的最终大小就是SpecSize的值。它对应于LayoutParams中的MATCH_PARENT和具体的数值这两种模式;
  • AT_MOST:父容器指定了一个可用的大小(可能就是父容器的大小,也可能是父容器中出去已有子View之外可用的大小),子View的大小不能大于这个值,具体是多少要看子View的具体实现。它对应于LayoutParams中的WRAP_CONTENT。

  另外,上面的代码中还提到了MeasureSpec类中的 makeMeasureSpec() 方法,这个方法就是将SpecSize和SpecMode打包成一个完整的MeasureSpec的方法。

  最后补充一点:

  我们上面说的都是针对普通View来说的,但是MeasureSpec不仅可以表示普通View的尺寸,也可以表示顶层View(DecorView)的尺寸。如果是针对DecorView,则其父容器就是当前Activity中窗口的大小,当SpecMode为EXACTLY的时候,其SpecSize就是窗口的尺寸大小。其他的和普通View的情况相同。

  有了MeasureSpec,我们就可以在自定义View的时候,在 onMeasure() 方法中通过传入的宽度和高度的MeasureSpec的值,获得父布局分配给我们的自定义View的尺寸大小,然后进行相应的修改,最后通过 setMeasuredDimension() 方法重置View的尺寸,达到想要的效果。可以看出,MeasureSpec在自定义View中还是比较核心的知识点。

 

posted on 2017-04-17 16:08  ITGungnir  阅读(436)  评论(0编辑  收藏  举报

导航