在自定义ViewGroup时,通常需要在onMeasure
方法中调用measureChildWithMargins
或measureChild
方法来测量子视图的尺寸。其中需要参考当前ViewGroup的MeasureSpec
,及其他条件用于生成子视图的MeasureSpec
(测量规格)。我们以measureChildWithMargins
为例。
/**
* Ask one of the children of this view to measure itself, taking into
* account both the MeasureSpec requirements for this view and its padding
* and margins. The child must have MarginLayoutParams The heavy lifting is
* done in getChildMeasureSpec.
*
* @param child The child to measure
* @param parentWidthMeasureSpec The width requirements for this view
* @param widthUsed Extra space that has been used up by the parent
* horizontally (possibly by other children of the parent)
* @param parentHeightMeasureSpec The height requirements for this view
* @param heightUsed Extra space that has been used up by the parent
* vertically (possibly by other children of the parent)
*/
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
中,又调用了getChildMeasureSpec
方法。从方法名可以得知,此方法就是用于确认传递给子视图的MeasureSpec
的方法。方法返回MeasureSpec
后,最后调用子视图的measure
方法,用于测量子视图的尺寸。getChildMeasureSpec
的传参为其父控件,即当前ViewGroup
的MeasureSpec
,以及Padding
和Margin
和已使用的空间的和,最后是子控件的宽高。根据参数,我们可以推测,子控件的MeasureSpec
是根据父控件的MeasureSpec
、自己自身的尺寸(final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams()
,用户从xml或代码中设置的尺寸),以及已经占用的空间来确定的。下面我们带着猜测看一下具体的源码实现。
/**
* 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
*/
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 them 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 = 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 = size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
方法开头部分获取了当前ViewGroup
的MeasureSpec
,并且int size = Math.max(0, specSize - padding)
获取了留给子控件的剩余空间。
int resultSize = 0;
int resultMode = 0;
声明了变量,尺寸和模式,用于构建子视图的MeasureSpec
.
接下来的大段switch
语句中,根据当前ViewGroup
的测量模式分别处理,区分MeasureSpec.EXACTLY
,MeasureSpec.AT_MOST
,MeasureSpec.UNSPECIFIED
。可以看出父控件的MeasureSpec
影响子控件的MeasureSpec
的生成.
然后,我们分别看这几种情况是如何处理的。
以MeasureSpec.EXACTLY
为例,
// 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;
childDimension
是getChildMeasureSpec
的最后一个参数,具体指为从measureChildWithMargins
中获取的开发者设置的子视图的尺寸(xml或代码),然后判断其是否大于等于0,如果为true
,则设置resultSize = childDimension; resultMode = MeasureSpec.EXACTLY;
,即父控件为精确模式,并且子控件也有精确尺寸的时候,那么子控件的测量模式也为精确模式。 如果childDimension == LayoutParams.MATCH_PARENT
时,即父控件为精确模式,子控件尺寸为LayoutParams.MATCH_PARENT
时,那么子控件的测量模式也为精确模式。 实际上,LayoutParams.MATCH_PARENT
本身也可以看成一个精确的尺寸,尺寸大小等于其可用的最大尺寸,即方法开头的size
。所以最后设置resultSize = size;resultMode = MeasureSpec.EXACTLY;
。如果子控件尺寸为childDimension == LayoutParams.WRAP_CONTENT
,即父控件为精确模式,子控件尺寸为childDimension == LayoutParams.WRAP_CONTENT
时,子控件的测量模式为最大限制模式。 所以最后设置resultSize = size;resultMode = MeasureSpec.AT_MOST;
。
同理分析父控件为MeasureSpec.AT_MOST
和MeasureSpec.UNSPECIFIED
,整理成一下表格
子控件MeasureSpec | 成立条件 |
---|---|
MeasureSpec.EXACTLY | 1. 父控件为MeasureSpec.EXACTLY,同时子控件使用精确尺寸 2. 父控件为MeasureSpec.EXACTLY,同时子控件尺寸为LayoutParams.MATCH_PARENT 3. 父控件为MeasureSpec.AT_MOST,同时子控件使用精确尺寸 4. 父控件为MeasureSpec.UNSPECIFIED,并且子控件使用精确尺寸 |
MeasureSpec.AT_MOST | 1. 父控件为MeasureSpec.EXACTLY,同时子控件尺寸为LayoutParams.WRAP_CONTENT 2. 父控件为MeasureSpec.AT_MOST,同时子控件尺寸为LayoutParams.MATCH_PARENT 3. 父控件为LayoutParams.MATCH_PARENT,同时子控件尺寸为LayoutParams.WRAP_CONTENT |
MeasureSpec.UNSPECIFIED | 1. 父控件为MeasureSpec.UNSPECIFIED, 并且同时子控件尺寸为LayoutParams.MATCH_PARENT 2. 父控件为MeasureSpec.UNSPECIFIED,并且同时子控件尺寸为LayoutParams.WRAP_CONTENT |
最后,调用MeasureSpec.makeMeasureSpec(resultSize, resultMode);
方法最终生成子控件的MeasureSpec
。
了解MeasureSpec
的生成过程,有助于理解自定义控件的整个测量过程。但是需要注意的是,在实际开发中,我们并不太需要关心这一部分的代码实现,直接调用现成的方法即可,如在自定义ViewGroup
时调用measureChild
、measureChildren
、measureChildWithMargins
方法。
当我们自定义控件测量其尺寸时,最终都要通过调用setMeasuredDimension(width, height)
完成,onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int)
的入参中传递了正确的测量规格。如果测量规格为MeasureSpec.EXACTLY
,我们直接获取其尺寸使用即可;而如果测量规格为MeasureSpec.AT_MOST
,其传递的是当前视图所能占用的最大尺寸,所以其具体尺寸需要我们自己通过逻辑来确定;MeasureSpec.UNSPECIFIED
则一般不会遇到。