知识用途:自定义View
基础知识:
1. MeasureSpec:
决定View的尺寸规格,可以unpack为SpecMode和SpecSize两个值,直译为测量模式和测量大小。
* SpecMode有三种值
1. UNSPECIFIED : 父容器不对View有任何限制,这种情况用于系统内部的临时状态
2. EXACTLY: 父容器已经检测出View的确切大小,这时View的最终大小即SpecSize所指定的值。对应于LayoutParams中的match_parent和具体数值两种模式
3. AT_MOST : 父容器指定了一个SpecSize,View大小不能大于这个值,具体大小要看各View不同的实现。对应于LayoutParams的wrap_content。
2. LayoutParams:
View的布局参数,通常是程序员使用XML指定的,对于一个典型的View,我们能指定其layout_width和layout_height为match_parent、x dp/px、wrap_content,这些值都存储在LayoutParams中。
开始解析:
1. ViewGroup的measure过程:
一个ViewGroup在measure的过程中应当有两个动作:测量子元素的大小和以此为基础测量自己的大小。但是经过搜索,我们发现ViewGroup中没有默认的测量自己大小的方法,而是每个具体的ViewGroup子类有自己实现的一个onMeasure方法。这也很好理解,每种ViewGroup的大小测量过程都是不一样的。
那么我们来看ViewGroup的测量子元素的方法:
measureChildren()
/** * Ask all of the children of this view to measure themselves, taking into * account both the MeasureSpec requirements for this view and its padding. * We skip children that are in the GONE state The heavy lifting is done in * getChildMeasureSpec. * * @param widthMeasureSpec The width requirements for this view * @param heightMeasureSpec The height requirements for this view */ 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) { //测量每个Child,传入ChildView、ViewGroup的widthMeasureSpec和heightMeasureSpec measureChild(child, widthMeasureSpec, heightMeasureSpec); } } }
继续看
measureChild()
/** * Ask one of the children of this view to measure itself, taking into * account both the MeasureSpec requirements for this view and its padding. * The heavy lifting is done in getChildMeasureSpec. * * @param child The child to measure * @param parentWidthMeasureSpec The width requirements for this view * @param parentHeightMeasureSpec The height requirements for this view */ protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) { final LayoutParams lp = child.getLayoutParams(); //使用父容器的MeasureSpec、Padding和Child的width/heigth来获得Child的MeasureSpec final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight, lp.width); final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom, lp.height); //使用Child的MeasureSpec来measure child child.measure(childWidthMeasureSpec, childHeightMeasureSpec); }
来看这里面的两个函数
首先是
getChildMeasureSpec()
/** * Does the hard part of measureChildren: figuring out the MeasureSpec to * pass to a particular child. This method figures out the right MeasureSpec * for one dimension (height or width) of one child view. * * The goal is to combine information from our MeasureSpec with the * LayoutParams of the child to get the best possible results. For example, * if the this view knows its size (because its MeasureSpec has a mode of * EXACTLY), and the child has indicated in its LayoutParams that it wants * to be the same size as the parent, the parent should ask the child to * layout given an exact size. * * @param spec The requirements for this view * @param padding The padding of this view for the current dimension and * margins, if applicable * @param childDimension How big the child wants to be in the current * dimension * @return a MeasureSpec integer for the child */ //三个参数分别是:父容器的Spec,相应的两个Padding,子元素的长度/宽度(大小) public static int getChildMeasureSpec(int spec, int padding, int childDimension) { //Unpack父元素的Spec int specMode = MeasureSpec.getMode(spec); int specSize = MeasureSpec.getSize(spec); //如果父元素减去Padding后还有剩余空间给子元素的话,否则则为0 int size = Math.max(0, specSize - padding); int resultSize = 0; int resultMode = 0; switch (specMode) { // Parent has imposed an exact size on us //父元素为match_parent或者固定值 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. //子元素为match_parent的话,大小就为父元素的剩余空间 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. //子元素为wrap_content的话,size为父元素的剩余空间,Mode为AT_MOST resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; // Parent has imposed a maximum size on us //父元素为wrap_content,下面请看英文注释 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; } //返回子元素的Spec return MeasureSpec.makeMeasureSpec(resultSize, resultMode); }
如果看到这里你还没有发现问题的话,要么是看的时候没动脑子,要么是对Android开发不够熟悉。
上面代码中有一段很奇怪:
} else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size. It can't be // bigger than us. //子元素为wrap_content的话,size为父元素的剩余空间,Mode为AT_MOST resultSize = size; resultMode = MeasureSpec.AT_MOST; }
这里是子元素为wrap_content,父元素为match_parent或固定值,而此时size为父元素的剩余空间。这和我们平时使用wrap_content的经验相违背。wrap_content应该是以能包裹View里内容的最小大小为准,为什么这里会占据父元素的全部剩余空间?
注意,只有此时Mode为AT_MOST,指示这个size是最大size,意味着我们之后还要对AT_MOST情况的size做一些特殊处理。这里先按下不表。
2.View的Measure过程
然后看在获得child的spec后,程序调用了child的measure方法,measure方法位于View中:
measure()
/** * <p> * This is called to find out how big a view should be. The parent * supplies constraint information in the width and height parameters. * </p> * * <p> * The actual measurement work of a view is performed in * {@link #onMeasure(int, int)}, called by this method. Therefore, only * {@link #onMeasure(int, int)} can and must be overridden by subclasses. * </p> * * * @param widthMeasureSpec Horizontal space requirements as imposed by the * parent * @param heightMeasureSpec Vertical space requirements as imposed by the * parent * * @see #onMeasure(int, int) */ //传入的是此View的两个Spec 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); if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT || widthMeasureSpec != mOldWidthMeasureSpec || heightMeasureSpec != mOldHeightMeasureSpec) { // first clears the measured dimension flag mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET; resolveRtlPropertiesIfNeeded(); int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 : mMeasureCache.indexOfKey(key); if (cacheIndex < 0 || sIgnoreMeasureCache) { // measure ourselves, this should set the measured dimension flag back //关键点,传入此View的两个Spec 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 }
这个方法比较复杂,其关键在于onMeasure():
onMeasure()
/** * <p> * Measure the view and its content to determine the measured width and the * measured height. This method is invoked by {@link #measure(int, int)} and * should be overridden by subclasses to provide accurate and efficient * measurement of their contents. * </p> * * <p> * <strong>CONTRACT:</strong> When overriding this method, you * <em>must</em> call {@link #setMeasuredDimension(int, int)} to store the * measured width and height of this view. Failure to do so will trigger an * <code>IllegalStateException</code>, thrown by * {@link #measure(int, int)}. Calling the superclass' * {@link #onMeasure(int, int)} is a valid use. * </p> * * <p> * The base class implementation of measure defaults to the background size, * unless a larger size is allowed by the MeasureSpec. Subclasses should * override {@link #onMeasure(int, int)} to provide better measurements of * their content. * </p> * * <p> * If this method is overridden, it is the subclass's responsibility to make * sure the measured height and width are at least the view's minimum height * and width ({@link #getSuggestedMinimumHeight()} and * {@link #getSuggestedMinimumWidth()}). * </p> * * @param widthMeasureSpec horizontal space requirements as imposed by the parent. * The requirements are encoded with * {@link android.view.View.MeasureSpec}. * @param heightMeasureSpec vertical space requirements as imposed by the parent. * The requirements are encoded with * {@link android.view.View.MeasureSpec}. * * @see #getMeasuredWidth() * @see #getMeasuredHeight() * @see #setMeasuredDimension(int, int) * @see #getSuggestedMinimumHeight() * @see #getSuggestedMinimumWidth() * @see android.view.View.MeasureSpec#getMode(int) * @see android.view.View.MeasureSpec#getSize(int) */ //传入两个本View的Spec,经过getDefaultSize处理 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); }
看上去比较简单,从注释中我们可以看出,如果想要自定义View的measure过程,我们需要复写的是这个方法。观察其内部:
getSuggestedMinimumWidth() & getDefaultSize()
//如果有background,取其和android:minWidth的较大值,否则为minWidth。minWidth如果不设置则为0,Height同样 protected int getSuggestedMinimumWidth() { return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth()); } /** * Utility to return a default size. Uses the supplied size if the * MeasureSpec imposed no constraints. Will get larger if allowed * by the MeasureSpec. * * @param size Default size for this view * @param measureSpec Constraints imposed by the parent * @return The size this view should be. */ 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; }
所以既然View的默认实现没有对AT_MOST做特殊处理,而是直接返回了最大可用空间(父容器除去Padding之后的大小),那么就导出一个结论:
任何直接继承View的自定义控件,都需要我们手动重写onMeasure()来正确处理wrap_content情况
系统自带的TextView、ImageView等都对AT_MOST做了特殊处理。
结论
自定义控件时:
- 如果是ViewGroup,需覆写onMeasure()来正确使用测量好的子元素尺寸去决定此ViewGroup的尺寸。
- 如果是View,需要覆写onMeasure()来正确处理wrap_content情况和做其他工作。