推荐两篇博客:
实现特定的测量方式,比如实现一个时钟的自定义View要求宽高比例1:1、像TextView一样根据内容自适应。
onMeasure(int, int):测量View及其内容以确定测量宽度和测量高度。当重写此方法时,必须调用setMeasuredDimension(int, int)来存储该View的测量宽度和测量高度,否则将触发一个IllegalStateException,或者调用超类的onMeasure(int,int)(超类做了默认的测量方法)。
如果重写了该方法,子类有责任确保测量的宽度和高度至少是View的最小宽度和高度(getSuggestedMinimumWidth()和getSuggestedMinimumHeight())。
参数:
这两个参数都是由MeasureSpec类打包而成,里面包含测量模式和参考尺寸。widthMeasureSpec和heightMeasureSpec是由父布局确定的(除了DecorView )。
一个测量规格封装了从父布局传递给子View的布局要求。每个测量规格代表对宽度和高度的要求,由大小和模式组成。有三种可能的模式:
MeasureSpec的实现是为了减少对象分配,这个类提供了打包和解包的方法:
注:源码是23版本的
View的onMeasure(int,int)代码
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
getDefaultSize(int,int)代码:返回默认大小(参考尺寸),如果测量规格没有施加约束(UNSPECIFIED模式),则使用所提供的尺寸(子View建议的最小尺寸)。
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;
}
getSuggestedMinimumWidth():返回建议的最小宽度
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
getSuggestedMinimumHeight():返回建议的最小高度
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
getSuggestedMinimumWidth(),这个方法是从mBackground.getMinimumWidth()和mMinWidth中取最大的值返回,
mBackground是setBackgroundDrawable(Drawable background)设置的背景,
mMinWidth是setMinimumWidth(int minWidth)设置的最小宽度。
getSuggestedMinimumHeight()类似。
measureChildren(int, int) 代码:遍历所有子View,并调用measureChild(View, int, int)测量单个子View
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
measureChild(View, int, int)代码:调用getChildMeasureSpec(int spec, int padding, int childDimension)确定子View的widthMeasureSpec、heightMeasureSpec,并调用子View的measure(int,int)进行测量。
protected void measureChild(View child, int parentWidthMeasureSpec,int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
getChildMeasureSpec(int, int, int)代码:ViewGroup提供确定子View的widthMeasureSpec、heightMeasureSpec的方法(根据父布局对当前ViewGroup的约束、当前ViewGroup已用空间和子View的LayoutParams),但布局不一定或不单单调用这个方法来确定子View的widthMeasureSpec、heightMeasureSpec。
/*
* @param spec 父布局的measureSpec
* @param padding 当前布局已用空间(当前布局的padding、子View的margin)
* @param childDimension 子View想要的大小
* @return 子View的measureSpec
*/
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);//当前ViewGroup可用于布局的空间
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// 父布局的测量模式是EXACTLY
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {// 子View提供了明确的尺寸
// 子View得到明确的尺寸
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {// 子View想铺满父布局
// 子View得到父布局的尺寸
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {// 子View想要可以包含内容的尺寸
// 子View想自己决定自己的尺寸,这个尺寸不能比父布局的尺寸大
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 父布局的测量模式是AT_MOST
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {// 子View提供了明确的尺寸
// 子View得到明确的尺寸
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {// 子View想铺满父布局
// 子View想得到父布局的尺寸,但父布局的尺寸不是固定的
// 强迫子View不能大过父布局
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {// 子View想要可以包含内容的尺寸
// 子View想自己决定自己的尺寸,这个尺寸不能比父布局的尺寸大
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 父布局的测量模式是UNSPECIFIED
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {// 子View提供了明确的尺寸
// 让子View得到明确的尺寸
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// 子View想得到父布局的尺寸,查找子View想要多大
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {// 子View想要可以包含内容的尺寸
// 子View想自己决定自己的尺寸,查找子View想要多大
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
View.sUseZeroUnspecifiedMeasureSpec:boolean类型,判断当measureSpec的模式是UNSPECIFIED时,是否总是返回一个大小为0的值。在View的构造函数进行赋值,当targetSdkVersion <23时为true:
public View(Context context) {
...
sUseZeroUnspecifiedMeasureSpec = targetSdkVersion < M;
...
}
}
下面这张图片是getChildMeasureSpec(int spec, int padding, int childDimension)的表格形式,来自Android View系统解析(下) - 任玉刚 - CSDN博客。
还有个跟measureChild(View, int, int)类似的方法void measureChildWithMargins(View,int,int,int,int)
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);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin + heightUsed,//
lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
这个方法和measureChild(View, int, int)
的区别
1、多接收两个参数widthUsed和heightUsed
2、调用getChildMeasureSpec确定子View measureSpec时,
确定宽度约束时:在传的padding参数中多加上widthUsed和子View的leftMargin、rightMargin,
确定宽度约束时:在传的padding参数中多加上heightUsed和子View的topMargin、bottomMargin