Android自定义View:MeasureSpec的真正意义与View大小控制

自定义View是Android开发中最普通的需求,灵活控制View的尺寸是开发者面临的第一个问题,好比,为何明明使用的是WRAP_CONTENT却跟MATCH_PARENT表现相同。在处理View尺寸的时候,咱们都知道最好在onMeasure中设定好自定义View尺寸,那么究竟如何合理的选择这个尺寸呢。直观来讲,可能有如下问题须要考虑:javascript

  • 自定的View最好不要超过父控件的大小,这样才能保证本身能在父控件中完整显示
  • 自定的View(若是是ViewGroup)的子控件最好不要超过本身的大小,这样才能保证子控件显示完整
  • 若是明确为View指定了尺寸,最好按照指定的尺寸设置

以上三个问题多是自定义ViewGroup最须要考虑的问题,首先先解决第一个问题。java

父容器的限制与MeasureSpec

先假定,父容器是300dp*300dp的尺寸,若是子View的布局参数是android

<!--场景1-->
android:layout_width="match_parent"
android:layout_height="match_parent"复制代码

那么按照咱们的指望,但愿子View的尺寸要是300dp*300dp,若是子View的布局参数是ide

<!--场景2-->
android:layout_width="100dp"
android:layout_height="100dp"复制代码

按照咱们的指望,但愿子View的尺寸要是100dp*100dp,若是子View的布局参数是函数

<!--场景3-->
android:layout_width="wrap_content"
android:layout_height="wrap_content"复制代码

按照咱们的指望,但愿子View的尺寸能够按照本身需求的尺寸来肯定,可是最好不要超过300dp*300dp。布局

那么父容器怎么把这些要求告诉子View呢?MeasureSpec其实就是承担这种做用:MeasureSpec是父控件提供给子View的一个参数,做为设定自身大小参考,只是个参考,要多大,仍是View本身说了算。先看下MeasureSpec的构成,MeasureSpec由size和mode组成,mode包括三种,UNSPECIFIED、EXACTLY、AT_MOST,size就是配合mode给出的参考尺寸,具体意义以下:ui

  • UNSPECIFIED(未指定),父控件对子控件不加任何束缚,子元素能够获得任意想要的大小,这种MeasureSpec通常是由父控件自身的特性决定的。好比ScrollView,它的子View能够随意设置大小,不管多高,都能滚动显示,这个时候,size通常就没什么意义。
  • EXACTLY(彻底),父控件为子View指定确切大小,但愿子View彻底按照本身给定尺寸来处理,跟上面的场景1跟2比较类似,这时的MeasureSpec通常是父控件根据自身的MeasureSpec跟子View的布局参数来肯定的。通常这种状况下size>0,有个肯定值。
  • AT_MOST(至多),父控件为子元素指定最大参考尺寸,但愿子View的尺寸不要超过这个尺寸,跟上面场景3比较类似。这种模式也是父控件根据自身的MeasureSpec跟子View的布局参数来肯定的,通常是子View的布局参数采用wrap_content的时候。

先来看一下ViewGroup源码中measureChild怎么为子View构造MeasureSpec的:spa

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);
 }复制代码

因为任何View都是支持Padding参数的,在为子View设置参考尺寸的时候,须要先把本身的Padding给去除,这同时也是为了Layout作铺垫。接着看如何getChildMeasureSpec获取传递给子View的MeasureSpec的:code

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;
    }
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}复制代码

能够看到父控件会参考本身的MeasureSpec跟子View的布局参数,为子View构建合适的MeasureSpec,盗用网上的一张图来描述就是cdn

MeasureSpec构建

当子View接收到父控件传递的MeasureSpec的时候,就能够知道父控件但愿本身如何显示,这个点对于开发者而言就是onMeasure函数,先来看下View.java中onMeasure函数的实现:

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

其中getSuggestedMinimumWidth是根据设置的背景跟最小尺寸获得一个备用的参考尺寸,接着看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;
}复制代码

能够看到,若是自定义View没有重写onMeasure函数,MeasureSpec.AT_MOST跟MeasureSpec.AT_MOST的表现是同样的,也就是对于场景2跟3的表现实际上是同样的,也就是wrap_content就跟match_parent一个效果,如今咱们知道MeasureSpec的主要做用:父控件传递给子View的参考,那么子View拿到后该如何用呢?

自定义View尺寸的肯定

接收到父控件传递的MeasureSpec后,View应该如何用来处理本身的尺寸呢?onMeasure是View测量尺寸最合理的时机,若是View不是ViewGroup相对就比较简单,只须要参照MeasureSpec,并跟自身需求来设定尺寸便可,默认onMeasure的就是彻底按照父控件传递MeasureSpec设定本身的尺寸的。这里重点讲一下ViewGroup,为了得到合理的宽高尺寸,ViewGroup在计算本身尺寸的时候,必须预先知道全部子View的尺寸,举个例子,用一个经常使用的流式布局FlowLayout来说解一下如何合理的设定本身的尺寸。

