先写demo,看看实际表现。
布局代码如下:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="#ffffaa">
<com.lihy.customviewdemo.square.MyTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="子View match_parent"
android:background="#00ff00"/>
</LinearLayout>
MyTextView继承自AppCompatTextView,仅仅在onMeasure中加了日志
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int mode = MeasureSpec.getMode(widthMeasureSpec);
Log.e(TAG, "onMeasure: " +getModeString(mode));
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
private String getModeString(int mode){
if(mode == MeasureSpec.EXACTLY){
return "EXACTLY";
}
if(mode == MeasureSpec.AT_MOST){
return "AT_MOST";
}
return "UNSPECIFIED";
}
实际表现如截图
可以看到,LinearLayout和MyTextView的宽度都取了MyTextView内容所占宽度。和MyTextView宽度设置为了"wrap_conent"效果是一样的。
打开页面后,可以看到,onMeasure打印了4次
2024-04-29 16:53:45.353 28807-28807/com.lihy.customviewdemo E/MyTextView: onMeasure: AT_MOST
2024-04-29 16:53:45.353 28807-28807/com.lihy.customviewdemo E/MyTextView: onMeasure: EXACTLY
2024-04-29 16:53:45.369 28807-28807/com.lihy.customviewdemo E/MyTextView: onMeasure: AT_MOST
2024-04-29 16:53:45.369 28807-28807/com.lihy.customviewdemo E/MyTextView: onMeasure: EXACTLY
将MyTextView的宽度改为"wrap_content"后,onMeasure打印了2次
2024-04-29 16:55:55.096 29873-29873/com.lihy.customviewdemo E/MyTextView: onMeasure: AT_MOST
2024-04-29 16:55:55.105 29873-29873/com.lihy.customviewdemo E/MyTextView: onMeasure: AT_MOST
那么当LinearLayout的宽度为wrap_content时,子View的宽度为"match_parent"与"wrap_content"表现都是"wrap_content"了吗?
那也不是!!
当LinearLayout存在2个子View时,如果子View的宽度都为"match_parent",那么LinearLayout会将子View的width当作"wrap_content",去测量子View的实际宽度,取较大的那个作为LinearLayout的宽度。然后再重新测量宽度为"match_parent"的子View。代码不再贴出,最终表现如下:
下面我们看LinearLayout的onMeasure方法,是怎么做到这种效果的
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
这里我们走的是measureVertical(),省略了关于height的计算,只保留了计算width的逻辑
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
mTotalLength = 0;
int maxWidth = 0;
int childState = 0;
int alternativeMaxWidth = 0;
int weightedMaxWidth = 0;
boolean allFillParent = true;
final int count = getVirtualChildCount();
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
boolean matchWidth = false;
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
//lihy:首次测量
measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
heightMeasureSpec, usedHeight);
boolean matchWidthLocally = false;
//lihy:注意matchWidth的定义在循环外面,但凡存在一个子View宽度为MATCH_PARENT,matchWidth标记就为true。而matchWidthLocally在循环体里面,每次都重新赋值
if (widthMode != MeasureSpec.EXACTLY && lp.width == LayoutParams.MATCH_PARENT) {
matchWidth = true;
matchWidthLocally = true;
}
final int margin = lp.leftMargin + lp.rightMargin;
final int measuredWidth = child.getMeasuredWidth() + margin;
maxWidth = Math.max(maxWidth, measuredWidth);
childState = combineMeasuredStates(childState, child.getMeasuredState());
//lihy:是否当前View及之前的子View宽度全为match_parent
allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;
//lihy:当前View为match_parent时,matchWidthLocally为true,alternativeMaxWidth和margin做比较,即当前view的measureWidth无效。
// 为false时,alternativeMaxWidth的值取子View宽度最大值
alternativeMaxWidth = Math.max(alternativeMaxWidth,
matchWidthLocally ? margin : measuredWidth);
}
//lihy:可以看作这里的weightedMaxWidth就是0
alternativeMaxWidth = Math.max(alternativeMaxWidth,
weightedMaxWidth);
//lihy:当并不是所有子View都是match_parent时,maxWidth赋值alternativeMaxWidth。
//alternativeMaxWidth为所有子View(排除宽度为match_parent的)最大值
if (!allFillParent && widthMode != MeasureSpec.EXACTLY) {
maxWidth = alternativeMaxWidth;
}
maxWidth += mPaddingLeft + mPaddingRight;
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
//lihy:这里设置了自身的尺寸
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
heightSizeAndState);
//lihy:但凡存在宽度为match_parent的子View,会对所有子View重新measure
if (matchWidth) {
forceUniformWidth(count, heightMeasureSpec);
}
}
看完代码后,对这个LinearLayout宽度的赋值过程就更深刻了:
1. 当子View的宽度全部为match_parent时,LinearLayout取子View宽度最大值
2. 当子View的宽度存在wrap_content时,LinearLayout取子View(不包括“宽度为match_parent的子view”)的最大值
继续看forceUniformWidth(count, heightMeasureSpec),给子View设置的宽度是多少
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());
//lihy:可以看到,只会对子View宽度为"MATCH_PARENT"的重新测量
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();
measureChildWithMargins(child, uniformMeasureSpec, 0, heightMeasureSpec, 0);
lp.height = oldHeight;
}
}
}
}
从上面可以看到:
1. 只会对子View宽度为"MATCH_PARENT"的重新测量
2. 重新绘制时,对子View的约束为:size为测量好的父容器的宽度,mode为EXACTLY
结束:
- 宽度为"wrap_content"的子View会测量1次,传给子View的mode为"AT_MOST"。
- 宽度为"match_parent"的子View会测量2次,传给子View的mode:第1次是"AT_MOST",第2次是"EXACTLY"。
看下我们开始日志打印,onMeasure分别调用了2次和4次。也就是当前LinearLayout的方法被调用了2次。为什么外层容器的onMeasure会调用2次?