View的绘制-measure流程详解

3,547 阅读9分钟

目录

作用

用于测量View的宽高,在执行 layout 的时候,根据测量的宽高去确定自身和子 View 的位置。

基础知识

在 measure 过程中,设计到 LayoutParams 和 MeasureSpec 这两个知识点。 这里我们简单说一下,如果还有不明白之处,Google it!

LayoutParams

简单来说就是布局参数,包含了 View 的宽高等信息。每一个 ViewGroup 的子类都有相对应的 LayoutParams,如:LinearLayout.LayoutParams、RelativeLayout.LayoutParams。可以看出 LayoutParams 是 ViewGroup 子类的内部类。

含义
LayoutParams.MATCH_PARENT等同于在 xml 中设置 View 的属性为 match_parent 和 fill_parent
LayoutParams.WRAP_CONTENT等同于在 xml 中设置 View 的属性为 wrap_content

MeasureSpec

MeasureSpec 是 View 的测量规则。通常父控件要测量子控件的时候,会传给子控件 widthMeasureSpec 和 heightMeasureSpec 这两个 int 类型的值。这个值里面包含两个信息,SpecModeSpecSize。一个 int 值怎么会包含两个信息呢?我们知道 int 是一个4字节32位的数据,在这两个 int 类型的数据中,前面高2位是 SpecMode ,后面低30位代表了 SpecSize

mode 有三种类型:UNSPECIFIEDEXACTLYAT_MOST

测量模式解释适用情况
EXACTLY精准模式,父容器已经精确的检测出了子View的大小,子view的大小就是MeasureSpect.getSize()的值.a.子View的LayoutParameter使用具体的值(如:宽高为100dp),不管父容器的spectMode为什么,系统返回给子View的mode为EXACTLY,系统返回给子View的大小为子View自己指定的大小(100dp) b.子View的LayoutParams采用match_parent并且父容器的mode为EXACTLY,那么子View的mode即为EXACTLY,子View大小为父容器剩余的大小
AT_MOST父容器期望对子View的最大值做了限定c.子View的LayoutParams采用match_parent并且父容器的mode为AT_MOST,那么子View的mode即为AT_MOST,子View大小为父容器剩余的大小 d.当子View的LayoutParams采用wrap_content时并且父容器的mode为EXACTLY或者AT_MOST时,子View的Mode就为AT_MOST,子View的specSize就为该父容器剩余的大小
UNSPECIFIED父容器不限定大小,子View想多大就多大e.当子View的LayoutParams采用wrap_content时并且父容器的mode为UNSPECIFIED时,子View的Mode就为UNSPECIFIED,子View的大小不做限制一般用在系统内部,比如:Scrollview、ListView。

我们怎么从一个 int 值里面取出两个信息呢?别担心,在 View 内部有一个 MeasureSpec 类。这个类已经给我们封装好了各种方法:

//将 Size 和 mode 组合成一个 int 值
int measureSpec = MeasureSpec.makeMeasureSpec(size,mode);
//获取 size 大小
int size = MeasureSpec.getSize(measureSpec);
//获取 mode 类型
int mode = MeasureSpec.getMode(measureSpec);

具体实现细节,可以查看源码,or Google it!

执行流程

注:以下涉及到源码的,都是版本27的。

我们知道,一个视图的根 View 是 DecorView。在我们开启一个 Activity 的时候,会将 DecorView 添加到 window 中,同时会创建一个 ViewRootImpl 对象,并将 ViewRootImpl 对象和 DecorView 对象建立关联。ViewRootImpl 是连接 WindowManager 和 DecorView 的纽带。具体 DecorView 详解可以看 这篇文章

View的绘制流程就是从 ViewRootImpl 开始的。在它的 performTraversals()方法中执行了 performMeasure()performLayoutperformDraw方法。而这三个方法又分别执行了view.measure()view.layout()view.draw()方法,从而开始执行整个 View 树的绘制流程

ViewGroup 中 measure 的执行流程

ViewGroup 本身是继承 View 的,这是我们大家都知道的。在 ViewGroup 中并没有找到 measure 方法,那么就在它的父类 View 中找,具体源码如下:

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    /*....省略代码....*/
    if (forceLayout || needsLayout) {
     /*....省略代码....*/
        if (cacheIndex < 0 || sIgnoreMeasureCache) {
            // measure ourselves, this should set the measured dimension flag back
            //执行 onMeasure 方法
            onMeasure(widthMeasureSpec, heightMeasureSpec);
        }
        /*....省略代码....*/
     
    }
    /*....省略代码....*/
}

