Android自定义控件之测量onMeasure

一,写在前面

当Android原生控件无法满足开发需求时,需要自己来创造view,自定义控件。自定义控件分三步来完成:测量(onMeasure),布局(onLayout),绘制(onDraw)。今天主要介绍自定义流程的第一步-测量,通常一个布局文件的控件的简单嵌套,显示如下:
Android自定义控件之测量onMeasure_第1张图片
LinearLayout里面有两个子view:TextView,RelativeLayout(里面又有两个TextView)。 我们知道自定义控件无非是extends View, extends ViewGroup, extends 容器控件/非容器控件,因此提取出其中的View.java, FrameLayout.java, TextView.java进行分析。下面展示涉及类,方法的结构图,看不懂互相之间关系没事,可以看完后面的分析,然后再回过头来看结构图。 结构图:
Android自定义控件之测量onMeasure_第2张图片
由上图可知,语法角度:子类可以重写onMeasure,只能继承View的measure,setMeasuredDimension方法。测量流程分为两种情况讨论:容器控件ViewGroup,原始的View(非容器控件)。原始的View测量,只需要测量自己的宽高;而容器控件需要先测量所有的子View的宽高,然后再测量自己的宽高。 看懂本篇文章,还需要大家自己先去研究下类View$MeasureSpec,相对比较简单,本文不描述MeasureSpec相关知识。

二,源码分析之View

先分析原始的View,打开View.java文件,查看measure方法:
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int oWidth  = insets.left + insets.right;
            int oHeight = insets.top  + insets.bottom;
            widthMeasureSpec  = MeasureSpec.adjust(widthMeasureSpec,  optical ? -oWidth  : oWidth);
            heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
        }
        if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
                widthMeasureSpec != mOldWidthMeasureSpec ||
                heightMeasureSpec != mOldHeightMeasureSpec) {

            // first clears the measured dimension flag
            mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;

            resolveRtlPropertiesIfNeeded();

            // measure ourselves, this should set the measured dimension flag back
            onMeasure(widthMeasureSpec, heightMeasureSpec);

            // flag not set, setMeasuredDimension() was not invoked, we raise
            // an exception to warn the developer
            if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
                throw new IllegalStateException("onMeasure() did not set the"
                        + " measured dimension by calling"
                        + " setMeasuredDimension()");
            }

            mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
        }

        mOldWidthMeasureSpec = widthMeasureSpec;
        mOldHeightMeasureSpec = heightMeasureSpec;
    }
调用view.measure(w,h)来测量控件宽高,那么这个方法是何时调用的呢?在该view的父控件测量自己宽高时调用。因为该view所在父容器在测量自己宽高时,会先测量子view的宽高,最终都会调用child.measure(w,h),最后才测量自己的宽高。后面分析容器控件的测量流程时,会一目了然。
主要分析measure(w,h)的两个关键点:
一,字段mPrivateFlags
1.1 字段mPrivateFlags在调用onMeasure(w,h)前,执行mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET,设置 成员变量mPrivateFlags的MEASURED_DIMENSION_SET位设置为0;
1.2 在onMeasure(w,h)执行完成后,判断if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET)决定是否抛出IllegalStateException异常;
二,实际测量方法onMeasure(w,h), 进入该方法查看源码:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
进入setMeasuredDimension(w,h)查看源码:
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int opticalWidth  = insets.left + insets.right;
            int opticalHeight = insets.top  + insets.bottom;

            measuredWidth  += optical ? opticalWidth  : -opticalWidth;
            measuredHeight += optical ? opticalHeight : -opticalHeight;
        }
        mMeasuredWidth = measuredWidth;
        mMeasuredHeight = measuredHeight;

        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
    }
