LinearLayout
是ViewGroup
的子类,ViewGroup
是View
的子类
不考虑View
上层绘制传递过程的,View
的测量,是从measure()
方法开始看
View 层测量起点
一个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>
遍历 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
再确定自身的宽高信息