我们可以看出,measure 方法是被 final 修饰了,子类不能重写。measure 方法中调用了 onMeasure 方法。

然后我们继续寻找 onMeasure 方法,会发现在 ViewGroup 中并没有实现 onMeasure 方法,只有在 View 中发现了 onMeasure 方法。WTF?难道 ViewGroup 的 onMeasure 也会走 View 中的方法?并不是的,ViewGroup 本身是一个抽象类,在 Android SDK 中有很多它的子类,如:LinearLayout、RelativeLayout、FrameLayout等等,这些控件的特性都是不一样的,测量规则自然也都不一样。它们都各自实现了 onMeasure 方法,然后去根据自己的特定测量规则进行控件的测量。(PS:如果我们的自定义控件继承 ViewGroup 的时候,一定要重写 onMeasure 方法的,根据需求来制定测量规则)

这里我们以 LinearLayout 为例,来进行源码分析:

//LinearLayout 类
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    if (mOrientation == VERTICAL) {
    //如果方向是垂直方向,就进行垂直方向的测量
        measureVertical(widthMeasureSpec, heightMeasureSpec);
    } else {
    //进行水平方向的测量
        measureHorizontal(widthMeasureSpec, heightMeasureSpec);
    }
}

measureVertical 和 measureHorizontal 过程类似,我们对 measureVertical 进行分析。(以下源码有所删减)

//LinearLayout 类
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
    mTotalLength = 0;
    float totalWeight = 0;

    final int count = getVirtualChildCount();
    //获取 LinearLayout 的宽高模式 SpecMode
    final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    final int heightMode = MeasureSpec.getMode(heightMeasureSpec);

    boolean skippedMeasure = false;

    // See how tall everyone is. Also remember max width.
    //遍历子 View ,查看每一个子类有多高,并且记住最大的宽度。
    for (int i = 0; i < count; ++i) {
        final View child = getVirtualChildAt(i);
        if (child == null) {
        //measureNullChild() 恒返回 0,
            mTotalLength += measureNullChild (i);
            continue;
        }
        //如果子控件时 GONE 状态,就跳过,不进行测量。
        //也可以看出,如果子 View 是 INVISIBLE 也是要测量大小的。
        if (child.getVisibility() == View.GONE) {
        //getChildrenSkipCount 也是恒返回为 0 的。
           i += getChildrenSkipCount(child, i);
           continue;
        }

        //获取子控件的参数信息。
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();

        totalWeight += lp.weight;
        //子控件是否设置了权重 weight 
        final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
        if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
            final int totalLength = mTotalLength;
            mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
            //如果设置了权重,就将 skippedMeasure 标记为 true。
            //后面会根据 skippedMeasure 的值和其他条件来决定是否进行重新绘制。
            //所以说,在 LinearLayout 中使用了 weight 权重,会导致测量两次,比较耗时。
            //可以考虑使用 RelativeLayout 或者 ConstraintLayout
            skippedMeasure = true;
        } else {
            if (useExcessSpace) {
                lp.height = LayoutParams.WRAP_CONTENT;
            }

           //计算已经使用过的高度
            final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
            /*这句代码是关键,从字面意思就可以理解出,该方法是在 layout 
            之前进行子 View 的测量。*/
            measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
                    heightMeasureSpec, usedHeight);
            final int childHeight = child.getMeasuredHeight();
            //将这个 child View 的高度添加到总长度中。
            mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
                       lp.bottomMargin + getNextLocationOffset(child));
        }
    }
}

那么我们在查看 measureChildBeforeLayout 方法:

//LinearLayout 类
void measureChildBeforeLayout(View child, int childIndex,
        int widthMeasureSpec, int totalWidth, int heightMeasureSpec,
        int totalHeight) {
    measureChildWithMargins(child, widthMeasureSpec, totalWidth,
            heightMeasureSpec, totalHeight);
}

再查看 measureChildWithMargins 方法,最终来到了 ViewGroup 类:

//ViewGroup 类
protected void measureChildWithMargins(View child,
        int parentWidthMeasureSpec, int widthUsed,
        int parentHeightMeasureSpec, int heightUsed) {
        /*获取子 View 的布局参数 MarginLayoutParams 可以获取子 View 
        设置的 margin 属性。*/
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
    //获取子 View 宽度的 MeasureSpec 值。
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                    + widthUsed, lp.width);
    //获取子 View 高度的 MeasureSpec 值。
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                    + heightUsed, lp.height);

    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

