十九、 View 的工作原理(3)--- View 的工作流程之 measure 过程

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/yz_cfm/article/details/90732765

    前面说过了,View 的三大流程 --- measure 过程、layout 过程、draw 过程,即测量、布局和绘制。其中 measure 确定 View 的测量宽/高,layout 确定 View 的最终宽/高和四个顶点的位置,而 draw 则将 View 绘制到屏幕上。

    这里我们要学习的是 measure 过程,它分为两种情况:

    1. 针对一个原始的 View,那么它只需要通过 measure() 方法就完成了其测量过程。

    2. 针对一个 ViewGroup,它除了完成自己的测量过程外,还会遍历去调用所有子元素的 measure() 方法,各个子元素再递归下去执行这个流程。

    下面我们分别来看看这两种情况是怎么完成 measure 过程的:

    1. View 的 measure 过程:

    从 View 中的 measure() 方法开始分析:

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    ...
    if (cacheIndex < 0 || sIgnoreMeasureCache) {
        // measure ourselves, this should set the measured dimension flag back
        onMeasure(widthMeasureSpec, heightMeasureSpec);
        ...
    }
    ...
}

    上面可以看到,measure() 的参数为 MeasureSpec(上一篇文章分析了它具体的转换过程),并且它是一个 final 类型的方法,所以在子类中我们不能重写它,在它内部会调用 onMeasure() 方法,接着看看 onMeasure() 方法:

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

    可以看到,它是 protected 修饰的,所以继承自 View 的控件可以重写该方法。onMeasure() 方法中涉及四个方法,setMeasureDimension() 、getDefaultSize()、getSuggestedMinimumWidth()、getSuggestedMinimumHeight(),下面分别看看它们的作用:

    setMeasureDimension():

/*
* 源码注释说明,这个方法作用就是存储/设置测量后的宽和高
* */
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
    ...
}

    所以我们接着看 getDefaultSize() 方法,它需要 getSuggestedMinimumWidth() 和 getSuggestedMinimumHeight() 方法的结果作为参数,所以我们先看这两个方法:

protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

protected int getSuggestedMinimumHeight() {
        return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}

    上面两个方法类似,以 getSuggestedMinimumWidth() 为例,如果 View 没有设置背景,那么方法返回值为 mMinWidth,而 mMinWidth 对应于 android:minWidth 这个属性所指定的值,如果该属性没有指定则默认为 0;如果 View 设置了背景,那么方法返回值为 mMinWidth 和 mBackground.getMinimumWidth() 之间的最大值,下面看一下 mBackground.getMinimumWidth() :

private Drawable mBackground;

// Drawable.java
/* 如果这个 Drawable 有原始宽度,则该方法返回这个 Drawable 的原始宽度,否则返回 0 */
public int getMinimumWidth() {
        final int intrinsicWidth = getIntrinsicWidth();
        return intrinsicWidth > 0 ? intrinsicWidth : 0;
    }

    所以,getSuggestedMinimumWidth() 的返回值为: 如果 View 没有设置背景,那么返回 mMinWidth,而 mMinWidth 对应于 android:minWidth 这个属性所指定的值,如果该属性没有指定则默认为 0;如果 View 设置了背景,则返回 android:minWidth 和该背景原始宽度这两个值之间的最大值。同理,getSuggestedMinimumHeight() 和它类似。

    最后,看一下确定最终测量宽/高的 getDefaultSize() 方法:

/**
* 返回的结果就是 View 测量后的宽/高的大小
*/
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;
}

    MeasureSpec.UNSPECIFIED 这种模式一般用于系统内部的测量过程,这种模式下,View 最终测量的宽/高大小就是上面分析的 getSuggestedMinimumWidth() 方法和 getSuggestedMinimumHeight () 方法的返回值。而我们所关注的是在 MeasureSpec.AT_MOST 和 MeasureSpec.EXACTLY 模式下,通过 getDefaultSize() 方法可以看到它最终返回的大小其实就是 View 的 MeasureSpec 中的 SpecSize 的大小。

    View 的 MeasureSpec 创建总结图表(在 getDefaultSize 方法之前获取到的,getDefaultSize 方法再进行一次判断后返回最终的大小):

    从 getDefaultSize() 方法我们可以看到,当我们在布局中使用 wrap_content 时,它的宽/高大小就是该 View 的 MeasureSpec 中的 SpecSize 的大小。通过上图可以看到这个大小就是 parentSize 的大小,而 parentSize 就是父容器当前剩余的空间大小。所以这种效果就和布局中使用 match_parent 效果一样,显然不应该,所以当我们自定义的控件直接继承自 View 时,就要重写 onMeasure() 方法(因为没法重写 measure() 方法),并在布局使用 wrap_content 时,处理一下 View 最终测量的宽/高大小,通常情况下,我们都是指定一个默认的内部宽/高,并在 wrap_content 时设置此宽/高即可。

    模板代码:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);

    if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
        // 其中 mWidth 和 mHeight 就是当布局中使用 wrap_content 时,我们所指定的默认的大小
        setMeasuredDimension(mWidth, mHeight);
    }else if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.EXACTLY){
        setMeasuredDimension(mWidth, heightSpecSize);
    }else if(widthSpecMode == MeasureSpec.EXACTLY && heightSpecMode == MeasureSpec.AT_MOST){
        setMeasuredDimension(widthSpecSize,mHeight);
    }
}

