Android开发学习之线性布局测量流程源码阅读
背景
昨天我记录了安卓中相对布局的测量流程源码阅读,之后又读了一下线性布局LinearLayout的测量流程(onMeasure),但由于晚上突然来了个需求,文章记录就推迟到了现在。
onMeasure()
LinearLayout.onMeasure()代码如下
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
众所周知,线性布局的方向分为垂直和水平,两者分别对应measureVertical()方法和measureHorizontal()方法,两个方法思路一样,我就以垂直方向为例,阅读一下它的测量流程,主要解释都在代码中的注释里
measureVertical()
跟相对布局的onMeasure()方法阅读一样,我把measureVertical()的步骤分为了七步
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
/**
* 步骤:
* 1、初始化变量
* 2、遍历所有子view,进行第一次测量,但这不一定能测量所有的
* 3、更新布局的最大高度
* 4、如果有子view没有被测量,或者还有剩余空间进行权重分配,就再对所有子view进行一次测量,同时更新布局的最大尺寸
* 如果所有子view都被测量,也没法进行权重分配,但布局设置了"采用最大子view",并且高度不是精确模式,就把所有用权重要求的子view的高度,设置为最大子view的高度
* 此时不用更新布局的高度,因为如果到了这种情况,布局最大高度已经 = 子view数目 * 最大子view高度 + 所有间距 + 分割线高度,不可能再高了
* 5、更新布局最大宽度
* 6、保存布局信息
* 7、如果子view是match_parent,但当前布局不是精确模式,强制更新所有子view宽度为布局宽度,宽度模式是精确模式
*/
}
那我们就一步一步来吧
初始化变量
初始化一些用到的变量
mTotalLength = 0; // 总长度
int maxWidth = 0; // 所有子view的最大宽度
int childState = 0; // 子view测量状态
int alternativeMaxWidth = 0; // 没有权重需求的子view的最大宽度
int weightedMaxWidth = 0; // 有权重子view的最大宽度
boolean allFillParent = true; // 子view全都是fill_parent/match_parent
float totalWeight = 0; // 子view权重之和
final int count = getVirtualChildCount(); // 子view数量
final int widthMode = MeasureSpec.getMode(widthMeasureSpec); // 当前布局宽度测量模式
final int heightMode = MeasureSpec.getMode(heightMeasureSpec); // 当前布局高度测量模式
boolean matchWidth = false; // 存在子view宽度是match_parent,但当前布局宽度不是精确模式
boolean skippedMeasure = false; // 是否有子view因为某些原因跳过了测量
final int baselineChildIndex = mBaselineAlignedChildIndex; // 做为基准线的子view,默认是-1
final boolean useLargestChild = mUseLargestChild; // 默认为false
int largestChildHeight = Integer.MIN_VALUE; // 最大子view高度
int consumedExcessSpace = 0; // 可用来分配权重的剩余空间
int nonSkippedChildCount = 0; // 已经测量过的子view数目
第一次遍历测量所有子view
这一个遍历代码非常长,140多行代码,我还是分几步来看
处理子view为null或gone的情况,并处理分割线
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i); // 就是getChildAt(i)
if (child == null) {
mTotalLength += measureNullChild(i); // 0
continue;
}
if (child.getVisibility() == View.GONE) {
i += getChildrenSkipCount(child, i); // 0
continue;
}
nonSkippedChildCount++;
if (hasDividerBeforeChildAt(i)) {
// 如果这个子view之前有divider,就加上分割线的高度
mTotalLength += mDividerHeight;
}
....
处理子view高度和权重
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
totalWeight += lp.weight; // 累积子view的权重
final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
// 子view高度为0,但权重不是0
// 说明高度尺寸优先级大于权重
if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
// 当前布局既是精确测量,子view又是useExcessSpace,那就先不测量它,并且设定标志位
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin); // 但总长度还是要更新
skippedMeasure = true;
// 就加上子view的上下外边距,并且设置标志位skippedMeasure为true
} else {
if (useExcessSpace) {
// 如果当前布局不是精确测量,子view又是useExcessSpace
lp.height = LayoutParams.WRAP_CONTENT;
// 暂时把参数的height设为内容包裹,以供measureChildBeforeLayout()方法调用
}
// 如果之前有子view有权重需求,就给所有的子view以最大高度,事后根据权重再压缩
final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
heightMeasureSpec, usedHeight);
// 并没有用到参数i,调用viewGroup.measureChildWithMargins()
final int childHeight = child.getMeasuredHeight();
if (useExcessSpace) {
lp.height = 0; // 恢复子view的参数高度为0
consumedExcessSpace += childHeight; // 累加可以用来进行权重分配的空间
}
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
lp.bottomMargin + getNextLocationOffset(child));
// 最后一个方法返回0
if (useLargestChild) {
// 更新最大子view的高度
largestChildHeight = Math.max(childHeight, largestChildHeight);
}
}
更新基准线
// 设置了基准线的话,更新基准线顶端位置
if ((baselineChildIndex >= 0) && (baselineChildIndex == i + 1)) {
mBaselineChildTop = mTotalLength;
}
// if we are trying to use a child index for our baseline, the above
// book keeping only works if there are no children above it with
// weight. fail fast to aid the developer.
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) {
// The width of the linear layout will scale, and at least one
// child said it wanted to match our width. Set a flag
// indicating that we need to remeasure at least that view when
// we know our width.
matchWidth = true;
matchWidthLocally = true; // 仅仅是match_parent,但当前布局不是精确模式,此时当前布局还不知道自己的宽度
}
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); // 0
更新当前布局最大高度
if (nonSkippedChildCount > 0 && hasDividerBeforeChildAt(count)) {
// 有子view被测量,并且在这个view之前有divider,就把分割线的高度加进总高度中
mTotalLength += mDividerHeight;
}
// 如果设置了useLargetChild,就是以子view中最大为基准测量
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); // 0
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)); // 此处是总高度+n*最大子view高度
}
}
// 更新布局最大高度
// Add in our padding
mTotalLength += mPaddingTop + mPaddingBottom;
int heightSize = mTotalLength;
// Check against our minimum height
heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
// Reconcile our calculated size with the heightMeasureSpec
int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0); // 更新当前布局的heightSpec
heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
剩余空间分配
这里分了两种情况,存在子view没有测量或有剩余空间的情况行进行权重分配和useLargestChild模式下的权重分配,但是都要进行剩余空间的计算
计算剩余空间
// Either expand children with weight to take up available space or
// shrink them if they extend beyond our current bounds. If we skipped
// measurement on any children, we need to measure them now.
// 如果有子view没有被测量,再根据剩余空间分配,或者根据权重分配子view
int remainingExcess = heightSize - mTotalLength
+ (mAllowInconsistentMeasurement ? 0 : consumedExcessSpace);
// sdk<=23取前者,否则取后者,计算剩余的可以进行权重分配的空间
第一种情况
if (skippedMeasure || remainingExcess != 0 && totalWeight > 0.0f) {
// 存在子view没有被测量(当前布局是精确模式,而且存在子view没有高度,只有权重),或者还有剩余空间来进行权重分配
float remainingWeightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight;
mTotalLength = 0;
for (int i = 0; i < count; ++i) {
// 遍历所有子view
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) {
// 如果当前子view有权重
final int share = (int) (childWeight * remainingExcess / remainingWeightSum);
// 分给他应该有的剩余空间
remainingExcess -= share; // 计算剩余的空间
remainingWeightSum -= childWeight; // 计算剩余的权重
final int childHeight;
if (mUseLargestChild && heightMode != MeasureSpec.EXACTLY) {
// 用最大的子view分配高度
childHeight = largestChildHeight;
} else if (lp.height == 0 && (!mAllowInconsistentMeasurement
|| heightMode == MeasureSpec.EXACTLY)) {
// 子view参数高度为0,并且sdk > 23 或 当前布局模式是精确模式
// This child needs to be laid out from scratch using
// only its share of excess space.
childHeight = share; // 那些只设置了权重,没有设置高度的子view,直接分配应该有的空间
} else {
// This child had some intrinsic height to which we
// need to add its share of excess space.
// 如果子view本身有高度,就在原有的基础上加上权重分配来的高度
childHeight = child.getMeasuredHeight() + share;
}
final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
Math.max(0, childHeight), MeasureSpec.EXACTLY); // 利用计算出来的childHeight计算子view高度信息
final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin,
lp.width); // 利用当前布局宽度、间距、子view参数宽度计算子view的宽度信息
// 在这里,重新测量所有子view
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; // 子view宽度
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; // 更新是否全部是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 {
// 全部分配完毕,但又用了useLargestChild模式,就把有权重要求的子view的高度设为最大子view高度
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));
}
}
}
}
这里如果是使用最大子view,当前布局的最大高度并没有更新,原因参见我最开始分步骤时的注释
保存最大宽度
// 如果子view不都是fill_parent,就保存最大宽度
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);
存在子view宽度match_parent时的处理
if (matchWidth) {
// 子view是match_parent,但当前布局宽度不是精确模式
forceUniformWidth(count, heightMeasureSpec);
}
private void forceUniformWidth(int count, int heightMeasureSpec) {
// Pretend that the linear layout has an exact size.
int uniformMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth(),
MeasureSpec.EXACTLY); // 强制精确模式
for (int i = 0; i< count; ++i) {
final View child = getVirtualChildAt(i);
if (child != null && child.getVisibility() != GONE) {
LinearLayout.LayoutParams lp = ((LinearLayout.LayoutParams)child.getLayoutParams());
if (lp.width == LayoutParams.MATCH_PARENT) {
// Temporarily force children to reuse their old measured height
// FIXME: this may not be right for something like wrapping text?
int oldHeight = lp.height;
lp.height = child.getMeasuredHeight(); // 暂存参数中的height是子view的测量高度,确保高度不会因为measureChildWithMargins()而改变
// Remeasue with new dimensions
measureChildWithMargins(child, uniformMeasureSpec, 0, heightMeasureSpec, 0);
// 更新子view宽度为当前布局宽度,模式是精确模式
lp.height = oldHeight;
}
}
}
}
总结
可以看到,线性布局在处理权重分配时耗了比较大的精力,所以我们要尽量避免权重的设置,而要尽量通过跟ui同事的协调来确定准确的dp宽度,从而提高测量效率
通过跟相对布局的比较,会发现相对布局是通过设置四个端点的坐标来确定子view和自身的尺寸,而线性布局是直接测量高度或宽度来确定子view和自身的尺寸。或许从源码上看,线性布局代码要少一些,但它的灵活性要逊于相对布局,甚至可能要使用很多属性或层次,反而降低了效率增大了开销,所以还是要具体情况具体分析,相对布局和线性布局结合起来用,方可相得益彰
在安卓开发学习之LinearLayout的布局过程一文里,我将记录线性布局的onLayout()方法的阅读
上一篇: JS中的NaN
下一篇: 2021-04-20 四边形点顺时针排序