直接继承View来自定义控件时,需要重写onMeasure()方法并设置wrap_content时的大小 原理分析

来源:互联网 发布:wifi连接上没有网络 编辑:程序博客网 时间:2024/05/21 21:39

        之前在校学习的时候,一直没有在网上找到比较靠谱的解释,现在毕业了,编程能力也比之前有了不小的提高,就读了一些源代码,加上一些书上的解释,现在算是大体知道原因了吧!如果哪里说的不对,欢迎批评指正。

       在开始本篇的正文之前,请允许我先粗略的解释一下MeasureSpec的作用,对本篇的理解会有帮助,但是关于View绘制的流程,本篇暂时不多做介绍了,对View的绘制流程还不是很熟悉的同学,请先通过一些书籍或者其他的博客了解一下View绘制的流程。后期有时间的时候,我会整理一下View绘制的流程,然后发一篇博客,虽然现在网上相关的内容也有不少,但是看了许多之后,自身感受就是要么博客写的千篇一律,要么就是涵盖不全(虽然我是个渣渣,但是宝宝会努力的委屈),当然也有很多大神级的人物写的博客,还是很好的。(真心感谢一些大神的博客,收获颇丰)。下面开始:

      MeasureSpec可以理解成是View测量的说明书吧,一个View的MeasureSpce受到本身的LayoutParams以及父View的MeasureSpec的影响。 MeasureSpce里有两个比较重要的属性SpecMode和SpecSize,SpecMode可以理解成是View的测量模式,SpecSize代表的是View的测量大小。MeasureSpec很大程度上影响了一个View尺寸。

其中SpecMode有三类测量模式:

(1)UNSPECIFIED: 这个模式平时用的貌似不太多,查了一下资料,表示父容器不对View有任何限制,一般用于系统内部,其他的也就不再多提了。

(2)EXACTLY:精确模式,此时View的大小就是SpecSize的值,对应match_parent和具体的数值这两种。

(3)AT_MOST:最大化模式,此时View的大小不能超过SpecSize的大小,对应于wrap_content。

好了下面开始正文:为什么在直接继承View来自定义控件时,需要重写onMeasure()方法并设置wrap_content的大小奋斗

      我们应该都会了解过,View绘制的流程是从父View传递到子View的,对于ViewGroup来说,除了完成自己的measure过程之外,还要完成其所有子View的measure过程,因为对于不同的子View来说,测量的细节也是不相同的,ViewGroup不能够针对不同的子View做统一的measure,所以ViewGroup是一个抽象的类,把测量的过程交给了子View本身去测量,在ViewGroup中有一个measureChildren()方法来遍历所有的子View,执行子View的measure来进行子View的测量。以下是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(child, widthMeasureSpec, heightMeasureSpec);            }        }    }

我们可以看到在measureChildren()方法中,去遍历了每一个子View,并通过measureChild()方法来进行对子View的测量,以下是measureChild()方法的代码:

protected void measureChild(View child, int parentWidthMeasureSpec,            int parentHeightMeasureSpec) {        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);    }
我们可以看到,在measureChild()方法中,调用了getChildMeasureSpec()方法来获取width和height对应的MeasureSpec(稍后会再对getChildMeasureSpec()方法做介绍,在这个地方请先忽略,只知道是获取MeasureSpec的方法就行了。),然后作为参数,调用子View的measure()方法,即child.measure(childWidthMeasureSpec , childHeightMeasureSpec)方法来进行子View的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);        }        // Suppress sign extension for the low bytes        long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;        if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);        final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;        // Optimize layout by avoiding an extra EXACTLY pass when the view is        // already measured as the correct size. In API 23 and below, this        // extra pass is required to make LinearLayout re-distribute weight.        final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec                || heightMeasureSpec != mOldHeightMeasureSpec;        final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY                && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;        final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)                && getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);        final boolean needsLayout = specChanged                && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);        if (forceLayout || needsLayout) {            // first clears the measured dimension flag            mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;            resolveRtlPropertiesIfNeeded();            int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);            if (cacheIndex < 0 || sIgnoreMeasureCache) {                // measure ourselves, this should set the measured dimension flag back                onMeasure(widthMeasureSpec, heightMeasureSpec);                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;            } else {                long value = mMeasureCache.valueAt(cacheIndex);                // Casting a long to int drops the high 32 bits, no mask needed                setMeasuredDimensionRaw((int) (value >> 32), (int) value);                mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;            }            // 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("View with id " + getId() + ": "                        + getClass().getName() + "#onMeasure() did not set the"                        + " measured dimension by calling"                        + " setMeasuredDimension()");            }            mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;        }        mOldWidthMeasureSpec = widthMeasureSpec;        mOldHeightMeasureSpec = heightMeasureSpec;        mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |                (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension    }

