我们在使用自定义控件的时候,有时候会发现当我们设置子View的属性为wrap_content时,发现它最终展现的效果跟我们说预想的不一样,它展现的是match_parent的效果,这是为什么呢?先把问题抛出来,接下来就来简要讲解一下。
问题就出在我们自定义View时的绘制视图阶段,即onMeasure()设置View宽/高这一步。
我们在自定义View的onMeasure方法中,是这样写的:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//widthMeasureSpec:View宽的测量规格
//heightMeasureSpec:View高的测量规格
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
跟进getDefaultSize
/**
* @param size View的默认大小
* @param measureSpec 父容器限制的大小
* @return 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;
}
我们可以看到,当View的测量模式为AT_MOST和EXACTLY时,View的大小都会被设置成子View MeasureSpec的specSize。而AT_MOST对应的是wrap_content,EXACTLY对应的是match_parent。所以不管是设置wrap_content还是match_parent,效果都是match_parent。
划重点!!由于在getDefaultSize( )的默认实现中,当View被设置成wrap_content和match_parent时,View的大小都会被设置成子View MeasureSpec的specSize。所以子View的MeasureSpec的specSize就是关键了。先把看看这个specSize是怎么生成的。
它依赖于父容器的getChildMeasureSpec( )方法:
//子view的确切大小由两方面共同决定:父view的MeasureSpec 和 子view的LayoutParams属性
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);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == 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 == 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;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
!!!!!!!
呃……代码真的有点复杂,不过我们可以看看下面这个表格。该表格其实就是代码以表格的形式呈现的效果,是不是很简单明了!?
对于普通View,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams来共同决定,那么针对不同的父容器和View本身不同的LayoutParams,View就可以有多种MeasureSpec。
这里简单说一下:
- 当View采用固定宽/高的时候,不管父容器的MeasureSpec是什么,View的MeasureSpec都是精确模式(EXACTLY)并且大小遵循LayoutParams中的大小。
- 当View的宽/高是match_parent时,如果父容器的模式是精确模式,那么View也是精确模式并且其大小是父容器的剩余空间;如果父容器是最大模式(AT_MOST),那么View也是最大模式并且大小不会超过父容器的剩余空间。
- 当View的宽/高是wrap_content时,不管父容器的模式是精准还是最大化,View的模式总是最大化并且大小不能超过父容器的剩余空间。可能你会发现,在我们分析中漏掉了UNSPECIFIED模式,那是因为这个模式主要用于系统内部多次Measure的情形,一般来说,我们不需要关注此模式。
通过上表就可以看出,只要提供父容器的MeasureSpec和子元素的LayoutParams,就可以快速地确定子元素的MeasureSpec了,有了MeasureSpec就可以进一步确定出子元素测量后的大小了。需要说明的是上表不是说明公式或者经验总结,它只是getChildMeasureSpec这个方法以表格的方式呈现出来而已。、
总结一下:
ViewGroup在计算子View MeasureSpec的getChildMeasureSpec( )中,子View MeasureSpec在属性被设置为wrap_content或match_parent情况下,子View MeasureSpec的specSize被设置成parenSize ,即父容器当前剩余空间大小。所以,wrap_content起到了和match_parent相同的作用:等于父容器当前剩余空间大小
怎么解决?这就很简单了,你只要在自定义View的onMeasure( )方法中给出View的默认大小(宽 / 高)就可以了。
网上流传着这么一个解决方案:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 获取宽-测量规则的模式和大小
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
// 获取高-测量规则的模式和大小
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
// 设置wrap_content的默认宽 / 高值
// 默认宽/高的设定并无固定依据,根据需要灵活设置
// 类似TextView,ImageView等针对wrap_content均在onMeasure()对设置默认宽 / 高值有特殊处理,具体读者可以自行查看
int mWidth = 400;
int mHeight = 400;
// 当布局参数设置为wrap_content时,设置默认值
if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT && getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {
setMeasuredDimension(mWidth, mHeight);
// 宽 / 高任意一个布局参数为= wrap_content时,都设置默认值
} else if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT) {
setMeasuredDimension(mWidth, heightSize);
} else if (getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {
setMeasuredDimension(widthSize, mHeight);
}
}
这样,有了默认值,View的wrap_content就不会失效啦~