在 ViewGroup 中还有一个方法为 measureChild(int widthMeasureSpec, int heightMeasureSpec)。这个方法和 measureChildWithMargins 作用一致,都是生成子 View 的 measureSpec。只是传参不同。

里面在获取子 View 宽高属性的时候,都是通过 getChildMeasureSpec 方法来获取的。这个方法是 ViewGroup 具体实现的,是根据自身的 measureSpec 和子 View 的 LayoutParams 来设置子 View 的 measureSpec 的主要过程。

//ViewGroup 类
/**
 * @param spec 父类的 measureSpec
 * @param padding 父类的 padding + 子类的 margin
 * @param childDimension 子 View 的 LayoutParams.width/LayoutParams.height 属性
 */
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    //获取父控件的测量模式 specMode
    int specMode = MeasureSpec.getMode(spec);
    //获取父控件的测量大小 SpecSize
    int specSize = MeasureSpec.getSize(spec);
    //获取父控件剩余的宽度/高度大小
    int size = Math.max(0, specSize - padding);
    //子 View 的测量大小
    int resultSize = 0;
    //子 View 的测量模式
    int resultMode = 0;

    switch (specMode) {
    // 父控件的宽高模式是精准模式 EXACTLY
    case MeasureSpec.EXACTLY:
        if (childDimension >= 0) {
            //如果子 View 的宽/高是具体的值(具体的 xxdp/px)
            //模式 mode 就设置为精准模式 EXACTLY,大小 size 就是具体设置的大小
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            //如果子 View 的宽/高是 MATCH_PARENT
            //模式 mode 就设置为精准模式 EXACTLY,大小 size 就是父控件剩余的空间
            resultSize = size;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            //如果子 View 的宽/高是 WRAP_CONTENT
            /*模式 mode 就设置为精准模式 AT_MOST,大小 size 就是父控件剩余的空间,
            子控件可以在在这个size大小范围内设置宽高*/
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

    // Parent has imposed a maximum size on us
    //父控件测量模式为 AT_MOST,会给子 View 一个最大的值
    case MeasureSpec.AT_MOST:
        if (childDimension >= 0) {
            //如果子 View 的宽/高是具体的值(具体的 xxdp/px)
            //模式 mode 就设置为精准模式 EXACTLY,大小 size 就是具体设置的大小
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            //如果子 View 的宽/高是 MATCH_PARENT
            /*模式 mode 就设置为精准模式 AT_MOST,大小 size 就是父控件剩余的空间,
            子控件可以在在这个size大小范围内设置宽高*/
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            //如果子 View 的宽/高是 MATCH_PARENT
            /*模式 mode 就设置为精准模式 AT_MOST,大小 size 就是父控件剩余的空间,
            子控件可以在在这个size大小范围内设置宽高*/
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

    // Parent asked to see how big we want to be
    //父控件不限制子 View 的宽高,一般用于 ListView、Scrollview
    //平时基本不用,暂不分析
    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;
    }
    //生成子 View 的 measSpec
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

以上就是 ViewGroup 根据自身 measureSpec 和 子 View 的 LayoutParams 生成子 View 的 measureSpec 的过程。具体总结如下: 以上就是 LinearLayout 测量子控件宽高的过程。

从上述表格我们也可以看出,当我们在自定义控件继承 View 的时候,还是要重写 View 的 onMeasure 方法来处理 wrap_content 的情况,如果不处理 wrap_content 的情况,wrap_content 的效果是和 match_parent 一样的,都是填充满父控件。可以在 xml 布局中直接添加一个 <View android:layout_width="match_parent" android:layout_height="wrap_content"/> 控件自行感受一下。

LinearLayout 测量完子控件后,根据子控件的宽高来设置自身的宽高:

void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
    // Add in our padding
    //添加自身的 padding 值
    mTotalLength += mPaddingTop + mPaddingBottom;

    int heightSize = mTotalLength;

    // Check against our minimum height
    //从 最小建议高度 和 heightSize 中取最大值,getSuggestedMinimumHeight 在后面有分析
    heightSize = Math.max(heightSize, getSuggestedMinimumHeight());

    int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
    /*....省略代码....*/
    //遍历完子控件后,来设置自身的宽高
    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
            heightSizeAndState);
}
//如果 LinearLayout 高为具体值,heightSizeAndState 就是具体的值
//否则是 子控件 的高度之和,但是也不能超过它的父容器的剩余空间。
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);
}

