Android 视图自定义:探究设置wrap_content背后的原理

更新:11-13 现代故事 我要投稿 纠错 投诉

目录

示意图

1. 问题描述

使用自定义View时,View宽/高的wrap_content属性没有自己的功能,和match_parent起到同样的作用。

wrap_content 和match_parent 的区别:

wrap_content:视图的宽度/高度设置为刚好适合视图内容的最小尺寸。 match_parent:视图的宽度/高度设置为填充整个父布局。

(Android API 8之前称为fill_parent)其实这里有两个问题:

问题1:wrap_content属性没有发挥其应有的作用。问题2:wrap_content与match_parent的作用相同。

2. 知识储备

在分析解决问题之前,请先阅读自定义View原理(二)自定义View测量流程-最易理解的自定义View原理系列

3. 问题分析

视图的宽度/高度设置出现问题。我们直接看自定义View绘制中设置View宽高的第一步:测量过程中的onMeasure()方法。

onMeasure()

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

//参数说明:View的宽高测量规格

//setMeasuredDimension()用于获取View宽/高的测量值

//这两个参数是通过getDefaultSize()获取的

setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),

getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));

}继续往下看getDefaultSize()

getDefaultSize()

功能:根据View宽/高的测量规格,计算View的宽/高值。源码分析如下: public static int getDefaultSize(int size, intmeasureSpec) {

//参数说明:

//第一个参数size:提供的默认大小

//第二个参数:宽/高测量规格(包括模式测量尺寸)

//设置默认大小

int 结果=大小;

//获取宽高测量规格的模式测量尺寸

int specMode=MeasureSpec.getMode(measureSpec);

int specSize=MeasureSpec.getSize(measureSpec);

开关(规格模式){

//当模式为UNSPECIFIED 时,使用提供的默认大小

//即第一个参数:size

案例MeasureSpec.UNSPECIFIED:

结果=大小;

休息;

//当模式为AT_MOST,EXACTLY时,使用View测量的宽/高值

//即measureSpec中的specSize

案例MeasureSpec.AT_MOST:

案例MeasureSpec.EXACTLY:

结果=规格大小;

休息;

}

//返回View的宽/高值

返回结果;

从上面发现:

在getDefaultSize() 的默认实现中,当View 的测量模式为AT_MOST 或EXACTLY 时,View 的大小将被设置为子View MeasureSpec 的specSize。因为AT_MOST对应的是wrap_content; EXACTLY 与match_parent 对应,因此默认情况下,wrap_content 和match_parent 具有相同的效果。修复问题2:wrap_content与match_parent作用相同

那么有人可能会问:wrap_content和match_parent效果是一样的,为什么是填充父容器的效果呢?

因为在getDefaultSize()的默认实现中,当View设置为wrap_content和match_parent时,View的大小会被设置为子View MeasureSpec的specSize。因此,这个问题的关键是子View MeasureSpec的specSize的值是多少。我们知道,子View的MeasureSpec值是根据子View的布局参数(LayoutParams)和父容器的MeasureSpec值计算出来的。具体的计算逻辑封装在getChildMeasureSpec()中。

接下来我们看一下生成子View MeasureSpec的方法:getChildMeasureSpec()的源码分析:

getChildMeasureSpec()

//影响:

/根据父视图的MeasureSpec布局参数LayoutParams计算单个子View的MeasureSpec

//即子视图的具体大小由两个方面决定:父视图的MeasureSpec和子视图的LayoutParams属性。

公共静态int getChildMeasureSpec(int 规范, int 填充, int childDimension) {

//参数说明

* @param spec 父视图的详细测量值(MeasureSpec)

* @param padding 视图当前大小的内边距和外边距(padding、margin)

* @param childDimension 子视图的布局参数(宽度/高度)

//父视图的测量模式

int specMode=MeasureSpec.getMode(spec);

//父视图的大小

int specSize=MeasureSpec.getSize(spec);

//通过父视图计算出来的子视图=父尺寸-边距(父视图需要的尺寸,但子视图不一定使用这个值)

int size=Math.max(0, specSize - 填充);

//子view实际需要的尺寸和模式(需要计算)

int 结果大小=0;

int 结果模式=0;

//通过父视图的MeasureSpec和子视图的LayoutParams确定子视图的大小

//当父视图的模式为EXACITY时,父视图将精确值强加于子视图

//一般父视图设置为match_parent或者固定值ViewGroup

开关(规格模式){

案例MeasureSpec.EXACTLY:

//当子视图的LayoutParams0有精确值时

if (子维度=0) {

//子视图的大小是分配给子视图本身的值,模式大小是EXACTLY

结果大小=子维度;

resultMode=MeasureSpec.EXACTLY;

//当子视图的LayoutParams为MATCH_PARENT (-1)时

} else if (childDimension==LayoutParams.MATCH_PARENT) {

//子视图的大小就是父视图的大小,模式为EXACTLY

结果大小=大小;

resultMode=MeasureSpec.EXACTLY;

//当子视图的LayoutParams为WRAP_CONTENT (-2)时

} else if (childDimension==LayoutParams.WRAP_CONTENT) {

//子视图决定自己的大小,但最大不能超过父视图。模式为AT_MOST。

结果大小=大小;

resultMode=MeasureSpec.AT_MOST;

}

休息;

//当父视图的模式为AT_MOST时,父视图对子视图强加一个最大值。 (一般父视图设置为wrap_content)

案例MeasureSpec.AT_MOST:

//原理同上

if (子维度=0) {

结果大小=子维度;

resultMode=MeasureSpec.EXACTLY;

} else if (childDimension==LayoutParams.MATCH_PARENT) {

结果大小=大小;

resultMode=MeasureSpec.AT_MOST;

} else if (childDimension==LayoutParams.WRAP_CONTENT) {

结果大小=大小;

resultMode=MeasureSpec.AT_MOST;

}

休息;

//当父视图的模式为UNSPECIFIED时,父容器对视图没有任何限制。它可以想多大就多大。

//常见于ListView和GridView

案例MeasureSpec.UNSPECIFIED:

if (子维度=0) {

//子视图的大小是分配给子视图本身的值

结果大小=子维度;

resultMode=MeasureSpec.EXACTLY;

} else if (childDimension==LayoutParams.MATCH_PARENT) {

//因为父视图是UNSPECIFIED,所以如果使用MATCH_PARENT,子类的大小为0。

结果大小=0;

resultMode=MeasureSpec.UNSPECIFIED;

} else if (childDimension==LayoutParams.WRAP_CONTENT) {

//因为父视图是UNSPECIFIED,所以WRAP_CONTENT中子类的大小为0

结果大小=0;

resultMode=MeasureSpec.UNSPECIFIED;

}

休息;

}

返回MeasureSpec.makeMeasureSpec(resultSize, resultMode); getChildMeasureSpec()中确定子View的测量模式和尺寸的逻辑有点复杂;别着急,我已经给你总结好了。子View的具体测量方式和尺寸请见下表: Paste_Image.png 从上可以看出,当子View的布局参数使用match_parent或wrap_content时:

子View的SpecMode模式:AT_MOST 子View的SpecSize(宽/高):parenSize=父容器当前剩余空间大小=match_content

4. 问题总结

在onMeasure()中getDefaultSize()的默认实现中,当View的测量模式当AT_MOST或EXACTLY时,View的大小将被设置为子View MeasureSpec的specSize。

因为AT_MOST对应的是wrap_content; EXACTLY 与match_parent 对应,因此默认情况下,wrap_content 和match_parent 具有相同的效果。

因为在计算子View MeasureSpec的getChildMeasureSpec()时,当子View MeasureSpec的属性设置为wrap_content或match_parent时,子View MeasureSpec的specSize设置为parenSize=父容器当前剩余空间大小。

所以:wrap_content与match_parent作用相同:等于父容器当前剩余空间

5. 解决方案

当自定义View的布局参数设置为wrap_content时,指定默认大小(宽度/高度)。

具体来说,它是在重写onMeasure() 中设置的

@覆盖

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等类似,在onMeasure()中对wrap_content设置默认宽/高值有特殊处理。读者可以自行查阅。

int mWidth=400;

int mHeight=400;

//当布局参数设置为wrap_content时,设置默认值

if (getLayoutParams().width==ViewGroup.LayoutParams.WRAP_CONTENT getLayoutParams().height==ViewGroup.LayoutParams.WRAP_CONTENT) {

setMeasuredDimension(mWidth, mHeight);

//当width/height任意布局参数=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属性时就会生效。

特别注意

网上流传着这样的解决方案:

@覆盖

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等类似,在onMeasure()中对wrap_content设置默认宽/高值有特殊处理。读者可以自行查阅。

int mWidth=400;

int mHeight=400;

//设置模式为AT_MOST时的默认值(即wrap_content)

if (widthMode==MeasureSpec.AT_MOST heightMode==MeasureSpec.AT_MOST) {

setMeasuredDimension(mWidth, mHeight);

//当宽度/高度模式为AT_MOST(即wrap_content)时,设置默认值

} else if (widthMode==MeasureSpec.AT_MOST) {

setMeasuredDimension(mWidth, heightSize);

} else if (heightMode==MeasureSpec.AT_MOST) {

setMeasuredDimension(widthSize, mHeight);

}上面的解决方案是通过判断测量模式是否为ATMOST来判断View的参数是否为wrap_content。不过,通过下表我们发现,View的AT_MOST模式不仅对应于wrap_content,还对应于match_parent,即当父View为AT_MOST时,将View属性设置为match_parent时

Paste_Image.png 如果我们还是按照上面的做法,当父View为AT_MOST且View为match_parent时,View的match_parent的效果不就等于wrap_content了吗?答:是的,当父View为AT_MOST且View为match_parent时,View的match_parent的效果等于wrap_content。上述方法存在逻辑错误,但由于这种情况很特殊,所以最终的结果是没有错误的。具体分析请看下面的例子:

?xml version="1.0"encoding="utf-8"?--父View设置为wrap_content,即AT_MOST模式--android:layout_width="match_parent"

android:layout_height="match_parent" /Rendering 从上面的效果可以看出,View size=默认值

然后我将子视图的属性更改为wrap_content:

?xml version="1.0"encoding="utf-8"?--父View设置为wrap_content,即AT_MOST模式--android:layout_width="wrap_content"

android:layout_height="wrap_content" /Rendering 从上面的效果可以看出,View的大小仍然等于默认值。

与上面的分析相同

对于第一种情况:当父View为AT_MOST且View为match_parent时,该View的match_parent的效果等于wrap_content。如上所述,这种情况很特殊:父View的大小能刚好包裹子View,子View的大小充满父View的大小。换句话说:父View的大小是看子View的,子View的大小又是看父View的。那么谁看谁的尺寸呢?

答:如果不设置默认值,则继续填充上层VIew的大小,即父View的大小等于顶层View的大小(),则大小子视图=父视图的大小。如果设置了默认值,则使用默认值。我相信你已经明白这一点:

其实上面提到的解决方案(通过判断测量模式是否为AT_MOST来判断View参数是否为wrap_content)只是说明了一些逻辑错误,但就最终结果而言,并没有错,因为当父View为AT_MOST、View为match_parent时,该View的match_parent的效果就等于wrap_content如果默认的话value没有设置,继续填充上层VIew的大小,即父View的大小等于顶层View的大小(),则子View的大小=子View的大小父视图。如果设置了默认值,则使用默认值。为了更好的表示判断逻辑,我建议你们用本文提供的解决方案,即根据布局参数判断默认值的设置

Android 视图自定义:探究设置wrap_content背后的原理和的问题分享结束啦,以上的文章解决了您的问题吗?欢迎您下次再来哦!

用户评论

Hello爱情风

这篇文章终于来了!我一直想知道怎么让自定义View 完美适配不同分辨率

    有15位网友表示赞同!

遗憾最汹涌

我也是特别想学习Android自定义View,这个"为什么你设定的wrap..." 我感觉很有意思!

    有5位网友表示赞同!

巷陌繁花丶

我之前遇到过这个问题,设置了wrap_content但是view不是完全适应内容啊……

    有12位网友表示赞同!

心安i

期待作者分享一些应对这种情况的技巧!

    有9位网友表示赞同!

颓废i

学习自定义View确实很有学问啊,希望能了解更多相关知识。

    有9位网友表示赞同!

烬陌袅

这个标题抓住问题关键点了,直接切入主题!

    有19位网友表示赞同!

青瓷清茶倾城歌

对Android开发有那么点基础的我,想了解一下 wrap_content 的具体用法!

    有16位网友表示赞同!

余笙南吟

感觉这篇文章能帮助我解决一些自定义View的开发难题!

    有15位网友表示赞同!

麝香味

安卓开发真是太有趣了,自定义View 可以实现很多个性化定制!

    有6位网友表示赞同!

墨染殇雪

分享学习Android自定义View的经验是很有价值的事情哦!

    有10位网友表示赞同!

顶个蘑菇闯天下i

我也曾困扰过这个问题,希望这篇文章能给我一些启发!

    有14位网友表示赞同!

在哪跌倒こ就在哪躺下

如果作者能提供一些代码实例,那更棒了!

    有9位网友表示赞同!

▼遗忘那段似水年华

学习Android开发一直是我的目标,这篇文章非常适合我!

    有15位网友表示赞同!

终究会走-

我希望能从这篇文章中学到很多关于自定义View的知识!

    有13位网友表示赞同!

浮殇年华

期待一下具体的解决方案,让我不再困惑!

    有18位网友表示赞同!

古巷青灯

终于有人讲解这个问题了!我也想知道为什么会出现这种情况。

    有11位网友表示赞同!

暖栀

Android开发需要不断学习新的知识和技巧!

    有12位网友表示赞同!

巷雨优美回忆

这篇文章能帮助我更深入地理解Android自定义View!

    有20位网友表示赞同!

【Android 视图自定义:探究设置wrap_content背后的原理】相关文章:

1.动物故事精选:寓教于乐的儿童故事宝库

2.《寓教于乐:精选动物故事助力儿童成长》

3.探索动物旅行的奇幻冒险:专为儿童打造的童话故事

4.《趣味动物刷牙小故事》

5.探索坚韧之旅:小蜗牛的勇敢冒险

6.传统风味烤小猪,美食探索之旅

7.探索奇幻故事:大熊的精彩篇章

8.狮子与猫咪的奇妙邂逅:一场跨界的友谊故事

9.揭秘情感的力量:如何影响我们的生活与决策

10.跨越两岸:探索彼此的独特世界

上一篇:轻松赚取每日100元:揭秘无广告赚钱软件及游戏攻略 下一篇:【GeekBand】深入解析C++面向对象编程技巧