LinearLayout源码详解
需要关注的点
1:linearlayout只进行一次测量,但是在设置了weight之后会进行两次测量
源码分析
一般所有控件类的源码,都会从 measure, layout和draw3个方法入手,查看他们的回调函数onMeasure, onLayout和onDraw 只要明白这3个流程,一般控件的整个实现也就明白了 LinearLayout作为一个ViewGroup的子类,主要作为一个布局容器出现,所以我们需要重点查看onMeasure方法,
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
在onMeasure中,根据orientation的值来进行纵向的测量或者是横向的测量,纵向和横向测量的逻辑相识,所以我们只需要选取其中一个来进行分析,这里,我们只分析纵向的测量
//记录了内部已经被使用的高度
mTotalLength = 0;
//childView 最大的宽度,用于计算LinearLayout的宽度
int maxWidth = 0;
int childState = 0;
int alternativeMaxWidth = 0;
int weightedMaxWidth = 0;
boolean allFillParent = true;
//权重值的总和
float totalWeight = 0;
//纵向上面子控件的个数
final int count = getVirtualChildCount();
//宽度模式和高度模式
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
boolean matchWidth = false;
boolean skippedMeasure = false;
final int baselineChildIndex = mBaselineAlignedChildIndex;
final boolean useLargestChild = mUseLargestChild;
int largestChildHeight = Integer.MIN_VALUE;
int consumedExcessSpace = 0;
int nonSkippedChildCount = 0;
首先初始化了一大堆常量,这里我们只需要关注我注释了的几个就好了
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)) {
mTotalLength += mDividerHeight;
}
首先是将子控件取出来,判断控件是否为null,如果为空或者Visibility为gone就直接下一个控件,这里也可以看出来gone与invisible的区别,如果有分割线,再将分割线的高度也加上
//有时候我们在代码里面通过Inflater服务,动态加载一个布局,然后去设置他的LayoutParams,如果不引用父容器的LayoutParams就会报一个强转错误,原因就在这个 父容器在add,measure的时候都会把子View的LayoutParams强转成自己的类型
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
// 得到每个子控件的LayoutParams后,累加权重和,后面用于跟weightSum相比较
totalWeight += lp.weight;
// 我们都知道,测量模式有三种:
// * UNSPECIFIED:父控件对子控件无约束,一般只有在ScrollView这种滑动布局中才会用到
// * Exactly:父控件对子控件强约束,子控件永远在父控件边界内,越界则裁剪。如果要记忆的话,可以记忆为有对应的具体数值或者是Match_parent
// * AT_Most:子控件为wrap_content的时候,测量值为AT_MOST。
//下面的if/else分支都是跟weight相关
final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
// 这个if里面需要满足三个条件:
// * LinearLayout的高度为match_parent(或者有具体值)
// * 子控件的高度为0
// * 子控件的weight>0
// 这其实就是我们通常情况下用weight时的写法
// 测量到这里的时候,会给个标志位,稍后再处理。此时会计算总高度
// 除开这种情况的子控件不需要measure,其他的子控件都需要被measure一次,所以这样设置属性也可以提升性能
// Optimization: don't bother measuring children who are only
// laid out using excess space. These views will get measured
// later if we have space to distribute.
//优化:不要麻烦测量那些只使用多余空间的孩子。如果我们有空间分布,这些视图将在稍后得到度量。
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
skippedMeasure = true;
} else {
if (useExcessSpace) {
// 满足这两个条件,意味着父类即LinearLayout是wrap_content,或者mode为UNSPECIFIED
// 那么此时将当前子控件的高度置为wrap_content
// 为何需要这么做,主要是因为当父类为wrap_content时,其大小实际上由子控件控制
// 我们都知道,自定义控件的时候,通常我们会指定测量模式为wrap_content时的默认大小
// 这里强制给定为wrap_content为的就是防止子控件高度为0.
// The heightMode is either UNSPECIFIED or AT_MOST, and
// this child is only laid out using excess space. Measure
// using WRAP_CONTENT so that we can find out the view's
// optimal height. We'll restore the original height of 0
// after measurement.
//heightMode 是 UNSPECIFIED or AT_MOST, childView 只使用剩余空间
//使用WRAP_CONTENT进行测量,以便找出视图的最佳高度。测量后恢复原来高度 0
lp.height = LayoutParams.WRAP_CONTENT;
}
// 如果当前的LinearLayout不是EXACTLY模式,且子View的weight大于0,优先会把当前LinearLayout的全部可用高度用于子View测量
// 我们在代码中也可以很清晰的看到,在getChildMeasureSpec()中,子控件需要把父控件的padding,自身的margin以及一个可调节的量三者一起测量出自身的大小。那么假如在测量某个子控件之前,weight一直都是0,那么该控件在测量时,需要考虑在本控件之前的总高度,来根据剩余控件分配自身大小。而如果有weight,那么就不考虑已经被占用的控件,因为有了weight,子控件的高度将会在后面重新赋值。
// Determine how big this child would like to be. If this or
// previous children have given a weight, then we allow it to
// use all available space (and we will shrink things later
// if needed).
//确定这个孩子想要多大。如果这个或前面的孩子给了一个权重,那么我们允许它使用所有可用的空间
//(如果需要,我们将在稍后缩小内容)。
final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
heightMeasureSpec, usedHeight);
// 重置子控件高度,然后进行精确赋值
final int childHeight = child.getMeasuredHeight();
if (useExcessSpace) {
// Restore the original height and record how much space
// we've allocated to excess-only children so that we can
// match the behavior of EXACTLY measurement.
//恢复原来的高度,并记录我们分配给 excess-only children 的空间大小,以便我们能够准确地匹配测量的行为。
lp.height = 0;
consumedExcessSpace += childHeight;
}
final int totalLength = mTotalLength;
// getNextLocationOffset返回的永远是0,这里是加上子控件的margin值
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
lp.bottomMargin + getNextLocationOffset(child));
// 如果设置了measureWithLargestChild属性为true,获取最高子控件的高度
if (useLargestChild) {
largestChildHeight = Math.max(childHeight, largestChildHeight);
}
}
useLargestChild 可以通过 xml 属性 android:measureWithLargestChild 设置的,含义是所有带权重属性的View都会使用最大View的最小尺寸
//useLargestChild 属性指定
//所以接下来根据 largestChildHeight 重新计算高度
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));
}
}
使用 largestChildHeight 重新计算mTotalLength,在代码中也可以看到,这个属性只在wrap_content情况下生效
到第二次测量中间还有一些其他的计算,就不一一去看了,直接看第二次测量
// 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.
// 重新计算有 weight 属性的 childView 大小,
// 如果还有可用的空间,则扩展 childView,计算其大小
// 如果 childView 超出了 LinearLayout 的边界,则收缩 childView
int remainingExcess = heightSize - mTotalLength
+ (mAllowInconsistentMeasurement ? 0 : consumedExcessSpace);
if (skippedMeasure
|| ((sRemeasureWeightedChildren || remainingExcess != 0) && totalWeight > 0.0f)) {
// 根据 mWeightSum 计算得到 remainingWeightSum,mWeightSum 是通过
// android:weightSum` 属性设置的,totalWeight 是通过第一次 for 循环计算得到的
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;
// 这是设置了 weight 的情况下,最重要的一行代码
// remainingExcess 剩余高度 * ( childView 的 weight / remainingWeightSum)
// share 便是此 childView 通过这个公式计算得到的高度,
// 并重新计算剩余高度 remainingExcess 和剩余权重总和 remainingWeightSum
if (childWeight > 0) {
final int share = (int) (childWeight * remainingExcess / remainingWeightSum);
remainingExcess -= share;
remainingWeightSum -= childWeight;
final int childHeight;
if (mUseLargestChild && heightMode != MeasureSpec.EXACTLY) {
childHeight = largestChildHeight;
} else if (lp.height == 0 && (!mAllowInconsistentMeasurement
|| heightMode == MeasureSpec.EXACTLY)) {
//如果是当前LinearLayout的模式是EXACTLY
//那么这个子View是没有被测量过的,就需要测量一次
//如果不是EXACTLY的,在第一次循环里就被测量一些了
// This child needs to be laid out from scratch using
// only its share of excess space. childHeight = share;
} else {
//如果是非EXACTLY模式下的子View就再加上
//weight分配占比*剩余高度
// This child had some intrinsic height to which we
// need to add its share of excess space. 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));
}
}
}
}
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);
}
}
举例
在weight计算方面,我们可以清晰的看到,weight为何是针对剩余空间进行分配的原理了。 我们打个比方,假如现在我们的LinearLayout的weightSum=10,总高度100,有两个子控件(他们的height=0dp),他们的weight分别为2:8。
那么在测量第一个子控件的时候,可用的剩余高度为100,第一个子控件的高度则是100*(2/10)=20,接下来可用的剩余高度为80
我们继续第二个控件的测量,此时它的高度实质上是80*(8/8)=80
到目前为止,看起来似乎都是正确的,但关于weight我们一直有一个疑问:**就是我们为子控件给定height=0dp和height=match_parent时我们就会发现我们的子控件的高度比是不同的,前者是2:8而后者是调转过来变成8:2 **
对于这个问题,我们不妨继续看看代码。
接下来我们会看到这么一个分支:
if ((lp.height != 0) || (heightMode != MeasureSpec.EXACTLY)) { } else {}
首先我们不管heightMode,也就是父类的测量模式,剩下一个判定条件就是lp.height,也就是子类的高度。
既然有针对这个进行判定,那就是意味着肯定在此之前对child进行过measure,事实上,在这里我们一早就对这个地方进行过描述,这个方法正是measureChildBeforeLayout()。
还记得我们的measureChildBeforeLayout()执行的先行条件吗
YA,just u see,正是不满足(LinearLayout的测量模式非EXACTLY/child.height==0/child.weight/child.weight>0)之中的child.height==0
因为除非我们指定height=0,否则match_parent是等于-1,wrap_content是等于-2.
在执行measureChildBeforeLayout(),由于我们的child的height=match_parent,因此此时可用空间实质上是整个LinearLayout,执行了measureChildBeforeLayout()后,此时的mTotalLength是整个LinearLayout的大小
回到我们的例子,假设我们的LinearLayout高度为100,两个child的高度都是match_parent,那么执行了measureChildBeforeLayout()后,我们两个子控件的高度都将会是这样:
child_1.height=100
child_2.height=100
mTotalLength=100+100=200
在一系列的for之后,执行到我们剩余空间:
int delta = heightSize – mTotalLength;
(delta=100[linearlayout的实际高度]-200=-100)
没错,你看到的的确是一个负数。
接下来就是套用weight的计算公式:
share=(int) (childExtra * delta / weightSum)
即:share=-100(2/10)=-20;*
然后走到我们所说的if/else里面
if ((lp.height != 0) || (heightMode != MeasureSpec.EXACTLY)) {
// child was measured once already above...
// base new measurement on stored values
int childHeight = child.getMeasuredHeight() + share;
if (childHeight < 0) {
childHeight = 0;
}
child.measure(childWidthMeasureSpec,
MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY));
}
我们知道child.getMeasuredHeight()=100
接着这里有一条int childHeight = child.getMeasuredHeight() + share;
这意味着我们的childHeight=100+(-20)=80;
接下来就是走child.measure,并把childHeight传进去,因此最终反馈到界面上,我们就会发现,在两个match_parent的子控件中,weight的比是反转的。
小结
在文章的最后,我们小结一下对于测量这里的算法的不同情况下的区别以及原理:
父控件是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源码详解分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/13463.html