背景
昨天我记录了安卓中相对布局的测量流程源码阅读,之后又读了一下线性布局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()方法的阅读