自定义View wrap_content不起作用
设置wrap_content后,自定义View依然是match_parent的效果
ref: Android 自定义View:为什么你设置的wrap_content不起作用? - 简书 (jianshu.com)
问题描述:
最近实现了一个 QQ消息气泡 的功能,但是在测试的时候发现尽管自定义View设置了wrap_content的宽高,但是依然占据了所有的父容器空间,即和 match_parent 的效果相同。
问题分析
既然宽高异常,那么大概率是出现在 measure 过程的,我的自定义 view 没有重写 onMeasure 方法,估计问题就出现在这。可是为什么不重写 onMeasure 方法,wrap_content 和 match_parent 的效果相同呢,要看一下源码:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//参数说明:View的宽 / 高测量规格
//setMeasuredDimension() 用于获得View宽/高的测量值
//这两个参数是通过getDefaultSize()获得的
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
往下看 getDefaultSize():
public static int getDefaultSize(int size, int measureSpec) {
//参数说明:
// 第一个参数size:提供的默认大小
// 第二个参数:宽/高的测量规格(含模式 & 测量大小)
//设置默认大小
int result = size;
//获取宽/高测量规格的模式 & 测量大小
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
// 模式为UNSPECIFIED时,使用提供的默认大小
// 即第一个参数:size
case MeasureSpec.UNSPECIFIED:
result = size;
break;
// 模式为AT_MOST,EXACTLY时,使用View测量后的宽/高值
// 即measureSpec中的specSize
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
//返回View的宽/高值
return result;
}
这里可以知道,默认情况下,当模式是 AT_MOST 和 EXACTLY 时,view 大小会被设置为 measureSpec 的大小,所以,对应的,match_parent 和 wrap_content 默认情况下的实际效果是一样的。
然后,进一步思考,为什么 measureSpec 是父容器的大小呢,这就需要看父容器怎么得到的 measureSpec。父容器通过 getChildMeasureSpec 方法得到它的大小:
//作用:
/ 根据父视图的MeasureSpec & 布局参数LayoutParams,计算单个子View的MeasureSpec
//即子view的确切大小由两方面共同决定:父view的MeasureSpec 和 子view的LayoutParams属性
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//参数说明
* @param spec 父view的详细测量值(MeasureSpec)
* @param padding view当前尺寸的的内边距和外边距(padding,margin)
* @param childDimension 子视图的布局参数(宽/高)
//父view的测量模式
int specMode = MeasureSpec.getMode(spec);
//父view的大小
int specSize = MeasureSpec.getSize(spec);
//通过父view计算出的子view = 父大小-边距(父要求的大小,但子view不一定用这个值)
int size = Math.max(0, specSize - padding);
//子view想要的实际大小和模式(需要计算)
int resultSize = 0;
int resultMode = 0;
//通过父view的MeasureSpec和子view的LayoutParams确定子view的大小
// 当父view的模式为EXACITY时,父view强加给子view确切的值
//一般是父view设置为match_parent或者固定值的ViewGroup
switch (specMode) {
case MeasureSpec.EXACTLY:
// 当子view的LayoutParams>0,即有确切的值
if (childDimension >= 0) {
//子view大小为子自身所赋的值,模式大小为EXACTLY
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
// 当子view的LayoutParams为MATCH_PARENT时(-1)
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//子view大小为父view大小,模式为EXACTLY
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
// 当子view的LayoutParams为WRAP_CONTENT时(-2)
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//子view决定自己的大小,但最大不能超过父view,模式为AT_MOST
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 当父view的模式为AT_MOST时,父view强加给子view一个最大的值。(一般是父view设置为wrap_content)
case MeasureSpec.AT_MOST:
// 道理同上
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 当父view的模式为UNSPECIFIED时,父容器不对view有任何限制,要多大给多大
// 多见于ListView、GridView
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// 子view大小为子自身所赋的值
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// 因为父view为UNSPECIFIED,所以MATCH_PARENT的话子类大小为0
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// 因为父view为UNSPECIFIED,所以WRAP_CONTENT的话子类大小为0
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
各种情况的子View measureSpec 总结如下表:
子 \ 父 | EXACTLY | AT_MOST | UNSPECIFIED |
---|---|---|---|
具体数值 | EXACTLY + childSize | EXACTLY + childSize | EXACTLY + childSize |
match_parent | EXACTLY + parentSize | AT_MOST + parentSize | UNSPECIFIED + 0 |
wrap_content | AT_MOST + parentSize | AT_MOST + parentSize | UNSPECIFIED + 0 |
总结出一句话就是,子View有具体数值,则子View确定了是EXACTLY+childSize;子View是match_parent,则跟随父View;子View是wrap_content,则最高时是AT_MOST,跟随parentSize。
问题总结
如果没有重写 onMeasure,则使用默认的onMeasure方法调用getDefaultSize去计算子View大小。
因为父View是个 match_parent 的 LinearLayout,所以match_parent的子View measureSpec 被设置为了 EXACTLY + parentSize,wrap_content的子View measureSpec 被设置为了 AT_MOST + parentSize。
又因为在getDefaultSize中 不管是 EXACTLY 还是 AT_MOST,都会将size设置为 measureSpec 中的size(parentSize)。
所以,子View不管是 match_parent 还是 wrap_content,都会被设置为 父容器的大小。
解决方案
重写 onMeasure 方法:
@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 = (int) mBubbleRadius * 2;
int mHeight = (int) mBubbleRadius * 2;
// 当布局参数设置为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);
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?