LinearLayout是ViewGroup的子类,ViewGroup是View的子类
不考虑View上层绘制传递过程的,View的测量,是从measure()方法开始看
View 层测量起点
UI界面架构图
一个Activity,通过在onCreate()方法中,setContentView()方法,当作Content放在DecorView中
注意: DecorView 虽然宽高和手机屏幕一样,但是状态栏是不属于DecorView的
至于Activity如何通过setContentView()何DecorView建立起联系的,ViewRoot如何将WindowManager与DecorView以及自身关联的,没看懂,先不管
但View的measure,layout,draw三个流程都受ViewRoot控制,当一个ViewRoot与DecorView建立联系后,便会通过ViewRootImpl.performTraversals()来开始加载View
也就是说,View加载的起始点在ViewRootImpl.performTraversals()方法中开始
LinearLayout的测量,起点也就是这里
去除n多代码后:
private void performTraversals(){
...
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
performLayout(lp, mWidth, mHeight);
...
performDraw();
....
}
很直白的说明View的加载流程顺序,在performMeasure()方法中,调用了View的measure()方法
measure()方法是个final的,内部调用了onMeasure()方法
View.onMeasure()
当做一个直接继承自View自定义View时,需要重写这个方法,主要是为了处理宽和高使用wrap_content以及padding
注意:View的measure过程和Activity的生命周期方法是异步的,无法保证在onCreate(),onStart(),onResume()生命周期时,View已经测量完毕
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(
// 设置最终确定的宽 Width
getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
// 设置最终确定的高 Height
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)
);
}
由于LinearLayout重写了onMeasure()方法,也就是说,当measure()调用内部的onMeasure()方法时,会直接调用Linearlayout重写的onMearsure()方法
LinearLayout的测量onMeasure()
SDK源代码版本是25
代码:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
分Vertical和Horizontal两种情况
measureVertical()方法
有4个疑问:
-
LinearLayout内的所有childView的高度height如何累加的 -
LinearLayout的宽度width如何确定的 -
LinearLayout自身的height使用了wrap_content时,高度height如何确定 -
LinearLayout内的childView的高度使用了权重weight时,LinearLayout自身高度height如何确定,childView的高度如何确定
变量
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
// 统计所有的 verticalChildView 的高度和
mTotalLength = 0;
// 最大宽度
int maxWidth = 0;
// 子 View 的状态
int childState = 0;
// 可代替的最大宽度
int alternativeMaxWidth = 0;
// 使用 weight 属性的 childView 最大宽度
int weightedMaxWidth = 0;
// childView 是否都是 match_parent
// 判断是否需要重新测量
boolean allFillParent = true;
// 所的 Weight 总和
float totalWeight = 0;
// 获取 Virtual 的 childView 数量
final int count = getVirtualChildCount();
// LinearLayout 的 width 的 测量模式
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
// LinearLayout 的 hight 的 测量模式
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
//
boolean matchWidth = false;
// 是否跳过某个 childView,使用 weight 时,为true
boolean skippedMeasure = false;
// 基线对齐 childView 的 index
final int baselineChildIndex = mBaselineAlignedChildIndex;
//
final boolean useLargestChild = mUseLargestChild;
//
int largestChildHeight = Integer.MIN_VALUE;
//
int consumedExcessSpace = 0;
...
}
遍历累加 childView 的 height
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
// 局部变量
...
// See how tall everyone is. Also remember max width.
// 遍历数组统计 hight
for (int i = 0; i < count; ++i) {
// 获取一个 childView
final View child = getVirtualChildAt(i);
// 是否为 null
if (child == null) {
// measureNullChild() 目前返回值为 0
mTotalLength += measureNullChild(i);
continue;
}
// 判断 childView 的可见属性 Visibility 值
// 若不开见,就跳过测量
if (child.getVisibility() == View.GONE) {
// getChildrenSkipCount() 目前返回值 为 0
// 估计是预留给以后再做其他优化处理
i += getChildrenSkipCount(child, i);
continue;
}
// 是否需要加上 DividerHeight
if (hasDividerBeforeChildAt(i)) {
mTotalLength += mDividerHeight;
}
// 获取 childView 的 LayoutParams
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
// 累加权重
totalWeight += lp.weight;
// childView 是否使用 weight
final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
// 此时 LiLinearLayout 的 heightMode 为 MeasureSpec.EXACTLY
// 并且 childView 使用了 weight 权重
if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
// 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 {
// 判断 useExcessSpace 的值
// 若 useExcessSpace 为 true,说明 heightMode != EXACTLY
if (useExcessSpace) {
// 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.
// 此时,LinearLayout 的 heightMode 为 UNSPECIFIED 或者 AT_MOST
lp.height = LayoutParams.WRAP_CONTENT;
}
// 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).
// 已知的使用过的高度
// 先对 totalWeight 值进行判断,若为 0,说明到目前为止,遍历到的
// childView 没有使用 weight 属性的
// 若,有,就先将 usedHeight 置为0
final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
// 对当前的 childView 进行测量
measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
heightMeasureSpec, usedHeight);
// 获取 childView 的测量高
final int childHeight = child.getMeasuredHeight();
// 判断 useExcessSpace 值
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.
lp.height = 0;
consumedExcessSpace += childHeight;
}
// 记录临时总的 height
final int totalLength = mTotalLength;
// 统计 childView 使用 Margin 情况
mTotalLength =
Math.max(totalLength, totalLength + childHeight + lp.topMargin +
lp.bottomMargin + getNextLocationOffset(child));
// largestChildHeight,先不管
if (useLargestChild) {
largestChildHeight = Math.max(childHeight, largestChildHeight);
}
}
...
}
问题1,2
-
LinearLayout内的所有childView的高度height如何累加的 -
LinearLayout的宽度width如何确定的
当LinearLayout宽高都为match_parent,所有childView都没有使用weight时:
例如:
一个LinearLayout内,有两个TextView
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="@color/cardview_dark_background"
android:gravity="center"
android:text="@string/app_name"
android:textColor="@color/write"
android:textSize="30sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="@color/colorPrimaryDark"
android:gravity="center"
android:text="@string/measure_name"
android:textColor="@color/write"
android:textSize="30sp" />
</LinearLayout>
宽高都match_parent
遍历 childView
当执行到LinearLayout的measureVertical()方法内时,进入到遍历childView的for(int i = 0; i < count; ++i)循环后
直接开始考虑执行if (heightMode == MeasureSpec.EXACTLY && useExcessSpace){}else{}
由于两个TextView都没有使用weight,useExcessSpace为 false,也就走else{}内的逻辑
进入else{}内,最关键的点在于
final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
heightMeasureSpec, usedHeight)
measureChildBeforeLayout()方法便是内调用了ViewGroup的measureChildWithMargins()
在measureChildWithMargins()内,主要做了两件事:
- 根据
LinearLayout的MeasureSpec测量模式及自身padding,childView的LayoutParams和Margin,已经用掉的widthUsed,heightUsed,通过getChildMeasureSpec()计算出childView的MeasureSpec测量模式 - 拿到
childView的MeasureSpec测量模式之后,
child.measure(childWidthMeasureSpec, childHeightMeasureSpec),开始进入childView的measure()方法
measureChildWithMargins()方法内,childView的一些列测量回调方法完成后,此时也就可以拿到childHeight = child.getMeasuredHeight()
每拿到一个childView.height,将拿到的结果,累加进mTotalLength
final int totalLength = mTotalLength;
// 之前累加的高度,再加上当前childView的height,margin
// 目前getNextLocationOffset()返回结果都为 0,预留给以后做扩展的吧
mTotalLength = Math.max(totalLength, totalLength + childHeight +
lp.topMargin +lp.bottomMargin + getNextLocationOffset(child));
累加过当前的childView的hight之后,根据LinearLayout及两个TextView的宽高设置,此时matchWidthLocally是为false的
接着便是在遍历childView累加mTotalLength的同时,确定所有childView中的maxWidth
final int margin = lp.leftMargin + lp.rightMargin;
final int measuredWidth = child.getMeasuredWidth() + margin;
maxWidth = Math.max(maxWidth, measuredWidth);
先记录childView的leftMargin,rightMargin和,之后确定当前childView的measuredWidth,最后比较当前的childView的宽度与之前记录过的maxWidth做比较
TODO
childState = combineMeasuredStates(childState, child.getMeasuredState());
由于两个TextView都没有使用weight,所有if(lp.weight> 0){}else{}走的是else{}分支
在else{}内,会记录所有childView中最大的alternativeMaxWidth,当matchWidthLocally为false时,alternativeMaxWidth == maxWidth
for(){}也便结束,意味着childView遍历完成,接下来便是LiearLayout开始测量自身
mTotalLength便是当前LinearLayout的height,针对当前案例,maxWidth = alternativeMaxWidth = txet内容为RetrofitL的TextView的width
测量自身
根据LinearLayout的宽高及两个TextView的情况,useLargestChild是不考虑的
if (useLargestChild ... ) {}内的代码也就无需考虑
mTotalLength += mPaddingTop + mPaddingBottom加上自身的顶部和底部的padding
接着heightSize = mTotalLength,与背景background的高度做对比,取大值
之后再次计算LinaerLayout的精确高度
接着,走if (skippedMeasure ...) {}else{}的else{}分支,在else{}分支内,再次判断alternativeMaxWidth的大小,也就出了else{}分支
if (!allFillParent && widthMode != MeasureSpec.EXACTLY){},条件不满足,也就不会执行内部。此时,alternativeMaxWidth与maxWidth值是相等的
maxWidth += mPaddingLeft + mPaddingRight,加上LinearLayout自身左右内边距
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth()),比较maxWidth与背景的宽度width
最终也就调用了setMeasuredDimension()回调方法,设置最终的测量结果
问题3
-
LinearLayout自身的height使用了wrap_content时,高度height如何确定
当LinearLayout的height,两个TextView都没有使用weight时,整个逻辑和当当LinearLayout的height使用了mathch_parent一样
在遍历统计了所有的childView高度得到mTotalLength之后
// Add in our padding
mTotalLength += mPaddingTop + mPaddingBottom;
int heightSize = mTotalLength;
// Check against our minimum height
heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
int heightSizeAndState =
resolveSizeAndState(heightSize, heightMeasureSpec, 0);
heightSize = heightSizeAndState & MEASURED_SIZE_MASK
- 加上
LinearLayout自身的TopPadding,BottomPadding - 取
LinearLayout自身的高度与背景高度的大值 - 重新计算
LinearLayout自身的精确高度
问题4
-
LinearLayout内的childView的高度使用了权重weight时,LinearLayout自身高度height如何确定,childView的高度如何确定
加一个TextView,一个100dp,一个使用weight = 1,一个200dp
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="@color/cardview_dark_background"
android:gravity="center"
android:text="@string/app_name"
android:textColor="@color/write"
android:textSize="30sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="1"
android:background="@color/colorPrimaryDark"
android:gravity="center"
android:text="@string/measure_name"
android:textColor="@color/write"
android:textSize="30sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="200dp"
android:background="@color/colorAccent"
android:gravity="center"
android:text="@string/get_name"
android:textColor="@color/write"
android:textSize="30sp" />
</LinearLayout>
LinearLayout内有3个childView,getVirtualChildCount()也就为3
childView 遍历
- 第1个TextView
遍历childView时,第一个TextView为固定高度100dp,mTotalLength为100dp * 3,而alternativeMaxWidth等于measuredWidth为TextView的measuredWidth
- 第2个TextView
第2个TextView的height为0dp,weight为1
final boolean useExcessSpace
= lp.height == 0 && lp.weight > 0; // true
// 进入 if 分支
if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
final int totalLength = mTotalLength;
mTotalLength =
Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
skippedMeasure = true;
}else{
...
}
此时,LinearLayout也不知道当前这个TextView在weight为1的情况下高度为多少,会先跳过测量
在进入if分支语句内,只是统计了下当前childView的lp.topMargin + lp.bottomMargin,并将skippedMeasure设置为true,便结束if(){}else{}分支
计算统计maxWidth之后
if (lp.weight > 0) {
/*
* Widths of weighted Views are bogus if we end up
* remeasuring, so keep them separate.
*/
weightedMaxWidth = Math.max(weightedMaxWidth,
matchWidthLocally ? margin : measuredWidth);
} else {
alternativeMaxWidth = Math.max(alternativeMaxWidth,
matchWidthLocally ? margin : measuredWidth);
}
进入if(){}分支,记录下weightedMaxWidth
- 第3个TextView
第3个TextView和上面不用weight的情况下一样,将height累加到mTotalLength,之后再比较下当前的TextView的measuredWidth和之前的记录过的maxWidth的大小
统计自身及测量使用 weight 的 TextView
结束遍历childView后,LinearLayout统计自身的流程和不使用weight一样
mTotalLength先加水顶部和底部的padding,再比较mTotalLength和背景的高度,取大值,再根据heightMeasureSpec确认下自身的高度,这个高度就是LinearLayout最终要在屏幕显示时的高度,再确认在height方向是否还有剩余空间
由于第2个TextView使用了weight,跳过了测量,而LinearLayout自身高度使用match_parent,这时,肯定会有预留了空间,需要再次进行遍历测量
在if()内,条件用的是||,在遍历到第2个TextView时,已经将skippedMeasure置为true
还有一种情况,skippedMeasure == false,但remainingExcess != 0 && totalWeight > 0.0f为true。这个时候,LinearLayout的高使用wrap_content,而内部的childView使用weight
int remainingExcess =
heightSize - mTotalLength
+ (mAllowInconsistentMeasurement ? 0 : consumedExcessSpace);
if (skippedMeasure || remainingExcess != 0 && totalWeight > 0.0f) {
// 重置
mTotalLength = 0;
// 再次遍历
for (int i = 0; i < count; ++i) {
...
if(childWeight){
// 统计并确认使用 weight 的 childView 高度
...
}
}
}else{
...
}
在for(){}内,先查看每个当前的childView的weight是否大于0,if (childWeight > 0) { ... }
在遍历前,mTotalLength被重置为了0
- 第1个
TextView
第1个TextView并没有使用weight,不会走if(childWeight){}分支内逻辑
记录TextView的宽度,保存所有的childView中最大的宽度maxWidth
final int margin = lp.leftMargin + lp.rightMargin;
final int measuredWidth = child.getMeasuredWidth() + margin;
maxWidth = Math.max(maxWidth, measuredWidth);
接着是将TextView的高度累加到mTotalLength
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + child.getMeasuredHeight()
+lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
mTotalLength在遍历前被重置为0,根据当前例子,此时mTotalLength为child.getMeasuredHeight()
- 第2个TextView
第2个TextView使用了weight,会进入if(childWeight){}语句内
首先,根据weight和剩余空间大小来确定可用的空间高度
final int share = (int) (childWeight * remainingExcess / remainingWeightSum);
remainingExcess -= share;
remainingWeightSum -= childWeight;
remainingExcess是在遍历之前计算出来的剩余空间,remainingWeightSum是所有childView的weight值和
当前的TextView可用的空间高度,就是share = weight /remainingWeightSum * 剩余总高度
计算得到share之后,可用空间就要减去share高度,同时remainingWeightSum也要减去当前TextView的weight
得到高度空间后,根据条件,将share分配给TextView
final int childHeight;
if (mUseLargestChild && heightMode != MeasureSpec.EXACTLY) {
childHeight = largestChildHeight;
} else if (lp.height == 0 && (!mAllowInconsistentMeasurement
|| heightMode == MeasureSpec.EXACTLY)) {
// This child needs to be laid out from scratch using
// only its share of excess space.
childHeight = share;
} else {
// This child had some intrinsic height to which we
// need to add its share of excess space.
childHeight = child.getMeasuredHeight() + share;
}
mUseLargestChild默认值为false,在这个例子中为false,不会进入if( ... ){}分支
在四个参数的构造方法中
mAllowInconsistentMeasurement = version <= Build.VERSION_CODES.M;
手机版本是6.0(23),mAllowInconsistentMeasurement为true,! mAllowInconsistentMeasurement就为false
但heightMode == MeasureSpec.EXACTLY为true,最终会进入到else if ( ... ){ }
分支中,进行childHeight = share赋值,之后便结束整个if(){}分支
进行打包确认childWidthMeasureSpec, childHeightMeasureSpec之后进入View.measure()方法
child.measure(childWidthMeasureSpec, childHeightMeasureSpec),这里便进入到了TextView,measure(),if(childWeight){ ... }便结束
之后便可以拿到第2个TextView的宽高,再次记录maxWidth,再把height累加到mTotalLength
第3个TextView的过程便和第1个一样
当3个TextView遍历结束后,mTotalLength += mPaddingTop + mPaddingBottom,累加顶部个底部的paddding,结束if (skippedMeasure || remainingExcess != 0 && totalWeight > 0.0f) {}else{}
最后进行setMeasuredDimension( ... ),最终,LinearLayout的宽高便确定
总结
当LinearLayout使用vertical时
-
自身的
height使用match_parent,wrap_content时,在测量阶段都是先对内部childViews遍历一次,拿到累积的高度,及childViews中最大的maxWidth,之后再测量确定自身的高度和宽度 -
当
childViews有使用weight并设置height = 0dp时,在第一次遍历chidlViews时,LinearLayout会先测量没有使用weight的childView,拿到高度后与根据heightMeasureSpec计算出来的高度作对比计算,可以得到剩余空间高度,再次遍历childViews,再根据weight进行计算出使用weight的childView的高度。拿到所有的childView的宽高信息后,LinearLayout再确定自身的宽高信息










网友评论