PS:如果不能预测未来是什么样的,那么就好好做好当下。
今天来分享一个我在自定义 View 中遇到的问题,如果分析有误,还望各位指出,在自定义 View 的过程中一定会遇到一个问题,自定义 View 没有问题,唯独在自定义的 View 中 match_parent 和 wrap_content 效果一致,onMeasure () 方法如下:
/**
*
* 测量View的宽度和高度,这个方法由 measure方法调用,一般由子类重写该方法以提供更加精确和高效的测量
*
* 规定:当重写该方法的时候。你必须调用setMeasuredDimension(int, int)方法来存放View的测量宽度和高度,
* 测量失败抛出 IllegalStateException
*
* 测量的基类实现默认为背景大小,除非允许更大尺寸的测量规范,子类应该重写该方法以便提供更好地测量
*
* 如果该方法被重写,则子类的责任就是确保测量的高度和宽度至少是View的最小宽度和高度
*
* @param widthMeasureSpec 父容器施加的水平方向测量规范.
* @param heightMeasureSpec 父容器施加的垂直方向测量规范.
*/
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//存储测量的宽高
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
看一下 getDefaultSize 方法的具体实现:
//根据指定的测量模式获得对应的大小
public static int getDefaultSize(int size, int measureSpec) {
//默认大小,其大小与是否设置背景相关
int result = size;
//从View的测量规范中获得对应的测量模式和测量大小
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
//根据View的测量模式设置当前View的大小
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
//返回View的尺寸大小
return result;
}
显然,默认情况下 View 指定的宽高为 match_parent 和 wrap_content 时,也就是测量模式为 AT_MOST 和 EXACTLY 的时候,最终返回的都是从指定 MeasureSpec 中获得的尺寸大小,所以默认情况下设置 View 的宽高为 match_parent 和 wrap_content 时效果是一样的。
onMeasure 中的宽度和高度的 MeasureSpec 是由父 View MeasureSpec 及自身的布局参数 LayoutParamas 来确定,具体体现在 ViewGroup 中的 getChildMeasureSpec () 方法中,源码如下:
/**
* 测量子View的难点:找出MeasureSpec传递给指定的子View,该方法找出了正确的MeasureSpec用于子View宽度或高度的测量
*
* 该方法的目标是结合MeasureSpec和子View的LayoutParams获得最准确结果
* 如父View的MeasureSpec中指定的测量模式为EXACTLY,子View制定了确切的尺寸大小,则父View会给子View一个确切的大小
*
* @param spec 父View指定的宽或高的MeasureSpec
* @param padding 如果当前View设置了外边距和内边距,表示当前View的内边距和外边距,
* 如 mPaddingLeft + mPaddingRight + mLeftMargin + mRightMargin
* @param childDimension 当前View的宽或高
* @return 子View测量后的对应尺寸大小
*/
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//获得当前测量维度的父View测量模式和测量大小
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
//获得子View真正可以可以使用的尺寸大小
int size = Math.max(0, specSize - padding);
//子View的测量模式和尺寸大小,还需测量完成
int resultSize = 0;
int resultMode = 0;
//根据父View指定的不同的测量模式对子View进行测量
switch (specMode) {
//EXACTLY:父View指定确切的尺寸,如match_parent、100dp
case MeasureSpec.EXACTLY:
//如果子View指定了确切的尺寸大小
if (childDimension >= 0) {
//指定尺寸大小为子View自己设置的尺寸大小
resultSize = childDimension;
//指定子View测量模式为EXACTLY
resultMode = MeasureSpec.EXACTLY;
//如果子View设置了尺寸大小是:MATCH_PARENT
} else if (childDimension == ViewGroup.LayoutParams.MATCH_PARENT) {
//指定子View的尺寸大小为子View可以是使用的最大的尺寸
resultSize = size;
//指定子View测量模式为EXACTLY
resultMode = MeasureSpec.EXACTLY;
//如果子View设置了尺寸大小是:WRAP_CONTENT
} else if (childDimension == ViewGroup.LayoutParams.WRAP_CONTENT) {
// 子View的尺寸大小由自己包裹的内容决定,但是不能超过父View指定的尺寸大小
resultSize = size;
//指定子View测量模式为EXACTLY
resultMode = MeasureSpec.AT_MOST;
}
break;
//AT_MOST:父View允许子View一个能够达到的最大的尺寸大小,如wrap_content
case MeasureSpec.AT_MOST:
//如果子View指定了确切的尺寸大小
if (childDimension >= 0) {
// 指定子View的测量模式和测量大小,此时可以确定子View的测量模式的尺寸大小
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == ViewGroup.LayoutParams.MATCH_PARENT) {
//子View想要获得父View指定的最大尺寸,但是父View的尺寸大小不能确定,子View的大小只能由子View自己包裹的内容决定
//故测量模式指定为 MeasureSpec.AT_MOST
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == ViewGroup.LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// UNSPECIFIED:父View不对子View约束,子View可以设置它想要的大小
//如果父View指定的测量模式是UNSPECIFIED,其子View自身的尺寸大小如果不确切指定,则
// 子View尺寸大小有两种取值范围:0或子View所被允许的最大尺寸
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == ViewGroup.LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == ViewGroup.LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//返回子View对应的MeasureSpec
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
上面只是对 getChildMeasureSpec () 源码的分析,回到问题本身,为什么在自定义 View 中默认情况下为何宽高设置为 match_parent 和 wrap_content 效果一样,下面表格是根据上述代码分析总结如下:
父 View 的 MeasureSpec | EXACTLY | AT_MOST | UNSPECIFIED |
---|---|---|---|
具体尺寸 | childDimension(EXACTLY) | childDimension(EXACTLY) | childDimension(EXACTLY) |
match_parent | size(EXACTLY) | size(AT_MOST) | size(UNSPECIFIED) |
wrap_content | size(AT_MOST) | size(AT_MOST) | size 或 0 (UNSPECIFIED) |
上面表格内容都是基于 getChildMeasureSpec () 方法的内部实现,默认状态下当子 View 的宽高设置为 match_parent 或 wrap_content 时,其子 View 的实际大小是 size,size 表示为子 View 可以真正可以使用的大小,及除去内边距和外边距之外子 View 可以使用的尺寸大小,当我们自定义 View 时计算坐标的时候使用到的就是尺寸大小 size,虽然最终子 View 的 MeasureSpec 可能不同,但是从某个具体的 MeasureSpec 中解析出来的 size 是一样,所以在自定义 View 中默认情况下宽高设置为 match_parent 和 wrap_content 效果一样,到此这个问题就算找到原因了。
那么如何解决这个问题呢,当然就要在获取自定义 View 的宽高之前重新保存合适的宽高的尺寸大小,这个尺寸大小可以由具体的需求设置一个默认的大小,即当设置自定义 View 的 LayoutParams 设置为 wrap_content 时要设置默认的尺寸,比如下面这样处理:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//wrap_content默认宽高
Rect mRect = new Rect();
mLetterPaint.getTextBounds("A", 0, 1, mRect);
int mDefaultWidth = mRect.width() + dpToPx(mContext, 12);
int mDefaultHeight = mRect.height() + dpToPx(mContext, 5);
//重新设置wrap_content时的默认宽高
if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT &&
getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {
//重新保存合适的宽高
setMeasuredDimension(mDefaultWidth, mHeight);
} else if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT) {
setMeasuredDimension(mDefaultWidth, heightSize);
} else if (getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {
setMeasuredDimension(widthSize, mDefaultHeight);
}
//如果当前View的LayoutParams为wrap_content则获取的宽高就是对应的默认宽高
int mWidth = getMeasuredWidth();
int mHeight = getMeasuredHeight();
}
上面是自定义 View 实现一个日期选择器这篇文章中的相关代码,具体可以点击查看,