Android View 框架(2)-- measure

本篇你将了解到:

  1. LayoutParamsMeasureSpec 的作用和使用场景
  2. 父 View 如何使用 MeasureSpec 影响子 View 的测量
  3. 重写 onMeasure 的作用

View 的绘制过程,主要体现在 onMeasure()onLayout()onDraw() 这三个方法,这三部对应着一个 View 的测量,布局,绘制三个步骤。

在讲 onMeasure() 方法之前,我们先讲一下 Android 中,测绘中常用的参数类 -- LayoutParamsMeasureSpec

LayoutParams

LayoutParams 类只简单的描述了宽高,他们往往被设置成以下这三种值:

  1. 一个确定的值;
  2. FILL_PARENT,即填满父容器;
  3. WRAP_CONTENT,刚刚好包裹组件;

常见的使用方式如下:


// FrameLayout 的子 View 
FrameLayout.LayoutParams lytp = new FrameLayout.LayoutParams(80, LayoutParams.WRAP_CONTENT);
lytp .gravity = Gravity.CENTER;
btn.setLayoutParams(lytp);


// RelativeLayout 的子 View
RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); 
lp.addRule(RelativeLayout.ALIGN_PARENT_RIGHT, RelativeLayout.TRUE); 
lp.addRule(RelativeLayout.ALIGN_PARENT_TOP, RelativeLayout.TRUE); 
btn1.setLayoutParams(lp);

MeasureSpec

MeasureSpec 是父控件提供给子 View 的一个参数,作为设定自身大小参考,而子 View 也可以完全不参照这个值,设定自己的大小。

MeasureSpecsizemode 构成,size 代表着子 View 当前所在父布局的具体尺寸,其中 mode 包括以下三种:

  1. EXACTLY:父布局为子 View 指定确切大小,希望子 View 完全按照自己给定尺寸来处理。
  2. AT_MOST:父布局为子元素指定最大参考尺寸,希望子 View 的尺寸不要超过这个尺寸。
  3. UNSPECIFIED:父布局对子控件不加任何束缚,这种情况一般出现在 ScrollView 这种不限制大小的特殊父控件。

ViewGroup 如何测量子 View

看源码:

// 测量子 View
protected void measureChild(View child, int parentWidthMeasureSpec,
        int parentHeightMeasureSpec) {
    // 获取 child 的 LayoutParams
    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 的 MeasureSpec
 * @param spec 子 View MeasureSpec
 * @param padding 当前 View 的 padding 大小,需要被减去
 * @param childDimension 子 View LayoutParams 的height,width
 */
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    int specMode = MeasureSpec.getMode(spec);
    int specSize = MeasureSpec.getSize(spec);
    // 父 View 推荐的 Size 需要留有一部给 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;
        ...
        // 省略了 MeasureSpec.AT_MOST 和 MeasureSpec.UNSPECIFIED
    }
}

看完源码后,大致能了解到测量是由父 View 的 MeasureSpec 和子 View 的 LayoutParams 一起决定的。更具体的对应关系如下:

  1. 子 View 指定大小值时:

    • Mode = EXACTLY
    • Size = 指定的大小
  2. 子View指定为 MATCH_PARENT 时:

    • Mode = 父View此时的模式
    • Size = 父View的大小 – padding
  3. 子View指定为 WRAP_CONTENT 时:

    • Mode = AT_MOST
    • Size = 父View的大小 – padding

根据以上的规律,父 View 一层一层的向下设置子 View 的 MeasureSpec。那么子 View 是如何根据父 View 给的 MeasureSpec,确定自身的大小呢?答案就在下面马上要讲的 onMeasure()方法里。

为什么要重写 onMeasure ?

调用 setMeasuredDimension 后,正式的确定了当前 View 的大小,那默认的onMeasure是怎么去调用的呢?

 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
    
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;
    // AT_MOST 和 EXACTLY 使用同一种处理方式
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    }
    return result;
}

View 的默认 onMeasure 方法中,可以看到 MeasureSpec.AT_MOSTMeasureSpec.EXACTLY 方法处理的方式是一样的。这样导致,自定义的 View 使用 WRAP_CONTENTMATCH_PARENT 的效果是一样的,都会撑满整个父容器。

在某些情况下,我们并不想要默认的那种效果,这时候我们就可以根据自己的需求,去重写 onMeasure 方法。
这里我们简单的重写一下,实现功能如下:

  1. 如果指定高度和宽度的大小,那么结果就是这个值;
  2. 如果设置为 MATCH_PARENT ,就填满父容器;
  3. 如果设置为 WRAP_CONTENT ,就在父容器建议值和本 View 设置的建议值取最小值。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(doLocalMeasure(60, widthMeasureSpec),
            doLocalMeasure(30, heightMeasureSpec));
}

int doLocalMeasure(int defaultSize, int measureSpec) {
    int result = defaultSize;
    int mode = MeasureSpec.getMode(measureSpec);
    int size = MeasureSpec.getSize(measureSpec);
    switch (mode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.EXACTLY:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
            result = Math.min(result, size);
            break;
    }
    return result;
}

小结

这一章简单的讲了 View 的测量流程,以及重写 onMeasure 方法的作用。
下一篇:Android View 框架(3)-- layout

你可能感兴趣的:(Android View 框架(2)-- measure)