Android View的工作流程 - measure过程

2023-07-28  本文已影响0人  BlueSocks

1. View的工作流程概述

View的绘制流程主要是指View的 measure layout draw 过程:

View的绘制流程是从ViewRootImplperformTraversals方法开始的,performTraversals会依次调用performMeasureperformLayoutperformDraw三个方法:

2. measure过程概述

measure过程主要是为了确定View的测量宽/高,理解View测量过程的重点在于理解MeasureSpec和onMeasure方法。

measure的传递过程:

  1. performMeasure调用DecorViewmeasure方法;
  2. DecorViewmeasure方法调用onMeasure方法,在onMeasure方法中遍历子元素,并调用子元素的measure方法
  3. 子元素measure方法被调用后,重复过程2,如此反复,完成整个View树的遍历。

3. MeasureSpec

View的Measure过程主要是为了确定View的测量宽/高,而View的测量宽/高的确定是和MeasureSpec有关的,所以在了解View的测量过程前,需要先了解MeasureSpec

MeasureSpec代表一个32位的int值,高2位代表SpecMode,低30位代表SpecSizeSpecMode是指测量模式,而SpecSize是指在某种模式下的规格大小。MeasureSpec通过将SpecModeSpecSize打包成一个int值来避免过多的对象内存分配,为了方便操作,其提供了打包和解包的方法。它的定义如下:

public static class MeasureSpec {
    private static final int MODE_SHIFT = 30;
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;
    public static final int EXACTLY     = 1 << MODE_SHIFT;
    public static final int AT_MOST     = 2 << MODE_SHIFT;

    /**
     * 将SpecMode和SpecSize打包成一个int值,避免过多的对象内存分配
     * */
    public static int makeMeasureSpec(int size, int mode) {
        if (sUseBrokenMakeMeasureSpec) {
            return size + mode;
        } else {
            return (size & ~MODE_MASK) | (mode & MODE_MASK);
        }
    }

    /**
     * 将打包后的int值解包,获取SpecMode
     * */
    public static int getMode(int measureSpec) {
        return (measureSpec & MODE_MASK);
    }

    /**
     * 将打包后的int值解包,获取SpecSize
     * */
    public static int getSize(int measureSpec) {
        return (measureSpec & ~MODE_MASK);
    }
}

SpecMode有三类,如下:

4. MeasureSpec和LayoutParams的对应关系

View的MeasureSpec由父容器和View自身的LayoutParams共同决定:

4.1. DecorView的MeasureSpec

先来看下DecorView的MeasureSpec的创建过程,在ViewRootImpl中的measureHierarchy方法中有如下一段代码:

/**
 * @param desiredWindowWidth    屏幕宽度
 * @param desiredWindowHeight   屏幕高度
 * @param lp                    WindowManager.LayoutParams
 */
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

接着看getRootMeasureSpec方法的实现:

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.
            //窗口无法调整大小。强制根视图为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;
}

根据上述代码,DecorView的MeasureSpec的产生过程就很明确了,其由它的LayoutParams屏幕尺寸共同确定,如下:

4.2. 普通View的MeasureSpec

对于普通View来说,View的measure过程由ViewGroup传递而来,先看下ViewGroup的measureChildWithMargins方法:

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);
}

measureChildWithMargins方法会对子元素进行measure,在调用子元素的measure方法之前会先通过getChildMeasureSpec方法来得到子元素的MeasureSpec。从代码来看,子元素MeasureSpec的创建与父容器的MeasureSpec和子元素本身的LayoutParams有关,此外还和View的marginpadding有关,具体情况在ViewGroup的getChildMeasureSpec方法中,如下:

/**
 * 根据父容器的MeasureSpec和View自身的LayoutParams来确定子元素的MeasureSpec
 * @param spec 父容器的MeasureSpec
 * @param padding 父容器已占用的空间大小
 * @param childDimension View本身的LayoutParams,如MATCH_PARENT、WRAP_CONTENT、100dp
 * @return 返回View的MeasureSpec
 * */
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);

    //View最终的SpecSize
    int resultSize = 0;
    //View最终的SpecMode
    int resultMode = 0;

    //判断父容器的SpecMode,EXACTLY、AT_MOST、UNSPECIFIED
    switch (specMode) {
        case MeasureSpec.EXACTLY:
            //判断View自身的LayoutParams,MATCH_PARENT、WRAP_CONTENT、固定值(如100dp)
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == ViewGroup.LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == ViewGroup.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 == ViewGroup.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 == ViewGroup.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 them have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == ViewGroup.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 == ViewGroup.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;
    }
    //noinspection ResourceType
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

getChildMeasureSpec方法清楚的展示了普通View的MeasureSpec的创建规则。最后附上getChildMeasureSpec对应的一个表格梳理,其中parentSize是指父容器中目前可用的大小:

父容器的SpecMode-EXACTLY 父容器的SpecMode-AT_MOST 父容器的SpecMode-UNSPECIFIED
自身LayoutParams-固定值 EXACTLY
childSize EXACTLY
childSize EXACTLY
childSize
自身LayoutParams-match_parent EXACTLY
parentSize AT_MOST
parentSize UNSPECIFIED
0
自身LayoutParams-wrap_cotent AT_MOST
parentSize AT_MOST
parentSize UNSPECIFIED
0

