LinearLayout之垂直布局测量分析详解
大家先看一段代码
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="200dp"
android:orientation="vertical"
android:background="@mipmap/ic_launcher"
>
<View
android:layout_width="match_parent"
android:layout_height="200dp"
android:layout_weight="1"
android:background="@color/colorAccent"
/>
<View
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_weight="1"
android:background="@color/colorPrimary"
/>
</LinearLayout>
</LinearLayout>
你觉得上面布局中,两个View实际分配到的高度是多少?大家可以将代码运行下,其实最终第一个View分配到的高度是170dp,第二个是30dp;为什么呢,这就要熟读下面的分析了。
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
//记录测量的高度
mTotalLength = 0;
//记录LineraLayout 的最大宽度,每次测量子view时,都会对maxWidth赋值
int maxWidth = 0;
int childState = 0;
int alternativeMaxWidth = 0;
//记录设有权重的view的最大宽度
int weightedMaxWidth = 0;
boolean allFillParent = true;
//所有权重总和
float totalWeight = 0;
//所有子view数量
final int count = getVirtualChildCount();
//获取宽度测量模式
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
//获取高度测量模式
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
boolean matchWidth = false;
boolean skippedMeasure = false;
//这个baselineChildIndex 是什么东西
final int baselineChildIndex = mBaselineAlignedChildIndex;
//是否使用最大View的尺寸,默认情况下很少使用
final boolean useLargestChild = mUseLargestChild;
int largestChildHeight = Integer.MIN_VALUE;
//记录被消费的控件
int consumedExcessSpace = 0;
//记录真正测量的子view,即排除view==null或者view可见性为View.GONE
int nonSkippedChildCount = 0;
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
if (child == null) {
//measureNullChild一般默认返回0;
mTotalLength += measureNullChild(i);
continue;
}
if (child.getVisibility() == View.GONE) {
//getChildrenSkipCount一般默认返回0;
i += getChildrenSkipCount(child, i);
continue;
}
nonSkippedChildCount++;
//如果子view之间有分割线,记录分割线的高度
if (hasDividerBeforeChildAt(i)) {
mTotalLength += mDividerHeight;
}
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
//累加设有权重的view
totalWeight += lp.weight;
final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
//如果是精确测量模式,并且子view属性layout_height=0;layout_weight>0 ;对于这种view,我们先不对view测量,先记录下topMargin和bottomMargin ,为什么要这样?把其他view消耗的高度测量出来,剩余高度然后根据权重比重新分配给设有权重的View,因此,我们在写LineraLayout布局时,layout_heigith>0的View,总是优先分配高度
mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
skippedMeasure = true;
} else {
/**
这个分支分以下几种情况
1.AT_MOST
1.1.height=0 && weight =0
1.2.height>0&&weight=0
1.3.height=0&&weight>0(在后面源码中设置成了同1.5)
1.4.height>0&&weihgt>0
1.5.height=wrap_parent&&weight>0
1.6.height=wrap_parent&&weight=0
1.7.height=match_parent&&weight>0
1.8.height=match_parent&&weight=0
2.EXACTLY
2.1.height=0 && weight =0
2.2.height>0&&weight=0
2.3.height>0&&weihgt>0
2.4.height=wrap_parent&&weight>0
2.5.height=wrap_parent&&weight=0
2.6.height=match_parent&&weight>0
2.7.height=match_parent&&weight=0
2.8.height=0&&weight>0(这个情况不会出现在这个分支,放一起好分析)
从上面情况结合下面measureChildBeforeLayout方法可以得出结论:
a.1.1;1.2;1.4;2.1;2.2;2.3;测得子view的height为view中的LayoutParams中的height,自己对自己的子view的测量模式为EXACTLY
b.1.3;1.5;1.6;1.7;1.8;2.4;2.5;测得子view的height为父view中期望高度-已使用的高度-子view的padding-子view中的margin,自己对自己的子view的测量模式为AT_MOST
c.2.6;2.7;测得子view的height为父view中期望高度-已使用的高度-子view的padding-子view中的margin,自己对自己的子view的测量模式为EXACTLY
d.2.8;只有某一个子view是2.8这种情况或者在a,b,c 这几种情况测量完成后剩余空间不为0(大于或小于0)并且子view中有weight>0d的,二者满足其一会再一次对权重大于0 的子view调试尺寸
*/
if (useExcessSpace) {
//对于1.3.height=0&&weight>0;为什么要这样设置,因为这样能给这类子view于父View中剩余最大高度尺寸,能最大限度的满足子view;无疑这样设计是最佳的选择
lp.height = LayoutParams.WRAP_CONTENT;
}
/*usedHeight 已使用的高度,如果totalWeight>0,说明之前有view设有权重,这里就采用了一种策略,先尽可能的给予每个设有权重的子view最大尺寸高度,后面再根据策略调整。*/
final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
//测量子view的尺寸
measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
heightMeasureSpec, usedHeight);
final int childHeight = child.getMeasuredHeight();
if (useExcessSpace) {
consumedExcessSpace += childHeight;
}
final int totalLength = mTotalLength;
//累计总高度
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
lp.bottomMargin + getNextLocationOffset(child));
//是否用最大view尺寸,不分析,默认不使用
if (useLargestChild) {
largestChildHeight = Math.max(childHeight, largestChildHeight);
}
}
if ((baselineChildIndex >= 0) && (baselineChildIndex == i + 1)) {
mBaselineChildTop = mTotalLength;
}
//view索引小于baselineChildIndex,如果设置了lp.weight 会有异常
if (i < baselineChildIndex && lp.weight > 0) {
throw new RuntimeException("A child of LinearLayout with index "
+ "less than mBaselineAlignedChildIndex has weight > 0, which "
+ "won't work. Either remove the weight, or don't set "
+ "mBaselineAlignedChildIndex.");
}
boolean matchWidthLocally = false;
if (widthMode != MeasureSpec.EXACTLY && lp.width == LayoutParams.MATCH_PARENT) {
//这种模式下,如果缩放LineraLayout ,view也需要相应重新测量,所以这里先做个标记,如果子view中有该种模式;
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);
}
//获取需要跳过测量的标示,getChildrenSkipCount默认返回0,如果返回1,则代表下一个view不需要测量
i += getChildrenSkipCount(child, i);
}
/*for循环结束,初步测量完成,总结下上面for循环干了啥事
1.测量出部分子view的高度,(父View为EXACTLY并且子view高度和weight都为0 ,这里view先不测量其高度,)
2.记录总的需要的高度如:mTotalLength,mTotalLength包含已测量的部分view 的高度和margin,分割线,父View为EXACTLY并且子view高度和weight都为0这类view的margin*/
if (nonSkippedChildCount > 0 && hasDividerBeforeChildAt(count)) {
mTotalLength += mDividerHeight;
}
//如果使用了使用最大子view尺寸,并且父view模式为Wrap_content,则需要重新计算mTotalLength,所以最好不要设置useLargestChild,效率低
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();
// Account for negative margins
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + largestChildHeight +
lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
}
}
// Add in our padding
mTotalLength += mPaddingTop + mPaddingBottom;
int heightSize = mTotalLength;
// Check against our minimum height
//看下是否有背景宽高,取二者大的
heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
//对比下heightSize 与父view期望的高度
//1.如果父view测量模式是EXACTLY,heightSizeAndState 赋值为父view的期望高度
//2.如果父view测量模式是AT_MOST,heightSizeAndState 赋值为父view的期望高度与heightSize 哪个小就用哪个;
int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
/**
remainingExcess的值用来判断剩余空间以便来决定是否需要缩小或者扩大weight>0的子view,如果remainingExcess的值大于0,则会扩大部分View高度,如果remainingExcess小于0,则会缩小部分view高度,remainingExcess等于0,就不用再测量,这是效率最高的测量模式。平时开发应该尽量满足。为什么remainingExcess会小于0,在上面的测量中,有如果view的高度在xml明确写明了的话,会直接将该高度作为初步测量高度。mTotalLength是每个测量好的view高度的累计,比如父view高度为100dp,但是子view有几个50dp的,那最终mTotalLength的高度肯定高于heightSize*/
int remainingExcess = heightSize - mTotalLength
+ (mAllowInconsistentMeasurement ? 0 : consumedExcessSpace);
/*下面的if条件满足以下其一都执行,if里面的逻辑主要是缩放子view
1.如果子view中满足父View为EXACTLY,子view高度和weight都为0
2.remainingExcess!=0 && 至少一个子view中的weight>0*/
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) {
//share的值如果大于0;说明是给满足条件的view放大;如果小于0;则是缩小满足条件的
final int share = (int) (childWeight * remainingExcess / remainingWeightSum);
remainingExcess -= share;
remainingWeightSum -= childWeight;
final int childHeight;
//mUseLargestChild 默认为false
if (mUseLargestChild && heightMode != MeasureSpec.EXACTLY) {
childHeight = largestChildHeight;
} else if (lp.height == 0 && (!mAllowInconsistentMeasurement
|| heightMode == MeasureSpec.EXACTLY)) {
//这个就是为父View为EXACTLY,子view高度和weight都为0的这类view高度赋值,这类view在前面并没有参与测量。这类view的高度分配权最低;
childHeight = share;
} else {
//这类view为前面参与测量的view,重新分配高度
childHeight = child.getMeasuredHeight() + share;
}
//经过这次调整的View,都将自己对子view的高度测量模式设置为EXACTLY了,因为这次所有的view的高度尺寸都已知道了。
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));
}
}
}
}
if (!allFillParent && widthMode != MeasureSpec.EXACTLY) {
maxWidth = alternativeMaxWidth;
}
maxWidth += mPaddingLeft + mPaddingRight;
// Check against our minimum width
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
heightSizeAndState);
if (matchWidth) {
forceUniformWidth(count, heightMeasureSpec);
}
}
通过上面的分析,可以总结如下集中模式,以后LinearLayout布局高度尺寸分配有什么疑惑直接套用就行了
1.父View高度测量模式AT_MOST
1.1.height=0 && weight =0(最终测量的高度为0)
1.2.height>0&&weight=0 (最终测量的高度为height)
1.3.height=0&&weight>0(最终测量的高度父view期望高度-已消耗的高度-padding-margin+调整尺寸)
1.4.height>0&&weihgt>0(height+调整尺寸)
1.5.height=wrap_parent&&weight>0(最终测量的高度父view期望高度-已消耗的高度-padding-margin+调整尺寸)
1.6.height=wrap_parent&&weight=0(最终测量的高度父view期望高度-已消耗的高度-padding-margin)
1.7.height=match_parent&&weight>0(最终测量的高度父view期望高度-已消耗的高度-padding-margin+调整尺寸)
1.8.height=match_parent&&weight=0(最终测量的高度父view期望高度-已消耗的高度-padding-margin)
2.父View高度测量模式EXACTLY
2.1.height=0 && weight =0(最终测量的高度为0)
2.2.height>0&&weight=0(最终测量的高度为height)
2.3.height>0&&weihgt>0(height+调整尺寸)
2.4.height=wrap_parent&&weight>0(最终测量的高度父view期望高度-已消耗的高度-padding-margin+调整尺寸)
2.5.height=wrap_parent&&weight=0(最终测量的高度父view期望高度-已消耗的高度-padding-margin)
2.6.height=match_parent&&weight>0(最终测量的高度父view期望高度-已消耗的高度-padding-margin+调整尺寸)
2.7.height=match_parent&&weight=0(最终测量的高度父view期望高度-已消耗的高度-padding-margin)
2.8.height=0&&weight>0(调整尺寸)
以上如果最终高度小0,则设置为0;
我们用这几种模式来套用开头的案例,案例中:父View高度为200dp,测量模式为EXACTLY,有两个子View,view1 高度为200dp,weight=1;view2高度为60dp,weight=1;
分析:两个子view都符合2.3这种情况;那么我们来看看其最终高度view1=200+调整尺寸;view2=60+调整尺寸;调整尺寸 = (父View高度-子view1高度-子view2高度)/2 = (200-200-60)/2 = -30 ;所以最终
view1的高度=200+(-30)=170;
view2的高度=60+(-30)=30;
当然以上计算还不严谨,但基本上可以用这个模型来阐述整个测量流程。算是有理有据吧。
好了再总结下流程和关键点
1.第一个for循环,首先是筛选出一批View先测量(父View高度测量模式== MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0 ;这类view放后面测量)
2.计算下剩余高度(这个高度控制在父View期望高度以内,参看这句代码heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0))
3.a,如果高度有剩余,b.至少有一个子view为:父View高度测量模式== MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0 ;c.至少一个子view的weight>0;如果满足a||(b&&c)则进入第二个for循环调整权重大于0的子view的高度
经过上面3个主要流程关键点,LinearLayout内部测量算是完成了。
通过以上学习,我们自问自答下。
1.通过这么多的测量,最终测量出来的高度会不会超过父view期望的高度?回答是不会。具体原因请参看文中int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);这句代码上的解释。
2.如何提高测量效率?少用weight,少用userLargest模式
3.如何绘制分割线,如果控制是否需要测量?参考分析源码
写在后文:忘各位同道指正评论。