setMeasuredDimension(w,h)是真正完成给view测量宽高,至于参数measuredWidth,measuredHeight是如何计算得来,下面会有分析。小结:测 量一个view实际上是给字段mMeasuredWidth,mMeasuredHeight设置值,最后执行mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET,将字段 mPrivateFlags的EASURED_DIMENSION_SET位设置为1。
mPrivateFlags更像是一个标志位,在onMeasure测量前设置一个值,在onMeasure执行的最后设置一个值,测量完成后判断mPrivateFlags的值。若前面没有执行setMeasuredDimension(w,h)完成测量,那么mPrivateFlags值则不会重新设置,判断mPrivateFlags时会执行if语句中内容,抛出IllegalStateException异常。继续分析前面提到:参数measuredWidth,measuredHeight是如何计算得来?只分析宽度(高度计算方式类似),分析这样一段代码:getDefaultSize(getSuggestedMinimumWidth(),widthMeasureSpec),于是进入方法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;
    }
第二个参数measureSpec是父容器调用child.measure(w,h)传入的参数, measureSpec取决于父容器的measureSpec(爷爷容器给的建议值)和自身布局参数LayoutParams(eg:控件宽高,外边距,内边距等),后面会具体分析。这里只需要记住,measureSpec是父容器测量子View时给的建议值。这个建议值measureSpec配合第一个参数size,决定view的最终宽度,也就是getDefaultSize方法返回值。那么size是什么东西呢?查看getSuggestedMinimumWidth()源码:
protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }
mMinWidth的值取决于view的布局参数android:minWidth="",如果没有设置,则default为0;mBackground.getMinimumWidth()返回该view的背景图片需要最小宽度值。如果没有背景图片,则返回 mMinWidth,否则max(mMinWidth,mBackground.getMinimumWidth()取较大值。也就是说参数size:要么取布局参数中最小宽度(还可能为0),要么取背景图片所需最小宽度。 继续回到getDefaultSize方法来分析宽度(高度计算方式类似), 取出父控件给的建议值的测量大小specSize,测量模式specMode。
判断specMode,1,如果是模式AT_MOST / EXACTLY,返回specSize;2,如果specMode是UNSPECIFIED,则父容器不对子view做任何限制,返回size。(UNSPECIFIED这种测量模式一般不做分析,不用管它) 
注意 ,specMode是EXACTLY,说明父控件已经知道子view需要的精确值,那么直接使用specSize容易理解;那specMode是AT_MOST时,说明父控件给的建议值是一个子view可以使用的最大值(<=specSize),为什么直接返回specSize呢?这里肯定需要修改,因为测量模式为EXACTLY,说明子view宽度:要么是match_parent,要么是具体的值(100dp)。测量模式为AT_MOST,说明子View宽度:只可能是wrap_content。在使用这个自定义的view时,不能让match_parent和wrap_content的体现的效果一样。
于是可以得出结论:在extends View的自定义控件中,需要重写onMeasure(w,h),并单独判断MeasureSpec.getMode(w)为 AT_MOST时,返回一个宽度值(具体逻辑按需求来吧),高度同理!查看TextView源码,onMeasure方法有对specMode为AT_MOST进行处理。
原始的View(非容器控件)的测量,代码流程图大致如下,保存该图片到本地可以清晰展示信息哦!
Android自定义控件之测量onMeasure_第3张图片

三,源码分析之容器控件

接下来分析 容器控件 测量流程,前面说到容器控件(继承ViewGroup)的测量过程:先测量所有子view,然后再测量容器控件本身。每种容器控件测量的细节不尽相同,但都遵循上面的方式。阅读LinearLayout源码时,发现里面if条件判断极为恶心,于是本篇以FrameLayout为例子分析容器控件的测量流程。首先贴上涉及的类,方法结构图如下,可以看完后面分析回过头来看此图。
Android自定义控件之测量onMeasure_第4张图片
查看FrameLayout源码,分析容器控件的测量过程:
@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
	int count = getChildCount();
        // ...code
        int maxHeight = 0;
        int maxWidth = 0;
        int childState = 0;

        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                maxWidth = Math.max(maxWidth,
                        child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                maxHeight = Math.max(maxHeight,
                        child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
                childState = combineMeasuredStates(childState, child.getMeasuredState());
                if (measureMatchParentChildren) {
                    if (lp.width == LayoutParams.MATCH_PARENT ||
                            lp.height == LayoutParams.MATCH_PARENT) {
                        mMatchParentChildren.add(child);
                    }
                }
            }
        }

        // Account for padding too
        maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
        maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();

        // Check against our minimum height and width
        maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

        // Check against our foreground's minimum height and width
        final Drawable drawable = getForeground();
        if (drawable != null) {
            maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
            maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
        }

        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                resolveSizeAndState(maxHeight, heightMeasureSpec,
                        childState << MEASURED_HEIGHT_STATE_SHIFT));
    }
