在学习View的工作原理时,书上说ViewGroup是一个抽象类,因此它没有重写View的onMeasure()方法,但是它提供了一个叫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) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
说实在的我没看出来上面的“但是”是啥意思。并且查了一下,没发现measureChildren()在源码中哪里被使用了。而且measureChildren()中使用的是measureChild(),这个函数没有考虑测量过程中之前已测量的子view对当前子view的影响。并且measureChild()内部也没有考虑子view自己的margin的影响。 感觉这么多问题,为什么View源码中要这样设计呢?查了很多资料,都没有讲这块。不过在一些Scroller等容器中看到别人有在onMeasure()中直接使用measureChildren()给其内部的所有子view测量,猜测measureChildren()的应用场景估计就这些吧。看了LinearLayout、FrameLayout和RelativeLayout()都没有使用measureChildren()。
/**
* 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();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
结合measureChildren()和measureChild()的实现来看,measureChildren()中循环调用measureChild(),并且measureChild()的参数都是一样的,再看measureChild()中的getChildMeasureSpec()只考虑了viewgroup的padding值,这个padding值是不会变化的。意味着父容器viewgroup的已使用空间是不会变的。这意味着ViewGroup中,对所有子View来说,父容器已占用的空间永远都是Math.max(0, specSize - padding)。它不像LinearLayout那样,子View排列会影响父容器的已使用宽高大小,从而导致子view在测量时由于viewgroup的剩余空间越来越小,导致子view的测量大小会变化。就比如假如子view的layoutParams为match_parent,则其specSize是Math.max(0, specSize - padding), padding在不断增大,意味着match_parent的子view越来越小。
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);
}
即上面代码中,View中所有子View的size都一样大, size = Math.max(0, specSize - padding)。size变量表示父容器可用剩余空间。
我在一个自定义ScrollerView中看到有这样使用的
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measuredWidth = 0;
int measuredHeight = 0;
final int childCount = getChildCount();
measureChildren(widthMeasureSpec, heightMeasureSpec);
...
}
猜想之所以选择使用measureChilren(),是因为这样可以使得Scroller容器所有页面的测量大小都一样大。
还有特别强调的一个函数是ViewGroup类的measureChildWithMargins(),它也是用来测量子view的,但是它共有5个参数,多出来的2个参数表示ViewGroup容器的宽或高已经使用的空间。因为线性布局中,子view摆放会占用空间,所以测量的时候需要计算剩余可用空间,从而来计算靠近边界的子view的大小。
/**
* 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);
}
我们可以进到LinearLayout看看对其的调用
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
mTotalLength = 0;
final int count = getVirtualChildCount();
for (int i = 0; i < count; ++i) {
...
if (hasDividerBeforeChildAt(i)) {
mTotalLength += mDividerHeight;
}
...
final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
...
measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
heightMeasureSpec, usedHeight);
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
lp.bottomMargin + getNextLocationOffset(child));
...
}
}
void measureChildBeforeLayout(View child, int childIndex,
int widthMeasureSpec, int totalWidth, int heightMeasureSpec,
int totalHeight) {
measureChildWithMargins(child, widthMeasureSpec, totalWidth,
heightMeasureSpec, totalHeight);
}
大概就是这样,可以看到LinearLayout在循环遍历子View,测量子View时调用
measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
heightMeasureSpec, usedHeight);
展示的LinearLayout的orientation=vertical,垂直方向布局的,所以上面usedWidth传入的是0, 这是符合逻辑的。因为垂直方向的线性布局中其子view是从上到下排列的,每个子view左侧没有被其他子View占用的,,所以线性布局已经使用的宽度usedWidth=0;
已使用高度空间用usedHeight来保存,可以看到在循环过程中它是不断变大的。这也符合线性布局的逻辑。
并且measureVertical()和measureChildWithMargins()都考虑了子view的margin值。
最后,我发现在RelativeLayout中,它自己写了measureChild()和getChildMeasureSpec(), 和View中的不同,不是简单的复写,因为函数参数不同了。
private int getChildMeasureSpec(int childStart, int childEnd,
int childSize, int startMargin, int endMargin, int startPadding,
int endPadding, int mySize)
private void measureChild(View child, LayoutParams params, int myWidth, int myHeight)