LinearLayout基础
LinearLayout所具有的属性:
- orientation:视图的布局方向,默认值:-1;
- gravity:绘制起始点,默认值:-1;
- baselineAligned:基准线对齐,其效果可以通过修改xml的属性值直接看到效果,默认值:true;
- weightSum:子视图权重和,默认值:-1.0f;
- baselineAlignedChildIndex:以第Index个子视图的基准线为对齐,该LinearLayout下的view以
某个继承TextView的View的基线对齐,默认值:-1; - measureWithLargestChild:以最大子视图宽高,为其子视图的宽高,其起作用前提是为true,且LinearLayout在该方向的宽或高为warp_content,且子视图具有权重。默认值:false;
- divider:分割线;
- showDividers:分割线显示样式(middle|end|beginning|non),默认值:SHOW_DIVIDER_NONE;
dividerPadding:分割线内边距,默认值:0;
解释:
- 基准线
其主要作用是在绘制字母的时候有个基线对齐,这个类似我们学习英语字母的时候用的四线谱:
其中红线就是基线(baseline),和下面我们书写英语字母的四线谱是不是很像,基线就是第三条。
- 源码之垂直方向测量(void measureVertical(int widthMeasureSpec, int heightMeasureSpec))
1 | /** |
在垂直绘制中主要执行逻辑在两大块代码,第一个for循环,第二个if判断中的for循环,接下来我们分块分析该函数源码:
变量的定义
1 | void measureVertical(int widthMeasureSpec, int heightMeasureSpec) { |
在变量定义中,我们主要留意三个方面:
- mTotalLength:这个就是最终得到的整个LinearLayout的高度(子控件高度累加及自身padding)
- 三个跟width相关的变量
- weight相关的变量
第一个for代码块和baselineChildIndex处理
1 | void measureVertical(int widthMeasureSpec, int heightMeasureSpec) { |
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
// … 局部变量定义和第一个for循环
// 下面的这一段代码主要是为useLargestChild属性服务的,不在本文主要分析范围,略过
if (mTotalLength > 0 && hasDividerBeforeChildAt(count)) {
mTotalLength += mDividerHeight;
}
if (useLargestChild &&
(heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED)) {
mTotalLength = 0;
// 如果设置了useLargestChild属性,且LinearLayout的垂直方向测量模式是AT_MOST或UNSPECIFIED,
//重新测量总高度,useLargestChild属性会使所有带weight属性的子视图具有最大子视图的最小尺寸
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));
}
}
//... 第2个重要代码块
}
1 | 这里主要对useLargestChild属性处理,执行前提是设置了useLargestChild属性,且LinearLayout的垂直 |
//当测量完子View的大小后,总高度会再加上padding的高度
// Add in our padding
mTotalLength += mPaddingTop + mPaddingBottom;
int heightSize = mTotalLength;
//如果设置了minimumheight属性,会根据当前使用高度和最小高度进行比较
//然后取两者中大的值,getSuggestedMinimumHeight为背景的最小高和视图设置的最小高的大值
// Check against our minimum height
heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
// Reconcile our calculated size with the heightMeasureSpe
// 把测量出来的高度与测量模式进行匹配,得到最终的高度,MeasureSpec实际上是一个32位的int,高两位是测量模式,剩下的就是大小,因此heightSize = heightSizeAndState & MEASURED_SIZE_MASK;作用就是用来得到大小的精确值(不含测量模式)
int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
//到了这里,会再对带weight属性的子View进行一次测绘
//首先计算剩余高度
//算出剩余空间,假如之前是skipp的话,那么几乎可以肯定是有剩余空间(同时有weight)的
// 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.
int delta = heightSize - mTotalLength;
if (skippedMeasure || delta != 0 && totalWeight > 0.0f) {
//如果设置了weightSum就会使用你设置的weightSum,否则采用当前所有子View的权重和。所以如果要手动设置weightSum的时候,千万别计算错误哦
float weightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight;
mTotalLength = 0;
//这里的代码就和第一次测量很像了
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
if (child.getVisibility() == View.GONE) {
continue;
}
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
float childExtra = lp.weight;
if (childExtra > 0) {
// 全篇最精华的一个地方。。。。拥有weight的时候计算方式,ps:执行到这里时,child依然还没进行自身的measure
//子控件的weight占比*剩余高度
// Child said it could absorb extra space -- give him his share
int share = (int) (childExtra * delta / weightSum);
// weightSum计余
weightSum -= childExtra;
//剩余高度减去分配出去的高度
delta -= share;
final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
mPaddingLeft + mPaddingRight +
lp.leftMargin + lp.rightMargin, lp.width);
//如果是当前LinearLayout的模式是EXACTLY
//那么这个子View是没有被测量过的,就需要测量一次
//如果不是EXACTLY的,在第一次循环里就被测量一些了
// TODO: Use a field like lp.isMeasured to figure out if this
// child has been previously measured
if ((lp.height != 0) || (heightMode != MeasureSpec.EXACTLY)) {
// child was measured once already above...
// base new measurement on stored values
//如果是非EXACTLY模式下的子View就再加上
//weight分配占比*剩余高度
// 上面已经测量过这个子视图,把上面测量的结果加上根据weight分配的大小
int childHeight = child.getMeasuredHeight() + share;
if (childHeight < 0) {
childHeight = 0;
}
//重新测量一次,因为高度发生了变化
child.measure(childWidthMeasureSpec,
MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY));
} else {
// child was skipped in the loop above.
// Measure for this first time here
//如果是EXACTLY模式下的
//这里只会把weight占比所拥有的高度分配给你的子View
// 上面测量的时候被跳过,那么在这里进行测量
child.measure(childWidthMeasureSpec,
MeasureSpec.makeMeasureSpec(share > 0 ? share : 0,
MeasureSpec.EXACTLY));
}
// 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 {
// 没有weight的情况下,只看useLargestChild参数,如果都无相关,那就走layout流程了,因此这里忽略
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.
// 使所有具有weight属性 视图都和最大子视图一样高,子视图可能在上面已经被测量过一次
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));
}
}
}
}
```
在进入ifelse分支前,先计算视图的总高度,并与测量模式进行比较(resolveSizeAndState)得到最终高度,
在减去总高度,得到最终还剩多高(也就是可以分配给带权重的视图的高);
ifelse首先判断(skippedMeasure || delta != 0 && totalWeight > 0.0f),
如果该条件为true,先将总高度置为0再进入for循环,此处根据子视图的权重,再次判定(lp.height != 0) || (heightMode != MeasureSpec.EXACTLY)
- true:子视图再次测量 则计算子视图可以得到多少高(有可能为负,也就是子视图要吐出一定高度出来),然后子视图测量高度和分配的高度相加,小于0,则重置为0,最后在测量一次。
false:直接测量子视图,这个是之前被跳过没有测量的子视图;
最后再次测量视图的宽和总高度。如果该条件为false:看useLargestChild参数,如果都无相关,那就走layout流程了,
我们可以看到这里直接判断是useLargestChild && heightMode != MeasureSpec.EXACTLY,如果条件成立的话,遍历子视图,再次判断子视图是否含有权重,如果有则直接将子视图高度都是largestChildHeight。如果条件不成立,则啥也不做。
最后就是视图maxWidth计算,并setMeasuredDimension(这个是一定要的),自此垂直方向已经测量完毕。
总结
这里大篇幅讲解measureVertical()的流程,事实上对于LinearLayout来说,其最大的特性也正是两个
方向的排布以及weight的计算方式。回过头来看测量过程,我们可以看出设计者的测量计算思路,就是将有weight
和不含有weight的测量分开处理,再利用height跟0比较来更加的细分每一种情况。
最后我们在理下其测量不同情况和原理:
- 父控件为match_parent(或者精确值),子控件拥有weight,并且高度给定为0,也即子控件明确表示使用剩余空间:
- 子控件的高度比例将会跟我们分配的layout_weight一致,原因在于weight二次测量时走了else分支,传入的是计算出来的share值;
- 父控件是match_parent(或者精确值),子控件拥有weight,但高度给定为match_parent(或者精确值),子控件使用自己的高度或者父控件的高度,但在父控件空间不足时,其大小可以调整:
- 子控件高度比例将会跟我们分配的layout_weight相反,原因在于在此之前子控件测量过一次,同时子控件的测量高度为父控件的高度,在计算剩余空间的时候得出一个负值,加上自身的测量高度的时候反而更小;
- 父控件是wrap_content,子控件拥有weight:
- 子控件的高度将会强行置为其wrap_content给的值并以wrap_content模式进行测量
- 父控件是wrap_content,子控件没有weight:
- 子控件的高度跟其他的viewgroup一致
自此,LinearLayout在垂直方向的测量分析已经结束。
参考地址:
[1]. baselineAligned解析 http://www.bubuko.com/infodetail-612730.html
[2]. measureWithLargestChild使用解析 https://blog.csdn.net/a87b01c14/article/details/49420449
[3]. LinearLayout垂直测量分析 https://www.jianshu.com/p/aea27bac7c8e
[4]. view和LinearLayout源码分析 https://www.jianshu.com/p/f9b9f05222a8