执行for循环遍历一个存储子View的对象数组,调用measureChildWithMargins方法,首先测量子View,该方法是从父类ViewGroup继承过来。然后调用setMeasuredDimension方法测量容器控件自己的宽高,该方法从爷爷类View继承过来。先来分析 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);
    }
参数child-->子View;参数parentWidthMeasureSpec,parentHeightMeasureSpec--容器控件的MeasureSpec;参数widthUsed,heightUsed-->容器控件中已被使用的宽/高的数值,这里为0。
执行getChildMeasureSpec(parentWidthMeasureSpec,mPaddingLeft+mPaddingRight+ lp.leftMargin +lp.rightMargin+widthUsed, lp.width)获取子View的宽度测量建议值,这个值最终要传入到child.measure(w,h)中,开始测量子View。 从该方法参数可知:测量时,子View的MeasureSpec值由两部分组成:( parentWidthMeasureSpec-->父控件的MeasureSpec),以及子View本身的布局参数LayoutParams决定的。 其中LayoutParams中,涉及子view的宽高值,外边距margin。至于mPaddingLeft 是指FrameLayout与其内容之间的距离,字段继承于类View。
接下来分析如何合成子View的MeasureSpec,上面已经得出结论,这里从代码角度具体分析,查看getChildMeasureSpec的源码:
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 = 0;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = 0;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }
参数spec是FrameLayout的MeasureSpec,就是子view的爷爷给爸爸的测量建议值;参数padding是子View的外边距margin与FrameLayout的padding相加;参数childDimession是子view的宽/高值,由lp.width/lp.height得到。 当爸爸FrameLayout的测量模式specMode为EXACTLY时,里面还要分三种情况讨论:
1,当 childDimension >= 0( android:layout_width="50dp" ),resultSize为50dp,resultMode为精确EXACTLY;
2,当 childDimension == LayoutParams.MATCH_PARENT时,由于父容器specMode是精确的,子view又填充所有空间,那么resultSize大小就为 size = Math.max(0, specSize - padding),有具体数值,属于精确模式Exactly;
3,当 childDimension == LayoutParams.WRAP_CONTENT时,子view要小于等于size,于是大小为size,模式为AT_MOST。 爸爸FrameLayout的测量模式为 AT_MOST,UNSPECIFIED的情况,就不再具体分析 了。最后,使用MeasureSpec合并resultSize,resultMode。
至于,child.measure(w,h)的继续分析,无非就是两种情况:child如果是容器控件,则继续重复上面的测量流程;如果child是一个原始的view,那就是进入文章前半部分的测量流程。
容器控件测量自己,调用方法setMeasuredDimension,由方法resolveSizeAndState的返回值,得到测量的宽高大小(不包含测量模式specMode)。这里不再分析resolveSizeAndState方法,跟原始的view在测量时的getDefaultSize方法很类似,前面已经分析了getDefaultSize方法。 子view的测量后宽高,影响到了容器控件测量自己,这也是为什么要先测量所有子view,然后才测量容器控件自己。
下面展示了代码走向流程图:
Android自定义控件之测量onMeasure_第5张图片
这里,原始的view(非容器控件)与容器控件的测量流程分析完毕了。















你可能感兴趣的:(android)