eg:

自定义的 View( CustomView.java ):

package com.cfm.viewtest;

public class CustomView extends View {
    public CustomView(Context context) {
        super(context);
    }

    public CustomView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public CustomView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);

        if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(100, 100);
        }else if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.EXACTLY){
            setMeasuredDimension(100, heightSpecSize);
        }else if(widthSpecMode == MeasureSpec.EXACTLY && heightSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(widthSpecSize, 100);
        }
    }
}

当我们在布局中使用 wrap_content 时的情况:

1. 

android:layout_width="match_parent"
android:layout_height="wrap_content"

效果图:

2.

android:layout_width="wrap_content"
android:layout_height="match_parent"

效果图:

3.

android:layout_width="wrap_content"
android:layout_height="wrap_content"

效果图:

4.

如果不重写这个方法,无论是 wrap_content 还是 match_parent,效果都是:

    2. ViewGroup 的 measure 过程:

    对于 ViewGroup 来说,除了完成自己的 measure 过程以外,还会遍历去调用所有子元素的 measure 方法,各个子元素再递归去执行这个过程。和 View 不同的是,ViewGroup 是一个抽象类,所以它没有重写 onMeasure() 方法,但是它提供了一个叫 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() 方法
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }

    接着看 measureChild() 方法:

protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {

        // 获取子元素的 LayoutParams
        final LayoutParams lp = child.getLayoutParams();

        // 计算子元素的 MeasureSpec
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);

        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);

        // 调用子元素的 measure() 方法进行测量
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

    从上面的分析过程我们可以看到,ViewGroup 并没有定义其测量的具体过程,这是因为 ViewGroup 是一个抽象类,其测量过程的 onMeasure() 方法需要各个子类去具体实现。为什么 ViewGroup 不像 View 一样对其 onMeasure() 方法做统一的实现呢?这是因为不同的 ViewGroup 子类常作为父类容器使用,比如 LinearLayout、RelativeLayout 等等,它们有不同的布局特性,这导致它们的测量细节各不相同,所以 ViewGroup 无法做统一实现。下面我们分析一下 Android 中提供的继承了 ViewGroup 的 LinearLayout 中 onMeasure() 的具体实现,以此来分析 ViewGroup 的 measure() 过程。

    首先我们看一下 LinearLayout 中的 onMeasure 方法:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    if (mOrientation == VERTICAL) {
        // 方向为竖直方向时
        measureVertical(widthMeasureSpec, heightMeasureSpec);
    } else {
        measureHorizontal(widthMeasureSpec, heightMeasureSpec);
    }
}

    我们以竖直布局来分析,所以接下来看一下 measureVertical() 方法:

void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
    ...
    for (int i = 0; i < count; ++i) {
        // 获取子 View
        final View child = getVirtualChildAt(i);

        // 获取子 View 的 LayoutParams
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();

        // 对每个子 View 执行 measureChildBeforeLayout() 方法
        measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
                heightMeasureSpec, usedHeight);

        final int childHeight = child.getMeasuredHeight();
        final int totalLength = mTotalLength;

        // 存储 LinearLayout 在竖直方向的初步高度,每测量一个子元素,mTotalLength 就会增加。
        // 增加的部分主要包括子 View 的高度,以及子 View 在竖直方向(上、下)的 margin等
        mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
                lp.bottomMargin + getNextLocationOffset(child));
    }

    // 上面对其内部的子 View 测量完毕后,LinearLayout 会根据子元素的情况来测量自己的大小
    //  mTotalLength 增加 LinearLayout 在竖直方向上的 padding
    mTotalLength += mPaddingTop + mPaddingBottom;
    int heightSize = mTotalLength;

    // Check against our minimum height
    heightSize = Math.max(heightSize, getSuggestedMinimumHeight());

    // Reconcile our calculated size with the heightMeasureSpec
    int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
    heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
    ...
    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
            heightSizeAndState);
}

   总体来说,针对竖直的 LinearLayout 而言,它在水平方向的测量遵循 View 的测量过程,在竖直方向的测量为: 如果它的布局中高度采用的是 match_parent 或者具体数值,那么它的测量过程和 View 一致,即高度为 SpecSize,如果它的布局中高度采用的是 wrap_content,那么它的高度是所有子元素所占用的高度总和,但是仍然不能超过它的父容器的剩余空间,并且它的最终高度还需要考虑它在竖直方向上的 padding。

    measure 过程完成之后,我们就可以通过 getMeasuredWidth() 和 getMeasuredHeight() 方法获取到 View 的测量宽/高。但是在某些极端情况下,系统可能需要多次 measure 才能确定最终的测量宽/高,所以建议在 onLayout() 方法中去获取 View 的测量宽/高或最终宽/高。

猜你喜欢

转载自blog.csdn.net/yz_cfm/article/details/90732765