5. LinearLayout的measure过程

ViewGroup没有实现onMeasure方法,它提供了一个叫measureChildren的方法,measureChildren的实现比较简单,就是遍历所有的子元素,取出子元素的LayoutParams,然后再通过getChilMeasureSpec来创建子元素的MeasureSpec,最后将子元素的MeasureSpec直接传递给子元素的measure方法来进行测量。getChildMeasureSpec方法也在分析MeasureSpec的时候介绍过了。

下面我们主要分析下LinearLayout的onMeasure方法,如下所示:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    if (mOrientation == VERTICAL) {
        measureVertical(widthMeasureSpec, heightMeasureSpec);
    } else {
        measureHorizontal(widthMeasureSpec, heightMeasureSpec);
    }
}

LinearLayout竖直布局和水平布局的测量过程是类似的,分析其中一个就可以了,如下:

void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
    //记录LinearLayout的总高度
    mTotalLength = 0;
    ...

    //遍历所有子元素
    for (int i = 0; i < count; ++i) {
        final View child = getVirtualChildAt(i);
        //获取子元素的LayoutParams
        final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();

        ...

        //measureChildBeforeLayout方法调用了measureChildWithMargins方法,
        //measureChildWithMargins方法在介绍MeasureSpec的时候已经介绍过了,
        //就是根据LinearLayout的margin、MeasureSpec和子元素本身的LayoutParams
        //来确定子元素的MeasureSpec,并调用子元素的measure方法
        measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
                heightMeasureSpec, usedHeight);

        //measureChildBeforeLayout已经调用了子元素的measure方法,此时可以获取到子元素的测量高
        final int childHeight = child.getMeasuredHeight();
        final int totalLength = mTotalLength;
        //最后使用mTotalLength累加子元素的高度
        mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
                lp.bottomMargin + getNextLocationOffset(child));
        ...
    }
    ...

    //加上LinearLayout自身的padding,
    mTotalLength += mPaddingTop + mPaddingBottom;
    int heightSize = mTotalLength;
    // Check against our minimum height
    heightSize = Math.max(heightSize, getSuggestedMinimumHeight());

    //计算LinearLayout的最终大小,
    // resolveSizeAndState方法就是根据LinearLayout的MeasureSpec和记录的mTotalLength
    // 得到LinearLayout最终的测量高
    int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
    heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
    ...
    //设置LinearLayout的测量宽/高
    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
            heightSizeAndState);

}

在measureVertical方法中,LinearLayout遍历所有子元素,通过measureChildBeforeLayout确定子元素的MeasureSpec并调用子元素的measure方法,完成子元素的测量。在遍历过程中,还会根据子元素的高度来测量自己的大小,即mTotalLength,最后LinearLayout根据自身的MeasureSpec和mTotalLength确定自己的测量高,并使用setMeasuredDimension方法设置自己的测量高。

6. View的Measure过程

View的measure过程由其measure方法来完成,measure方法是一个final类型的方法,这意味着子类不能重写此方法,在View的measure方法中会调用View的onMeasure方法,onMeasure方法如下:

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

setMeasuredDimension方法会设置View宽/高的测量值,因此我们只需要看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;
    //如果是AT_MOST或者EXACTLY,返回specSize。
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    }
    return result;
}

getDefaultSize方法中主要根据View的specMode来确定View的测量宽/高,对于我们来说,只需要看AT_MOST和EXACTLY这两种情况就可以了。在specMode是AT_MOST或者EXACTLY的时候,getDefaultSize返回的大小就是specSize,即View的测量宽/高就是specSize。在EXACTLY模式下,specSize是一个固定大小,而在AT_MOST模式下,specSize是多少?回顾上面根据MeasureSpec的创建规则:

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);

    //判断父容器的SpecMode,EXACTLY、AT_MOST、UNSPECIFIED
    switch (specMode) {
        case MeasureSpec.EXACTLY:
            //判断View自身的LayoutParams,MATCH_PARENT、WRAP_CONTENT、固定值(如100dp)
            ...
            else if (childDimension == ViewGroup.LayoutParams.WRAP_CONTENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
        case MeasureSpec.AT_MOST:
            ...
            else if (childDimension == ViewGroup.LayoutParams.WRAP_CONTENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
    }
    //noinspection ResourceType
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

可以发现,如果View在布局中使用wrap_cotent,,它的大小就是父容器可用大小,这种效果和在布局中使用match_parent一样,所以我们在自定义View的时候,要处理View宽/高是wrap_content的情况,重写View的onMeasure方法,如下:

@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.getMode(heightMeasureSpec);

    //对于wrap_content情况,设置一个默认的宽/高
    if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
        setMeasuredDimension(mDefaultWidth, mDefaultHeight);
    }else if (widthSpecMode == MeasureSpec.AT_MOST){
        setMeasuredDimension(mDefaultWidth, heightMeasureSpec);
    }else if (heightSpecMode == MeasureSpec.AT_MOST){
        setMeasuredDimension(widthMeasureSpec, mDefaultHeight);
    }
    //其它情况还是沿用系统的测量值
}

上一篇下一篇

猜你喜欢

热点阅读