先分析一下FLowLayout流式布局(从左到右)的特色:FLowLayout将全部子View从左往右依次放置,若是当前行,放不开的就换行。从流失布局的特色来看,在肯定FLowLayout尺寸的时候,咱们须要知道下列信息,

  • 父容器传递给FlowLayout的MeasureSpec推荐的大小(超出了,显示不出来,又没意义)
  • FlowLayout中全部子View的宽度与宽度:计算宽度跟高度的时候须要用的到。
  • 综合MeasureSpec跟自身需求,得出合理的尺寸

首先看父容器传递给FlowLayout的MeasureSpec,对开发者而言,它可见于onMeasure函数,是经过onMeasure的参数传递进来的,它的意义上面的已经说过了,如今来看,怎么用比较合理?其实ViewGroup.java源码中也提供了比较简洁的方法,有两个比较经常使用的measureChildren跟resolveSize,在以前的分析中咱们知道measureChildren会调用getChildMeasureSpec为子View建立MeasureSpec,并经过measureChild测量每一个子View的尺寸。那么resolveSize呢,看下面源码,resolveSize(int size, int measureSpec)的两个输入参数,第一个参数:size,是View自身但愿获取的尺寸,第二参数:measureSpec,其实父控件传递给View,推荐View获取的尺寸,resolveSize就是综合考量两个参数,最后给一个建议的尺寸:

public static int resolveSize(int size, int measureSpec) {
        return resolveSizeAndState(size, measureSpec, 0) & MEASURED_SIZE_MASK;
    }

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);
}复制代码

能够看到:

  • 若是父控件传递给的MeasureSpec的mode是MeasureSpec.UNSPECIFIED,就说明,父控件对本身没有任何限制,那么尺寸就选择本身须要的尺寸size
  • 若是父控件传递给的MeasureSpec的mode是MeasureSpec.EXACTLY,就说明父控件有明确的要求,但愿本身能用measureSpec中的尺寸,这时就推荐使用MeasureSpec.getSize(measureSpec)
  • 若是父控件传递给的MeasureSpec的mode是MeasureSpec.AT_MOST,就说明父控件但愿本身不要超出MeasureSpec.getSize(measureSpec),若是超出了,就选择MeasureSpec.getSize(measureSpec),不然用本身想要的尺寸就好了

对于FlowLayout,能够假设每一个子View均可以充满FlowLayout,所以,能够直接用measureChildren测量全部的子View的尺寸:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int paddingLeft = getPaddingLeft();
    int paddingRight = getPaddingRight();
    int paddingBottom = getPaddingBottom();
    int paddingTop = getPaddingTop();
    int count = getChildCount();
    int maxWidth = 0;
    int totalHeight = 0;
    int lineWidth = 0;
    int lineHeight = 0;
    int extraWidth = widthSize - paddingLeft - paddingRight;

    <!--直接用measureChildren测量全部的子View的高度--> measureChildren(widthMeasureSpec, heightMeasureSpec); <!--如今能够得到全部子View的尺寸--> for (int i = 0; i < count; i++) { View view = getChildAt(i); if (view != null && view.getVisibility() != GONE) { if (lineWidth + view.getMeasuredWidth() > extraWidth) { totalHeight += lineHeight ; lineWidth = view.getMeasuredWidth(); lineHeight = view.getMeasuredHeight(); maxWidth = widthSize; } else { lineWidth += view.getMeasuredWidth(); } <!--获取每行的最高View尺寸--> lineHeight = Math.max(lineHeight, view.getMeasuredHeight()); } } totalHeight = Math.max(totalHeight + lineHeight, lineHeight); maxWidth = Math.max(lineWidth, maxWidth); <!--totalHeight 跟 maxWidth都是FlowLayout渴望获得的尺寸--> <!--至于合不合适,经过resolveSize再来判断一遍,固然,若是你非要按照本身的尺寸来,也能够设定,可是不太合理--> totalHeight = resolveSize(totalHeight + paddingBottom + paddingTop, heightMeasureSpec); lineWidth = resolveSize(maxWidth + paddingLeft + paddingRight, widthMeasureSpec); setMeasuredDimension(lineWidth, totalHeight); }复制代码

能够看到,设定自定义ViewGroup的尺寸其实只须要三部:

  • 测量全部子View,获取全部子View的尺寸
  • 根据自身特色计算所须要的尺寸
  • 综合考量须要的尺寸跟父控件传递的MeasureSpec,得出一个合理的尺寸

顶层View的MeasureSpec是谁指定

传递给子View的MeasureSpec是父容器根据本身的MeasureSpec及子View的布局参数所肯定的,那么根MeasureSpec是谁建立的呢?咱们用最经常使用的两种Window来解释一下,Activity与Dialog,DecorView是Activity的根布局,传递给DecorView的MeasureSpec是系统根据Activity或者Dialog的Theme来肯定的,也就是说,最初的MeasureSpec是直接根据Window的属性构建的,通常对于Activity来讲,根MeasureSpec是EXACTLY+屏幕尺寸,对于Dialog来讲,若是不作特殊设定会采用AT_MOST+屏幕尺寸。这里牵扯到WindowManagerService跟ActivityManagerService,感兴趣的能够跟踪一下WindowManager.LayoutParams ,后面也会专门分析一下,好比,实现最简单试的全屏的Dialog就跟这些知识相关。

相关文章
相关标签/搜索