Android 的自定义控件写过很多了,但对于自定义 Layout 始终不够熟悉。原因之一是对 measure 和 layout 有点怵,测量模式在测量时占有很重要的作用。今天我们来学习下 Android的测量模式,把 ViewGroup 的神秘面纱一步步揭开。
MeasureSpec
官方文档对 MeasureSpec 的介绍是,MeasureSpec 封装了父控件对自控件的要求,每一个 MeasureSpec 代表着对宽度或者高度的要求,这个要求包含一个 size 和一种模式。
MeasureSpec 的三种模式
UNSPECIFIED
父控件对子控件没有任何要求,子控件可以要任何他想要的大小。
EXACTLY
父控件确定了严格的大小给子控件,子控件只能是这个大小。
AT_MOST
父控件给定了最大 size ,子控件不能超过它。
Demo
只说不练,可能很快就忘了,现在我们写一个标签布局练一下手。 不解释了,直接上源码:
#LabelLayout
public class LabelLayout extends ViewGroup {
private int mItemPadding = 30;
private int mItemSpaceing = 15;
public LabelLayout(Context context) {
super(context);
}
public LabelLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
@Override
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
return new MarginLayoutParams(lp);
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new MarginLayoutParams(MATCH_PARENT, LayoutParams.WRAP_CONTENT);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//父控件传进来的宽度和高度以及对应的测量模式
int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
int modeHeight = MeasureSpec.getMode(heightMeasureSpec);
int usedHeight = 0;
int usedWidth = 0;
//获取子view的个数
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
measureChild(child, sizeWidth, usedWidth, modeWidth, modeHeight);
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
if (i == 0) {
usedHeight += childHeight;
}
//如果当前剩余宽度不足以放下当前这个 child 换行
if (childWidth + usedWidth > sizeWidth) {
usedHeight += childHeight;
usedWidth = childWidth;
} else {
usedWidth = usedWidth + childWidth;
}
}
int heightSizeAndState = resolveSizeAndState(usedHeight, heightMeasureSpec, 0);
int widthSizeAndState = resolveSizeAndState(sizeWidth, widthMeasureSpec, 0);
setMeasuredDimension(widthSizeAndState, heightSizeAndState);
}
private void measureChild(View child, int parentWidth, int usedWidth, int parentSpecWidth, int parentSpecHeight) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
int childWidthMeasureSpec;
int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY);
switch (lp.width) {
case MATCH_PARENT:
//如果当前 View 的宽度是 match_parent,父 View 的测量模式是 EXACTLY 或 AT_MOST,子 View 要填满所有剩余的父空间,所以自己的测量模式是 EXACTLY
if (parentSpecWidth == MeasureSpec.EXACTLY || parentSpecWidth == MeasureSpec.AT_MOST) {
childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(parentWidth, MeasureSpec.EXACTLY);
} else {
//父 View 的测量模式是 UNSPECIFIED,也就是说父控件的可用空间可能是无限大,自己又是 MATCH_PARENT ,那怎么填?
//只能把 UNSPECIFIED 传下去,而 size 只能填0
childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
}
break;
case WRAP_CONTENT:
//如果当前 View 的宽度是 wrap_content,父 View 的测量模式是 EXACTLY 或 AT_MOST
// 也就是说子控件的最大可用空间是父控件的可用空间
// 所以测量模式是 AT_MOST 而 size 是父控件的可用 size
if (parentSpecWidth == MeasureSpec.EXACTLY || parentSpecWidth == MeasureSpec.AT_MOST) {
childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(parentWidth, MeasureSpec.AT_MOST);
} else {
//父 View 的测量模式是 UNSPECIFIED,也就是说父控件的可用空间可能是无限大,自己又是 WRAP_CONTENT ,那怎么填?
//只能把 UNSPECIFIED 传下去,而 size 只能填0
childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
}
break;
default:
childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY);
break;
}
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int width = getWidth();
int usedWidth = 0;
int usedHeight = 0;
//获取子view的个数
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
if (i == 0) {
usedHeight += childHeight;
}
if (childWidth + usedWidth > width) {
usedHeight = usedHeight + childHeight;
usedWidth = 0;
}
child.layout(usedWidth, usedHeight - childHeight, +usedWidth + +childWidth, usedHeight);
usedWidth += childWidth;
}
}
}
#布局文件
<?xml version="1.0" encoding="utf-8"?>
<me.xfly.viewdragdemo.LabelLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#999999">
<TextView
android:layout_width="wrap_content"
android:layout_height="30dp"
android:layout_margin="0dp"
android:text="fsasdfasfdasfdasdfasd"
android:background="#ff0000" />
<TextView
android:layout_width="wrap_content"
android:layout_height="30dp"
android:text="黑 我真的好想你"
android:layout_margin="0dp"
android:background="#ffff00" />
<TextView
android:layout_width="80dp"
android:layout_height="30dp"
android:layout_margin="0dp"
android:background="#ffffff" />
<TextView
android:layout_width="match_parent"
android:layout_height="30dp"
android:layout_margin="0dp"
android:background="#ff00ff" />
<TextView
android:layout_width="match_parent"
android:layout_height="30dp"
android:layout_margin="0dp"
android:background="#00ff00" />
<TextView
android:layout_width="50dp"
android:layout_height="30dp"
android:layout_margin="0dp"
android:background="#00ffff" />
<TextView
android:layout_width="80dp"
android:layout_height="30dp"
android:layout_margin="0dp"
android:background="#0000ff" />
<TextView
android:layout_width="80dp"
android:layout_height="30dp"
android:layout_margin="0dp"
android:background="#ff0000" />
<TextView
android:layout_width="100dp"
android:layout_height="30dp"
android:layout_margin="0dp"
android:background="#ffff00" />
<TextView
android:layout_width="80dp"
android:layout_height="30dp"
android:layout_margin="0dp"
android:background="#ffffff" />
<TextView
android:layout_width="90dp"
android:layout_height="30dp"
android:layout_margin="0dp"
android:background="#ff00ff" />
<TextView
android:layout_width="80dp"
android:layout_height="30dp"
android:layout_margin="0dp"
android:background="#00ff00" />
<TextView
android:layout_width="150dp"
android:layout_height="30dp"
android:layout_margin="0dp"
android:background="#00ffff" />
<TextView
android:layout_width="80dp"
android:layout_height="30dp"
android:layout_margin="0dp"
android:background="#0000ff" />
</me.xfly.viewdragdemo.LabelLayout>
效果图如下:
这个 Demo 中没有考虑 margin 和 padding,不过本节主要的内容是测量模式。margin 和 padding 只是一些体力活,不影响最终效果。
总结
上面 measureChild 中注释都已经写的很清楚了,不过还是要总结一下:
- 在调用 child.measure 之前要先生成 child 的两个 MeasureSpec。
- 在生成 MeasureSpec 的时候,开发者的意见最重要,如果开发者要的是固定大小。直接给它这个大小,测量模式是 EXACTLY,MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY);。
- 如果子 View 想要 match_parent
- 父 View 的测量模式是 EXACTLY,子 View 的测量模式也是 EXACTLY,可用大小是父 View 剩余可用空间。 MeasureSpec.makeMeasureSpec(父 View 可用空间, MeasureSpec.EXACTLY);
- 父 View 的测量模式是 AT_MOST,父 View 有一个最大可用空间,子 View 要占满它。MeasureSpec.makeMeasureSpec(父 View 可用空间, MeasureSpec.EXACTLY);
- 父 View 的测量模式是 UNSPECIFIED。
注意这里有一个问题,父 View UNSPECIFIED 也就是不限制大小,那么父 View 的可用空间就是无限大,子 View match_parent,子 View 也是无限大?size 怎么写?
这里只能给子 View UNSPECIFIED,size 写 0。MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
- 如果子 View 要 wrap_content:
- 父 View 的测量模式是 EXACTLY,子 View [最大]大小是父 View 的剩余可用空间,因此测量模式是 AT_MOST。 MeasureSpec.makeMeasureSpec(父 View 可用空间, MeasureSpec.AT_MOST);
- 父 View 的测量模式是 AT_MOST,父 View 有一个最大可用空间,子 View 最大可用空间也是这个空间,因此测量模式是 AT_MOST。MeasureSpec.makeMeasureSpec(父 View 可用空间, MeasureSpec.EXACTLY);
- UNSPECIFIED 同 match_parent 的 UNSPECIFIED。