自定义控件中,measure的流程
继承 View 的子类
一般来说继承 View 的子类需要重写 onMeasure()
,会在 measure()
中被调用,而 measure()
是被 final 修饰的,也就表明它不希望被重写,所以只要重写 onMeasure()
完成测量即可。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
onMeasure()
只有一行代码,进入 getDefaultSize()
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
可以发现,这里面的 AT_MOST 和 EXACTLY 两种模式,得到的 size 都是一样。也就是说,不论 View 设置 wrap_content 还是 match_parent,getDefaultSize()
都会返回父容器剩余的空间。所以,在自定义 View 的时候,如果不重写 onMeasure()
,设置宽高为 wrap_content 或 match_parent 时,展示是没有任何区别的。
下面我们先看下熟悉的 TextView 的 onMeasure()
的源码。
TextView 源码分析
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width;
int height;
...省略代码...
if (widthMode == MeasureSpec.EXACTLY) {
// Parent has told us how big to be. So be it.
width = widthSize;// 当前view的尺寸就为父容器的尺寸
} else {
...省略代码...
if (widthMode == MeasureSpec.AT_MOST) {
width = Math.min(widthSize, width);// 当前view的尺寸就为内容尺寸和父容器尺寸当中的最小值
}
}
...省略代码...
if (heightMode == MeasureSpec.EXACTLY) {
// Parent has told us how big to be. So be it.
height = heightSize;// 当前view的尺寸就为父容器的尺寸
mDesiredHeightAtMeasure = -1;
} else {
...省略代码...
if (heightMode == MeasureSpec.AT_MOST) {
height = Math.min(desired, heightSize);// 当前view的尺寸就为内容尺寸和父容器尺寸当中的最小值
}
}
...省略代码...
setMeasuredDimension(width, height);// 调用View的方法
}
最后调用 View.java 的 setMeasuredDimension()
保存 measuredWidth 和 measuredHeight。
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int opticalWidth = insets.left + insets.right;
int opticalHeight = insets.top + insets.bottom;
measuredWidth += optical ? opticalWidth : -opticalWidth;
measuredHeight += optical ? opticalHeight : -opticalHeight;
}
// 保存宽高,注意是measuredWidthm不是width
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
套路总结
View 的 measure 的流程就是 measure
-> onMeasure
-> setMeasuredDimension
-> setMeasuredDimensionRaw
。
在自定义 View 只需要重写 onMeasure()
测量自己的宽高,最终调用 setMeasuredDimension()
保存自己的测量宽高。
伪代码:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int mode = MeasureSpec.getMode(measureSpec);
int size = MeasureSpec.getSize(measureSpec);
int viewSize = 0;
switch (mode) {
case MeasureSpec.EXACTLY:
viewSize = size;//当前view的尺寸就为父容器的尺寸
break;
case MeasureSpec.AT_MOST:
viewSize = Math.min(size, getContentSize());//当前view的尺寸就为内容尺寸和费容器尺寸当中的最小值。
break;
case MeasureSpec.UNSPECIFIED:
viewSize = getContentSize();//内容有多大,久设置多大尺寸。
break;
default:
break;
}
setMeasuredDimension(viewSize);
}
继承 ViewGroup 的子类
和 View 一样,只需要重写 onMeasure()
即可,但是里面涉及到 child 的测量,还是比较复杂的。
FrameLayout 源码分析
这里目的是总结归纳,所以简化并修改了一些代码,源码要比下面复杂的多。
FrameLayout 中的 onMeasure()
方法。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
...省略代码...
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);// ①
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
maxWidth = Math.max(maxWidth,
child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
maxHeight = Math.max(maxHeight,
child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
}
...省略代码...
// ⑤ 保存 FrameLayout 的 measuredWidth 和 measuredHeight
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec,
childState << MEASURED_HEIGHT_STATE_SHIFT));
...省略代码...
}
ViewGroup 的 measureChildWithMargins()
方法。
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
// ② 为每一个 child 计算 MeasureSpec
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
// ③ 对child 完成测量
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
View 的 resolveSizeAndState()
方法。
// ④ 计算 FrameLayout 的 measuredWidth 和 measuredHeight
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
final int specMode = MeasureSpec.getMode(measureSpec);
final int specSize = MeasureSpec.getSize(measureSpec);
final int result;
switch (specMode) {
case MeasureSpec.AT_MOST:
if (specSize < size) {
result = specSize | MEASURED_STATE_TOO_SMALL;
} else {
result = size;
}
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
case MeasureSpec.UNSPECIFIED:
default:
result = size;
}
return result | (childMeasuredState & MEASURED_STATE_MASK);
}
上面主要的流程是
- ②处 为每一个 child 计算 MeasureSpec。
- ③处 对 child 完成测量。
- ④处 计算 FrameLayout 的 measuredWidth 和 measuredHeight。
- ⑤处 保存 FrameLayout 的 measuredWidth 和 measuredHeight。
LinearLayout 源码分析
这里目的是总结归纳,所以简化并修改了一些代码,源码要比下面复杂的多。
LinearLayout 有 HORIZONTAL 和 VERTICAL 两种样式,这里就用纵向举例 measureVertical()
方法。
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
int maxWidth = 0;
int childState = 0;
final int count = getVirtualChildCount();
...省略代码...
// See how tall everyone is. Also remember max width.
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
...省略代码...
measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
heightMeasureSpec, usedHeight);// ①
...省略代码...
final int measuredWidth = child.getMeasuredWidth() + margin;
maxWidth = Math.max(maxWidth, measuredWidth);
}
maxWidth += mPaddingLeft + mPaddingRight;
// Check against our minimum width
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
heightSizeAndState);// ⑥ 保存 LinearLayout 的 measuredWidth 和 measuredHeight。
}
LinearLayout 的 measureChildBeforeLayout()
方法。
void measureChildBeforeLayout(View child, int childIndex,
int widthMeasureSpec, int totalWidth, int heightMeasureSpec,
int totalHeight) {
measureChildWithMargins(child, widthMeasureSpec, totalWidth,
heightMeasureSpec, totalHeight);// ②
}
ViewGroup 的 measureChildWithMargins()
方法。
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);// ③ 为每一个 child 计算 MeasureSpec
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);// ③ 为每一个 child 计算 MeasureSpec
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);// ④ 对child 完成测量
}
View 的 resolveSizeAndState()
方法。
// ⑤ 计算 LinearLayout 的 measuredWidth 和 measuredHeight
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
final int specMode = MeasureSpec.getMode(measureSpec);
final int specSize = MeasureSpec.getSize(measureSpec);
final int result;
switch (specMode) {
case MeasureSpec.AT_MOST:
if (specSize < size) {
result = specSize | MEASURED_STATE_TOO_SMALL;
} else {
result = size;
}
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
case MeasureSpec.UNSPECIFIED:
default:
result = size;
}
return result | (childMeasuredState & MEASURED_STATE_MASK);
}
上面的流程是
- ③处 为每一个 child 计算 MeasureSpec。
- ④处 对 child 完成测量。
- ⑤处 计算 LinearLayout 的 measuredWidth 和 measuredHeight。
- ⑥处 保存 LinearLayout 的 measuredWidth 和 measuredHeight。
套路总结
ViewGroup 的 measure 的流程就是 measure
-> onMeasure
(测量子控件的宽高) -> setMeasuredDimension
-> setMeasuredDimensionRaw
(保存自己宽高)。
通过 FrameLayout 和 LinearLayout 不难看出 自定义 ViewGroup 的 measure 的流程。主要是两点:
- 测量所有子控件的尺寸。
- 设置自己的尺寸。
伪代码:
// 为每一个child计算测量规格信息(MeasureSpec)
getChildMeasureSpec();
// 将上面测量后的结果,传给每一个子View,子view测量自己的尺寸
child.measure();
// 子View测量完,ViewGroup就可以拿到这个子View的测量后的尺寸了
child.getChildMeasuredSize();//child.getMeasuredWidth() 和 child.getMeasuredHeight()
// ViewGroup自己就可以根据自身的情况(Padding等等),来计算自己的尺寸
ViewGroup.calculateSelfSize();
// 保存ViewGroup自己的尺寸
setMeasuredDimension(size);
自定义 ViewGroup 的实现
最后写个 demo,按照上面总结的套路,自定义 ViewGroup ,实现下面的效果。
自定义 ViewGroup
@UiThread
public class MyViewGroup extends ViewGroup {
private static final int OFFSET = 80; // 每个child横向偏移量
public MyViewGroup(Context context) {
this(context, null);
}
public MyViewGroup(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width = 0;
int height = 0;
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
LayoutParams lp = child.getLayoutParams();
// 为每一个child计算测量规格信息(MeasureSpec)
int childWidthSpec = getChildMeasureSpec(widthMeasureSpec, 0, lp.width);
int childHeightSpec = getChildMeasureSpec(heightMeasureSpec, 0, lp.height);
// 将上面测量后的结果,传给每一个子View,子view测量自己的尺寸
child.measure(childWidthSpec, childHeightSpec);
}
// ViewGroup自己就可以根据自身的情况(Padding等等),来计算自己的尺寸
switch (widthMode) {
case MeasureSpec.EXACTLY:
width = widthSize;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.UNSPECIFIED:
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
// 子View测量完,ViewGroup就可以拿到这个子View的测量后的尺寸了
int widthAndOffset = i * OFFSET + child.getMeasuredWidth();
width = Math.max(width, widthAndOffset);
}
break;
default:
break;
}
switch (heightMode) {
case MeasureSpec.EXACTLY:
height = heightSize;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.UNSPECIFIED:
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
// 子View测量完,ViewGroup就可以拿到这个子View的测量后的尺寸了
height = height + child.getMeasuredHeight();
}
break;
default:
break;
}
// 保存ViewGroup自己的尺寸
setMeasuredDimension(width, height);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// 摆放
int left = 0;
int top = 0;
int right = 0;
int bottom = 0;
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
left = i * OFFSET;
right = left + child.getMeasuredWidth();
bottom = top + child.getMeasuredHeight();
child.layout(left, top, right, bottom);
top += child.getMeasuredHeight();
}
}
}
布局文件
<?xml version="1.0" encoding="utf-8"?>
<com.ff.ui.MyViewGroup xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/darker_gray">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="我是文本aaaa" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="我是文本bbbb" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="我是文本cccc" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="我是文本ddddd" />
</com.ff.ui.MyViewGroup>