该代码相对比较多,我们只看比较重要的部分,由以上代码我们可以看出measure()方法为final类型的方法,因此不被重写,我们可以看到在以上代码的第38行,调用了onMeasure()方法来进行View的测量,下面是onMeasure()方法的相关代码:

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

可以看到,在onMeasure()方法中调用了setMeasureDimension()方法,看这个方法的名字就就可以知道,setMeasureDimension()方法的作用是设置View测量后width和height的大小,这个方法的代码就不贴出来了,我们可以看到该方法的有两个参数,两个参数分别对代表View测量的width和height,两个参数的值都是getDefaultSize()方法的返回值,下面我们来看下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;    }
在getDefaultSize()中,首先根据传递过来的MeasureSpec,来获取对应的SpecMode和SpecSize,我们重点看下SpecMode的AT_MOST和EXACTLY模式,当View的SpecMode处于AT_MOST和EXACTLY这两种模式下的时候,其返回值result都是SpecSize的值。(结论1)

到这里,我先总结一下以上的流程:ViewGroup会以自身的MeasureSpec为参数,调用measureChildren()方法,遍历每一个子View,并以子View和自身的MeasureSpec为参数,调用measureChild()方法,在measureChild()中,会以自身的MeasureSpec为参数,调用getChildMeasureSpec()方法来获取子View的MeasureSpec,然后以获得的子View的MeasureSpec为参数,调用子View的measure()方法来进行子View的测量。在子View的measure()方法中,会去调用onMeasure()进行View的测量,在onMeasure()中会调用setMeasureDimension()方法来设置View测量的宽和高,该宽和高的值为getDefaultSize()方法的返回值,在getDefaultSize()方法中,当子View的MeasureSpec的specMode为AT_MOST和EXACTLY时,getDefaultSize()方法的返回值是子View的MeasureSpec中SpecSize的值。
下面我们来看一下上文中提到但是没有解释的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 = 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;        }        //noinspection ResourceType        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);    }
注意,该方法的第一个参数spec,即MeasureSpec,是父View的MeasureSpec,最后一个参数,childDimension对应的是wrap_content或者是match_parent或者是固定的值。

在该方法中,首先会获得父View的MeasureSpec中对应的specMode和specSize,然后对父View的specMode做判断,根据代码我们可以得出结论:不论父View的specMode是EXACTLY模式,还是AT_MOST模式,当childDimension == WRAP_CONTENT时,即我们设置View的layout_width或者layout_height为wrap_content时,最后得到的resultMode的结果都是AT_MOST模式,resultSize的值等于size的值,而size的值,请看上述代码的第5行,size = Math.max(0 , specSize - padding),即size的值是父View的specSize的值减去padding,也就是说,size的值是父View去掉padding之后剩余空间的值,总结一下就是:当我们对View设置wrap_content的时候,最终获得的View的MeasureSpec的值中,SpecMode的值是AT_MOST,SpecSize值是父View剩余去掉padding之后剩余空间的值,而从上述代码我们也可以看出,当childDimension == MATCH_PARENT时,最终获得的SpecMode的值可能是AT_MOST,也可能是EXACTLY,而SpecSize的值都是size,也就是父View去掉padding之后剩余空间的值也就是说,不管我们对View设置为wrap_content还是match_parent,最终我们把子View的MeasureSpec传递到getDefaultSize()中之后,得到最后的测量后的大小,都是父View去掉padding之后剩余空间的值,因为不管是wrap_content还是match_parent,最终得到的SpecMode不是AT_MOST就是EXACTLY。(结论2)(在这个地方有点晕的话请看结论1)。

    因此,现在我们可以得出结论,在我们直接继承View来自定义控件时,如果不重新onMeasure()方法,不对wrap_content做处理的话,最终对控件设置wrap_content和match_parent后,得到效果是一样的。所以我们需要重写onMeasure)()方法,设置wrap_content时的默认大小。


阅读全文
1 0
原创粉丝点击