上次对自定义ViewGroup中的onLayout()方法进行了基础巩固【http://www.cnblogs.com/webor2006/p/7507284.html】,对于熟知自定义ViewGroup的人应该都知道还有另外一个非常重要的方法在自定义ViewGroup中是不可获缺的,那就是onMeasure(),所以这次好好学习下它,为了更激情的学习,它的学习会融入到一个实际案列中,先贴一下该案例的最终效果:
基础框架搭建:
上面是纯界面布局显示,木有一些触摸事件,不过之后会加入触摸事件滴,一步步来,首先先搭建基础框架:
布局文件:
其中circle_bg3是一张背景图:
【原图地址】:http://images2017.cnblogs.com/blog/324374/201710/324374-20171010163936137-864691392.png
子视图的简单摆放:
对于效果图中的菜单子视图是动态进行添加,动态进行布局的,所以说先要准备一些菜单元素的元数据,如下:
其中布局文件circle_item内容为:
上面不多解释,比较简单,这时运行看下:
可见添加的子视图木有按我们的预期显示,这也是我们需要对它进行处理滴,那如何去摆放我们添加的子视图呢?下面画一个图来分析一下:
对于一个子控件的摆放,是由它的layout方法来决定的,如下:
而上面四个参数中只要确定了left和top,right=(left+子视图宽度)和bottom=(top+子视图宽度)也就确定了,所以关键在于求解left和top,继续看图:
那如何计算呢,这里先以计算left为例,继续对图进行细化:
但是!!a是多少?b是多少?这才是问题的核心,所以接下来需要解决这两值的求解:
①、"a"的值其实就是圆的半径,比如称为R,如下:
②、"b"的值要稍微麻烦一下,下面来挼一下:
而c = (子视图宽 / 2),已知的,那现在的重点就是如何算d的值了,如何算呢?那这时就得用到三角函数啦,这个之前在画饼图时已经复习过,就不多解释,继续看图细化:
所以同理,top的值的计算为:top = R + ((temp * sin α) - (子视图 / 2))
基于上面的原理,下面来用代码具体摆放下子视图:
public class CircleMenu extends ViewGroup { /* 代表是子视图所在圆的直径 */ private int d = 480; private int startAngle; public CircleMenu(Context context) { this(context, null); } public CircleMenu(Context context, AttributeSet attrs) { this(context, attrs, 0); } public CircleMenu(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { } public void setData(int[] resIds, String[] texts) { for (int i = 0; i < resIds.length; i++) { View view = View.inflate(getContext(), R.layout.circle_item, null); ImageView image_icon = (ImageView) view.findViewById(R.id.image_icon); image_icon.setImageResource(resIds[i]); TextView tv_label = (TextView) view.findViewById(R.id.tv_label); tv_label.setText(texts[i]); addView(view); } } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); //temp:相当于自定义控件所在圆的圆心到子视图所在矩形的几何中心的距离 float temp = d / 3.0f; int childWidth = child.getMeasuredWidth(); int left = (int) (d / 2 + Math.round(temp * Math.cos(Math.toRadians(startAngle))) - childWidth / 2); int right = left + childWidth; int top = (int) (d / 2 + Math.round(temp * Math.sin(Math.toRadians(startAngle))) - childWidth / 2); int bottom = top + childWidth; child.layout(left, top, right, bottom); startAngle += 360 / getChildCount();//每次角度进行累加 } } }
上面代码完全遵照其原理图来写的,不多解释,只是说明一点:代码中的temp为三分一直径的长度,这个是可以人为去定义的,长度不一样其子视图所在外切矩形也不一样。编译运行:
呃~~为啥元素还是木有显示出来呢,不是已经在onLayout方法中对其子视图进行处理了么?这里自定义ViewGroup都熟知的onMeasure()方法就派上它的用场啦,因为对于自定义ViewGroup,系统默认只处理当前控件的测量【也就是当前自定义的ViewGroup】,不会对子视图进行测量,所以子视图不知道它的宽高信息那摆放的大小就没法确定,所以只重写onLayout还是无济于事,需要重写onMeasure对其子视图进行手动测量才行,于是乎重写如下:
这时再运行看效果:
嗯~~效果还不错!但是对于onMeasure的过程还是一知半解,而且代码中标红处也不知道为啥要这样写,所以接下来花点时间来剖析它!
分析onMeasure():【重要!!】
onMeasure()方法用于测量控件,如果控件中还有子控件则会递归测量子控件【注:这指的是已有的控件如:LinearLayout、RetativeLayout,而非是继续ViewGroup的自定义控件】,如果是ViewGroup,默认只会测量自身,而不会测量子控件,这个已经在我们的实现中进行了体现。所以在自定义控件中它发挥着很重要的作用,接下来彻底剖析它,在onMeasure()中MeasureSpec是一个非常重要的东东,那它到底是个什么东东呢?下面先给onMeasure()方法将其参数打印进行分析:
运行看输出:
"1073742304",这个数看不出啥明堂呀,继续!将这个数转换成二进制再来看:
编译运行:
总共是31位数字,而一个int是占32的,其中前面的0被省略掉了,这里为了学习添上这个0,于是乎整个结果为:
01 000000000000000000000111100000
那光有这数字也看不出啥明堂出来呀,接下来需要清楚,MeasureSpec是由两部分组成:前两位mode + 后30位size大小。
而其中的mode有三种:
- 未指定:UNSPECIFIED,它的值是0<<30;
- 确切的:EXACTLY,它的值是1<<30;
- 至多:AT_MOST,它的值是2<<30;
其实对应MeasureSpec源码:
而后30位size大小并非是我们传多大就多大:
它是由父布局和子控件共同决定的,为什么这么说呢?比如现在屏幕宽度是480dp,而我们给自定义控件指定1000dp,那最终的大小肯定得综合父布局来决定大小,而并非子控件的宽度就是1000dp;另外如果自定义控件的宽高指定match_parent呢?很显然得看父布局显示多大,则子控件才多大,进一步说明这个size是需要结合父布局来定大小滴。
上面提到的Mode有三种情况,那什么情况下对应什么模式呢,下面给出理论:
- 未指定:UNSPECIFIED,它的值是0<<30【不常用】
未限定实际高度,这种情况极少,实际自定义中很少用到,例如:ScrollView对于其子视图的高度的限定,屏幕只有480高,而子视图定义600高,不影响,因为是可以上下滑动的。 - 确切的:EXACTLY,它的值是1<<30;【常用】
明确的尺寸,例如:xxdp、MATCH_PARENT - 至多:AT_MOST,它的值是2<<30;【常用】
至多为多少?例如:WRAP_CONTENT,若控件本身没有默认尺寸,则系统尽可能的把空间赋予控件,为MATCH_PARENT,也就是说我们定义的WRAP_CONTENT就是至多模式。
口说无凭,下面实际验证一下,只验证常用的EXACTLY和AT_MOST,在正式实验之前,需要介绍一个API,用来获得widthMeasureSpec的mode,如下:
先来查看一下这个获取mode的具体实现,其实很简单:
等于就是与操作取出头二位的值,也就是取出了mode了,比较容易理解不多解释。下面打印一下当前值看模式是多少:
而目前我们就是指定了宽高值:
那我们如果指定wrap_content呢?
对于MeasureSpec知道上面的mode和size之后,对于我们自定义控件有啥意义呢?当然意义重大,我们就可以根据用户在布局中定义的宽高来根据我们的实际自定义的业务去写不同的代码。那具体如何去测量呢?分为自身的测量和子视图的测量,下面一一说明:
外部测量处理【ViewGroup自身】:
有了MeasureSpec的认知,那到底如何去测量呢,我们知道自定义ViewGroup中系统默认只会处理自身的测量,而这个自身的测量是在哪做的呢?
这就是它的自身测量处理,点击进去看一下它的源代码实现:
但是,这个默认的自身测量并不一定能满足我们的需求,这里我们的需求是:不管用户在布局文件中填多大的宽高值,都要完全显示当前自定义控件【也就是正常显示,而不能变形】,那先不说系统的自身测量是否有问题,我们先来将宽高调大,看效果:
编译运行:
出问题了吧,也就说明目前系统默认的自身测量满足不了我们的需求,接着我们来尝试自己来写ViewGroup自身的测量代码,将系统的注释掉:
由于宽高是一样的,所以只要处理其中一个,其它的就有了,下面开始:
首先判断模式:因为用户可能在XML中输入具体值,也有可能是wrap_content,所以需要分条件去处理:
这里先处理else的情况,因为简单一些,其中需要获得屏幕宽高的最小值,所以这里写一个方法来获得该值:
接着来获得用户输入的宽高值,由于宽高值是一样的,所以只处理其中一个既可,这里处理宽:
先来看一下它的源码实现,其实比较容易理解,就是与非嘛:
取得了用户输入的大小之后,接下来就可以处理宽高啦,如下:
好了,接着处理if条件,也就是用户木有输入明确值滴,这个就稍复杂些啦,下面来处理下:
【知识点】:获取控件的背景宽度可以用getSuggestedMinimumWidth()的API去获得。
那有背景与没有背景该如何处理呢?1、如果用户没指定大小,那如何显示呢?这里需求是:背景有多大控件就有多大;2、如果木有背景,则用默认宽度作为控件宽度。
所以处理代码如下:
好了,整个ViewGroup自身的测量按我们的需求已经处理了,接下来我们来测试下在宽高都传1000dp的时候的运行效果,为了方便看效果这里将测量的宽高打印一下:
下面继续测试,如果传的是wrap_content呢?
运行:
ViewGroup是正常的,但是子视图乱了,这个先不用管,随后就会对它进行处理,这里只观注ViewGroup自身的处理既可。
再来测试:传个比较小的宽高值:
运行:
子视图测量处理:
在上一步骤中当用户传入wrap_content的属性时,其运行效果就变成这样了:
接着解决它,先分析下原因,为什么会这样呢?
解决办法就是在我们测量好ViewGroup的宽高时,再将d重新赋值就可以了,让d可以动态根据外部环境而变化,如下:
运行看效果:
嗯!!完美!!!是真的完美么?下面将子视图加一个背景,如下:
运行:
呃~~什么鬼!!全挤一块了,又是什么原因呢?这里就需要解释这句代码的含义啦:
解释:默认系统是可以通过onMeasure给予MeasureSpec参数的【从两个参数就可以看到】,而对于inflate进来的子视图是没有MeasureSpec参数的,而子视图都是通过inflate过来的,所以:需要我们自己设计MeasureSpec参数,所以"MeasureSpec.makeMeasureSpec"就是生成MeasureSpec,先看一下它的原码实现:
而目前的大小被写死成了200了:
所以,不管布局声明多大,其大小都是200dp,不信可以试验一下,改下布局,将其大小写很小:
运行:
显示还是一样,这样肯定是不对的,因为子视图的大小会随着ViewGroup的大小变化而变化,所以这里改下代码:
再运行:
如果将ViewGroup调大呢,其子视图是否也会随着变化?
运行:
至此就真正的完美啦!!!最后还看一个细节:
走进它的源码,直接看关键代码:
懂的否?实际上就是转由系统去进行测量了,最终还是调用setMeasuredDimension()方法由系统处理。