至此,我们可以得知,当 ViewGroup 生成子 View 宽/高的 measureSpec 后,开始调用子 View 进行测量。如果子 View 继承了 ViewGroup 就重复执行上述流程(各个不同的 ViewGroup 子类执行各自的 onMeasure 方法);如果是具体的 View,就开始执行具体 View 的 measure 过程。最后根据子控件的宽高和其他条件来决定自身的宽高。

View 中 measure 的执行流程

View 的 measure 具体源码在 ViewGroup 中已经分析过,这里主要分析 View 的 onMeasure 过程。

//View 类
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    //通过 getDefaultSize 获取宽高大小,设置为测量值。
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

getDefaultSize 具体源码

//View 类
/**
 * @param size 通过 getSuggestedMinimumWidth 获取的建议最小宽度
 * @param measureSpec 通过父控件生成的 measureSpec
 */
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:
    //如果是 UNSPECIFIED 就设置为建议最小值
        result = size;
        break;
    /*否则就都设置为通过父控件生成的值(如果子控件为具体的
    xxdp/px值,就是具体的值,如果不是就是父控件的剩余空间。具体可以查看上面的分析)*/
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    }
    return result;
}

//建议最小的值

//View 类
protected int getSuggestedMinimumWidth() {
    //判断是否有设置背景 Background 如果没有,建议最小值就是设置的 minWidth;
    //如果有,就取 mMinWidth 和 背景最小值 两者的最大值。
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

背景最小值是多少呢?点击查看源码,就来到了 Drawable 类。

//Drawable 类
public int getMinimumWidth() {
    //首先获取 Drawable 的原始宽度
    final int intrinsicWidth = getIntrinsicWidth();
    //如果有原始宽度,就返回原始宽度;如果没有,就返回 0
    //注: 比如 ShapeDrawable 就没有原始宽度,BitmapDrawable 有原始宽高(图片尺寸)
    return intrinsicWidth > 0 ? intrinsicWidth : 0;
}

至此,View的 measure 就分析完了。

DecorView 的 measureSpec 计算逻辑

可能我们会有疑问,如果所有子控件的 measureSpec 都是父控件结合自身的 measureSpec 和子 View 的 LayoutParams 来生成的。那么作为视图的顶级父类 DecorView 怎么获取自己的 measureSpec 呢?下面我们来分析源码:(以下源码有所删减)

//ViewRootImpl 类
private void performTraversals() {
    //获取 DecorView 宽度的 measureSpec 
    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
    //获取 DecorView 高度的 measureSpec
    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
    // Ask host how big it wants to be
    //开始执行测量
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}
//ViewRootImpl 类
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
    int measureSpec;
    switch (rootDimension) {
        case ViewGroup.LayoutParams.MATCH_PARENT:
            // Window can't resize. Force root view to be windowSize.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // Window can resize. Set max size for root view.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            // Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
    }
    return measureSpec;
}

windowSize 是 widow 的宽高大小,所以我们可以看出 DecorView 的 measureSpec 是根据 window 的宽高大小和自身的 LayoutParams 来生成的。

关于 MeasureSpec.UNSPECIFIED

UNSPECIFIED 未指定的意思,父控件不限制子控件的大小。

  • 这个模式什么时候会遇到?

    在列表中(如:RecyclerView、ListView、GridView等),由于列表是可以滑动的,item在 measure 的时候,如果 item 的宽或者高设置了 WRAP_CONTENT,那么接下来 itemView 在 onMeasure的时候就会收到 MeasureSpec.UNSPECIFIED。

    打开RecyclerView源码,会在getChildMeasureSpec方法里看到这么一句注释:

    "MATCH_PARENT can't be applied since we can scroll in this dimension, wrap instead using UNSPECIFIED."

    它想表达的是:在可滚动的ViewGroup中,不应该限制Item的尺寸(如果是水平滚动,就不限制宽度),为什么呢?

    因为是可以滚动的,就算Item有多宽,有多高,通过滚动也一样能看到滚动前被遮挡的部分。

  • 遇到以后怎么处理?以及注意事项。

    尺寸可以自己设置。但是也要按照基本原则。

    比如:ImageView 在 MeasureSpec.UNSPECIFIED 情况下设置尺寸。有图片的话,就按照图片的尺寸来设置宽高大小,当然也不会超过 maxWidth和maxHeight,也不会小于 minWidth 和 minHeight。如果没有内容就为0。

    TextView 处理 UNSPECIFIED 情况,是和 AT_MOST 一样的。有多少就显示多少。

总结

参考文档:

《Android开发艺术探索》第四章-View的工作原理

自定义View Measure过程 - 最易懂的自定义View原理系列(2)

图解View测量、布局及绘制原理