Android 进阶学习(二) LinearLayout测量过程
想必大家在面试的过程中都遇到过一些关于LinearLayout 和RelativeLayout 关于测量的问题,下面我就从源码的角度来分析这个过程,其实这个过程自己看源码并读懂他的原理对自己的提升还是很大的,不管是从知识储备还是激发自己的学习兴趣方面.
下面上源码
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
整个LinearLayout的测量过程分为水平和垂直两个方向,他们两个测量的过程是差不多的,我们只需要分析一个就可以了,我们来分析一下垂直方向上的
measureVertical
我们先来说一下LinearLayout的Child 的高度包含哪几个部分

我们再来说一说整个测量过程,这样在梳理整个测量的过程中便于理解
在整个测量过程中根据LinearLayout 的heightMode 分为了2中情况,
1. heightMode 为EXACTLY
即 LinearLayout 的高度是确定的,你写死了height 是多少,或者 你给定的是match_parent fill_parent,在计算过程中先不计算height=0 并且weight >0 的child ,其他都计算,并找到高度最大的child,在第二次计算的过程中,已经计算的child的高度已经知道了,根据剩余高度计算weight >0 的child 可以分到的高度,如果使用前面的最大值,则使用第一次计算的最大值,不使用分配的高度,,如果height=0 则child 的高度就是就是根据weight 分到的高度,如果heighti >0 并且weight >0 ,由于在第一次计算的过程中已经计算了他的高度,本次计算会将第一次计算的高度加上本次计算分到的高度加到一起,即为child 的最后高度,这就是在使用Android Studio 的时候,如果你使用了weight>0 ,但是还给定了height!=0,这样他就会提示你,这样做会浪费性能,原因是他会被测量2次,他的child都是测量一次的,虽然执行了2次计算,但是测量只进行了一次,
2.heightMode 为MeasureSpec.AT_MOST 或者 UNSPECIFIED
即高度不确定, warp_content ,在第一次计算过程中,会计算所有child的高度,但是如果child 的weight >0 并且height=0 ,他会按照warp_content 来计算child,找到所有child的最大高度,第二次计算过程中如果使用了前面计算的最大值,那么需要重新将weight>0 的child 的高度重新计算,修改为 第一次计算的最大值,其他的则不变
我把整个计算过程分为3步,
第一步:
mTotalLength作为整个LinearLayout的高度,先叠加child的Divider的高度,
如果child heightMode == MeasureSpec.EXACTLY (精确地高度或者FILL_PARENT)&&
( boolean useExcessSpace = LayoutParams.height == 0 && LayoutParams.weight > 0)(高度为0并且weight>0,即weight 属性生效),
那么他的高度先不做计算,只添加的他的marginTop和marginBottom 的高度,
否则先调用measureChildBeforeLayout 先测量child的真正的高度,为什么说是真正的高度呢,那就是在测量之前,如果child的useExcessSpace 为true ,那么将child 的LayoutParams的height 设置为LayoutParams.WRAP_CONTENT,因为他可能是UNSPECIFIED 或者 AT_MOST
在测量完成后,如果useExcessSpace 为true ,将它的实际高度统计起来,可能也会和上一个child的高度比较取最大值给largestChildHeight ,并还原child 的 height 为0, 在还原高度之后继续计算mTotalLength,也就是 此时mTotalLength增加的是Child的Divider marginTop marginBottom,还要加上Child的高度,
从这这里我们可以猜想,如果LinearLayout作为xml 的根view,他的高度是match_parent , 其中存在多个view,如果其中一个view 没有使用weight 并且高度设置为match_parent ,那么其他使用了weight 的控件就没有了可分配的空间
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
if (child == null) {
mTotalLength += measureNullChild(i);
continue;
}
if (child.getVisibility() == View.GONE) {
i += getChildrenSkipCount(child, i);
continue;
}
nonSkippedChildCount++;
if (hasDividerBeforeChildAt(i)) {///如果有Divider 则加上Divider 的高度
mTotalLength += mDividerHeight;
}
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
totalWeight += lp.weight;
//// weight 属性生效的条件 height=0 并且weight>0
final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
///当前LinearLayout的高度为精确值,并且child 的weight 属性生效
if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
skippedMeasure = true;
}
else ////LinearLayout 的高度为WARP_CONTENT 或者child的weight >0 并且height>0
{
///如果weight 属性生效,则测量child 的真实高度
if (useExcessSpace) {
lp.height = LayoutParams.WRAP_CONTENT;
}
final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
measureChildBeforeLayout(child, i, widthMeasureSpec, 0, heightMeasureSpec, usedHeight);
///如果weight 属性生效,将child的高度还原为0
final int childHeight = child.getMeasuredHeight();
if (useExcessSpace) {
lp.height = 0;
consumedExcessSpace += childHeight;
}
///最后计算高度,如果weight 属性生效,
/// 此时mTotalLength增加的是Child的Divider marginTop marginBottom,还要加上Child的高度,
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
lp.bottomMargin + getNextLocationOffset(child));
if (useLargestChild) {
largestChildHeight = Math.max(childHeight, largestChildHeight);
}
}
if ((baselineChildIndex >= 0) && (baselineChildIndex == i + 1)) {
mBaselineChildTop = mTotalLength;
}
if (i < baselineChildIndex && lp.weight > 0) {
throw new RuntimeException("");
}
boolean matchWidthLocally = false;
/// 没有精确的宽度,并且宽度是MATCH_PARENT
if (widthMode != MeasureSpec.EXACTLY && lp.width == LayoutParams.MATCH_PARENT) {
matchWidth = true;
matchWidthLocally = true;
}
final int margin = lp.leftMargin + lp.rightMargin;
final int measuredWidth = child.getMeasuredWidth() + margin;
maxWidth = Math.max(maxWidth, measuredWidth);
childState = combineMeasuredStates(childState, child.getMeasuredState());
allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;
///计算宽度
if (lp.weight > 0) {
weightedMaxWidth = Math.max(weightedMaxWidth,
matchWidthLocally ? margin : measuredWidth);
} else {
alternativeMaxWidth = Math.max(alternativeMaxWidth,
matchWidthLocally ? margin : measuredWidth);
}
i += getChildrenSkipCount(child, i);
}
第二步:
第二步其实是一个特定的使用场景,那就是使用了LinearLayout的measureWithLargestChild 属性,并且LinearLayout 的高度有最大值或者未指定高度,在这种情况下,重新计算高度,注意并不是测量,但是所有child的高度使用第一步中计算出来的那个最大的高度
if (useLargestChild &&(heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED)) {
mTotalLength = 0;///重新计算高度
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
if (child == null) {
mTotalLength += measureNullChild(i);
continue;
}
if (child.getVisibility() == GONE) {
i += getChildrenSkipCount(child, i);
continue;
}
final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)child.getLayoutParams();
final int totalLength = mTotalLength;
///所有child 的高度使用第一步计算出来的那个最大值
mTotalLength = Math.max(totalLength, totalLength + largestChildHeight +
lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
}
}
第三步:
第三部其实分为两种情况,else 里面是heightMode是UNSPECIFIED 或者 AT_MOST 使用了最大child 的高度并且weight>0的child 重新测量一遍就是我们上面总结的第二点,至于if里面,就是我们前面总结的第一点 heightMode是 EXACTLY ,并且weight>0 的child会触发重新测量,需要根据weight 属性对child 的高度进行缩放
if (skippedMeasure || ((sRemeasureWeightedChildren || remainingExcess != 0) && totalWeight > 0.0f)) {
float remainingWeightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight;
mTotalLength = 0;
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
if (child == null || child.getVisibility() == View.GONE) {
continue;
}
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final float childWeight = lp.weight;
if (childWeight > 0) {///如果weight >0 根据weight 占比计算高度
final int share = (int) (childWeight * remainingExcess / remainingWeightSum);
remainingExcess -= share;
remainingWeightSum -= childWeight;
final int childHeight;
if (mUseLargestChild && heightMode != MeasureSpec.EXACTLY) {///如果使用最大child的高度,则child的高度为最大的那个
childHeight = largestChildHeight;
} else if (lp.height == 0 && (!mAllowInconsistentMeasurement
|| heightMode == MeasureSpec.EXACTLY)) {///如果高度为0,并且 指定了heightMode为 EXACTLY,则高度就是剩余控件的比例
childHeight = share;
} else {///其他的情况则是自身的高度加上 所分配的剩余空间
childHeight = child.getMeasuredHeight() + share;
}
////下面就是计算高度的过程
final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
Math.max(0, childHeight), MeasureSpec.EXACTLY);
final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin,
lp.width);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
// Child may now not fit in vertical dimension.
childState = combineMeasuredStates(childState, child.getMeasuredState()
& (MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT));
}
////计算宽度
final int margin = lp.leftMargin + lp.rightMargin;
final int measuredWidth = child.getMeasuredWidth() + margin;
maxWidth = Math.max(maxWidth, measuredWidth);
boolean matchWidthLocally = widthMode != MeasureSpec.EXACTLY &&
lp.width == LayoutParams.MATCH_PARENT;
alternativeMaxWidth = Math.max(alternativeMaxWidth,
matchWidthLocally ? margin : measuredWidth);
allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;
///累计高度
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + child.getMeasuredHeight() +
lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
}
// Add in our padding
mTotalLength += mPaddingTop + mPaddingBottom;
// TODO: Should we recompute the heightSpec based on the new total length?
} else
{
alternativeMaxWidth = Math.max(alternativeMaxWidth,
weightedMaxWidth);
// We have no limit, so make all weighted views as tall as the largest child.
// Children will have already been measured once.
if (useLargestChild && heightMode != MeasureSpec.EXACTLY) {
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child == null || child.getVisibility() == View.GONE) {
continue;
}
final LinearLayout.LayoutParams lp =
(LinearLayout.LayoutParams) child.getLayoutParams();
float childExtra = lp.weight;
if (childExtra > 0) {
child.measure(
MeasureSpec.makeMeasureSpec(child.getMeasuredWidth(),
MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(largestChildHeight,
MeasureSpec.EXACTLY));
}
}
}
}