自定义控件之测量篇:测量与布局
2017-08-26 15:21
162 查看
一、ViewGroup绘制流程
注意,View及ViewGroup基本相同,只是在ViewGroup中不仅要绘制自己还是绘制其中的子控件,而View则只需要绘制自己就可以了,所以我们这里就以ViewGroup为例来讲述整个绘制流程。
绘制流程分为三步:测量、布局、绘制
分别对应:onMeasure()、onLayout()、onDraw()
其中,他们三个的作用分别如下:
onMeasure():测量自己的大小,为正式布局提供建议。(注意,只是建议,至于用不用,要看onLayout);
onLayout():使用layout()函数对所有子控件布局;
onDraw():根据布局的位置绘图;
有关绘图的部分,大家可以参考我的系列博客《android
Graphics(一):概述及基本几何图形绘制》共有四篇,讲述了有关android 绘图的90%内容,大家可以参考。
这篇文章着重将内容放在分析onMeasure()和onLayout()上。
布局绘画涉及两个过程:测量过程和布局过程。测量过程通过measure方法实现,是View树自顶向下的遍历,每个View在循环过程中将尺寸细节往下传递,当测量过程完成之后,所有的View都存储了自己的尺寸。第二个过程则是通过方法layout来实现的,也是自顶向下的。在这个过程中,每个父View负责通过计算好的尺寸放置它的子View。
前面讲过,onMeasure()是用来测量当前控件大小的,给onLayout()提供数值参考,需要特别注意的是:测量完成以后通过setMeasuredDimension(int,int)设置给系统。
首先,看一下onMeasure()的声明:
这里我们主要关注传进来的两个参数:int widthMeasureSpec, int heightMeasureSpec
与这两个参数有关的是两个问题:意义和组成。即他们是怎么来的,表示什么意思;还有,他们是组成方式是怎样的。
我们先说他们的意义:
他们是父类传递过来给当前view的一个建议值,即想把当前view的尺寸设置为宽widthMeasureSpec,高heightMeasureSpec
有关他们的组成,我们就直接转到MeasureSpec部分。
虽然表面上看起来他们是int类型的数字,其实他们是由mode+size两部分组成的。
widthMeasureSpec和heightMeasureSpec转化成二进制数字表示,他们都是32位的。前两位代表mode(测量模式),后面30位才是他们的实际数值(size)。
(1)模式分类
它有三种模式:
①、UNSPECIFIED(未指定),父元素不对自元素施加任何束缚,子元素可以得到任意想要的大小;
②、EXACTLY(完全),父元素决定自元素的确切大小,子元素将被限定在给定的边界里而忽略它本身大小;
③、AT_MOST(至多),子元素至多达到指定大小的值。
其值是:
由于最前面两位代表模式,所以他们分别对应十进制的0,1,2;
(2)模式提取
现在我们知道了widthMeasureSpec和heightMeasureSpec是由模式和数值组成的,而且二进制的前两位代表模式,后28位代表数字。
我们先想想,如果我们自己来提取widthMeasureSpec和heightMeasureSpec中的模式和数值是怎么提取呢?
首先想到的肯定是通过MASK和与运算去掉不需要的部分而得到对应的模式或数值。
说到这大家可能会迷茫,我们写段代码来提取模式部分吧:
相信大家看了代码就应该清楚模式和数值提取的方法了吧,主要用到了MASK的与、非运算,难度不大,如果有问题,自行谷歌一下与、非运算方法吧。
(3)、MeasureSpec
上面我们自已实现了模式和数值的提取。但在强大的andorid面前,肯定有提供提取模式和数值的类。这个类就是MeasureSpec
下面两个函数就可以实现这个功能:
另外MODE的取值为:
通过下面的代码就可以分别获取widthMeasureSpec和heightMeasureSpec的MODE和数值
其实大家通过查看代码可以知道,我们的实现就是MeasureSpec.getSize()和MeasureSpec.getMode()的实现代码。
(4)、模式有什么用呢
我们知道这里有三个模式:EXACTLY、AT_MOST、UNSPECIFIED
需要注意的是widthMeasureSpec和heightMeasureSpec各自都有它对应的模式,模式的由来分别来自于XML定义:
简单来说,XML布局和模式有如下对应关系大致如下:
wrap_content-> MeasureSpec.AT_MOST
match_parent -> MeasureSpec.EXACTLY
具体值 -> MeasureSpec.EXACTLY
当然,这也不是绝对的,子控件的模式还会受到父控件的模式有关,具体可以参考这篇文章《MeasureSpec之详解》。
例如,下面这个XML
那FlowLayout在onMeasure()中传值时widthMeasureSpec的模式就是 MeasureSpec.EXACTLY,即父窗口宽度值。heightMeasureSpec的模式就是 MeasureSpec.AT_MOST,即不确定的。
一定要注意是,当模式是MeasureSpec.EXACTLY时,我们就不必要设定我们计算的大小了,因为这个大小是用户指定的,我们不应更改。但当模式是MeasureSpec.AT_MOST时,也就是说用户将布局设置成了wrap_content,我们就需要将大小设定为我们计算的数值,因为用户根本没有设置具体值是多少,需要我们自己计算。
即,假如width和height是我们经过计算的控件所占的宽度和高度。那在onMeasure()中使用setMeasuredDimension()最后设置时,代码应该是这样的:
三、onLayout()
上面说了,onLayout()是实现所有子控件布局的函数。注意,是所有子控件!!!那它自己的布局怎么办?后面我们再讲,先讲讲在onLayout()中我们应该做什么。
我们先看看ViewGroup的onLayout()函数的默认行为是什么
在ViewGroup.java中
是一个抽象方法,说明凡是派生自ViewGroup的类都必须自己去实现这个方法。像LinearLayout、RelativeLayout等布局,都是重写了这个方法,然后在内部按照各自的规则对子视图进行布局的。
下面我们就举个例子来看一下有关onMeasure()和onLayout()的具体使用:
下面是效果图:
这个效果图主要有两点:
1、三个TextView竖直排列
2、背景的Layout宽度是match_parent,高度是wrap_content.
下面我们就看一下,代码上如何实现:
(1)、XML布局
首先我们看一下XML布局:(activity_main.xml)
可见里面有三个TextView,然后自定义的MyLinLayout布局,宽度设为了match_parent,高度设为了wrap_content.
(2)、MyLinLayout实现:重写onMeasure()函数
我们前面讲过,onMeasure()的作用就是根据container内部的子控件计算自己的宽和高,最后通过setMeasuredDimension(int width,int height设置进去);
下面看看onMeasure()的完整代码,然后再逐步讲解:
首先,是从父类传过来的建议宽度和高度值:widthMeasureSpec、heightMeasureSpec
从他们里面利用MeasureSpec提取宽高值和对应的模式:
接下来就是通过测量它所有的子控件来决定它所占位置的大小:
我们这里要计算的是整个VIEW当被设置成layout_width="wrap_content",layout_height="wrap_content"所占用的大小,因为我们是垂直排列其内部所有的VIEW,所以container所占宽度应该是各个TextVIew中的最大宽度,所占高度应该是所有控件的高度和。
最后,根据当前用户的设置来判断是否将计算出来的值设置进onMeasure()中,用它来计算当前container所在位置。
前面我们讲过,模式与XML布局的对应关系:
* wrap_content-> MeasureSpec.AT_MOST
* match_parent -> MeasureSpec.EXACTLY
* 具体值 -> MeasureSpec.EXACTLY
再看我们前面XML中针对MyLinLayout的设置:
所以这里的measureWidthMode应该是MeasureSpec.EXACTLY,measureHeightMode应该是MeasureSpec.AT_MOST;所以在最后利用setMeasuredDimension(width,height)来最终设置时,width使用的是从父类传过来的measureWidth,而高度则是我们自己计算的height.即实际的运算结果是这样的:
总体来讲,onMeasure()中计算出的width和height,就是当XML布局设置为layout_width="wrap_content"、layout_height="wrap_content"时所占的宽和高;即整个container所占的最小矩形
(3)、MyLinLayout实现:重写onLayout()函数
在这部分,就是根据自己的意愿把内部的各个控件排列起来。我们要完成的是将所有的控件垂直排列;
先看完整的代码,然后再细讲:
最核心的代码,就是调用layout()函数设置子控件所在的位置:
在这里top指的是控件的顶点,那bottom的坐标就是top+childHeight,我们从最左边开始布局,那么right的坐标就肯定是子控件的宽度值了childWidth.
到这里,这个例子就讲完了,源码会在文章底部给出,下面来讲一个非常容易混淆的问题。
(4)、getMeasuredWidth()与getWidth()
趁热打铁,就这个例子,我们讲一个很容易出错的问题:getMeasuredWidth()与getWidth()的区别。他们的值大部分时间都是相同的,但意义确是根本不一样的,我们就来简单分析一下。
区别主要体现在下面几点:
- 首先getMeasureWidth()方法在measure()过程结束后就可以获取到了,而getWidth()方法要在layout()过程结束后才能获取到。
- getMeasureWidth()方法中的值是通过setMeasuredDimension()方法来进行设置的,而getWidth()方法中的值则是通过layout(left,top,right,bottom)方法设置的。
还记得吗,我们前面讲过,setMeasuredDimension()提供的测量结果只是为布局提供建议,最终的取用与否要看layout()函数。大家再看看我们上面重写的MyLinLayout,是不是我们自己使用child.layout(left,top,right,bottom)来定义了各个子控件所应在的位置:
从代码中可以看到,我们使用child.layout(0, top, childWidth, top + childHeight);来布局控件的位置,其中getWidth()的取值就是这里的右坐标减去左坐标的宽度;因为我们这里的宽度是直接使用的child.getMeasuredWidth()的值,当然会导致getMeasuredWidth()与getWidth()的值是一样的。如果我们在调用layout()的时候传进去的宽度值不与getMeasuredWidth()相同,那必然getMeasuredWidth()与getWidth()的值就不再一样了。
一定要注意的一点是:getMeasureWidth()方法在measure()过程结束后就可以获取到了,而getWidth()方法要在layout()过程结束后才能获取到。再重申一遍!!!!!
源码在文章底部给出
前面我们说了,在派生自ViewGroup的container中,比如我们上面的MyLinLayout,在onLayout()中布局它所有的子控件。那它自己什么时候被布局呢?
它当然也有父控件,它的布局也是在父控件中由它的父控件完成的,就这样一层一层地向上由各自的父控件完成对自己的布局。真到所有控件的最顶层结点,在所有的控件的最顶部有一个ViewRoot,它才是所有控件的最终祖先结点。那让我们来看看它是怎么来做的吧。
在它布局里,会调用它自己的一个layout()函数(不能被重载,代码位于View.Java):
在SetFrame(l,t,r,b)就是设置自己的位置,设置结束以后才会调用onLayout(changed, l, t, r, b)来设置内部所有子控件的位置。
OK啦,到这里有关onMeasure()和onLayout()的内容就讲完啦,想必大家应该也对整个布局流程有了一个清楚的认识了,下面我们再看一个紧要的问题:如何得到自定义控件的左右间距margin值。
在这部分,大家先不必纠结这个例子为什么要这么写,我会先简单粗暴的教大家怎么先获取到margin值,然后再细讲为什么这样写,他们的原理是怎样的。
如果要自定义ViewGroup支持子控件的layout_margin参数,则自定义的ViewGroup类必须重载generateLayoutParams()函数,并且在该函数中返回一个ViewGroup.MarginLayoutParams派生类对象,这样才能使用margin参数。
我们在上面MyLinLayout例子的基础上,添加上layout_margin参数;
(1)、首先,在XML中添加上layout_margin参数
我们在每个TextView中都添加了一layout_marginTop参数,而且值分别是10dp,20dp,30dp;背景也都分别改为了红,绿,蓝;
现在我们运行一上,看看效果:
从图中可以看到,根本没作用!!!这是为什么呢?因为测量和布局都是我们自己实现的,我们在onLayout()中没有根据Margin来布局,当然不会出现有关Margin的效果啦。需要特别注意的是,如果我们在onLayout()中根据margin来布局的话,那么我们在onMeasure()中计算container的大小时,也要加上margin,不然会导致container太小,而控件显示不全的问题。费话不多说,我们直接看代码实现。
(2)、重写generateLayoutParams()函数
重写代码如下:
在这里,我们重写了两个函数,一个是generateLayoutParams()函数,一个是generateDefaultLayoutParams()函数。直接返回对应的MarginLayoutParams()的实例。至于为什么要这么写,我们后面再讲,这里先把Margin信息获取到再说。
(3)、重写onMeasure()
让我们先看一下重写好的onMeasure()函数代码:
最关键的地方是改了这句:
通过 child.getLayoutParams()获取child对应的LayoutParams实例,将其强转成MarginLayoutParams;
然后在计算childHeight时添加上顶部间距和底部间距。计算childWidth时添加上左边间距和右边间距。
也就是说,我们在计算宽度和高度时不仅考虑到子控件的本身的大小还要考虑到子控件间的间距问题。
(4)、重写onLayout()函数
同样,我们在布局时仍然将间距加到控件里就好了,完整代码如下:
在这里同样在布局子控件时,添加上子控件间的间距,具体就不讲了,很容易理解。
最终的效果图如下:
从效果图中可以明显的看到每个ITEM都添加上了间距了。
源码在文章底部给出
上面我们看了要重写generateDefaultLayoutParams()函数才能获取控件的margin间距。那为什么要重写呢?下面这句就为什么非要强转呢?
下面我们来看看这么做的原因。
首先,在container在初始化子控件时,会调用LayoutParams generateLayoutParams(LayoutParams p)来为子控件生成对应的布局属性,但默认只是生成layout_width和layout_height所以对应的布局参数,即在正常情况下的generateLayoutParams()函数生成的LayoutParams实例是不能够取到margin值的。即:
所以,如果我们还需要margin相关的参数就只能重写generateLayoutParams()函数了:
由于generateLayoutParams()的返回值是LayoutParams实例,而MarginLayoutParams是派生自LayoutParam的;所以根据类的多态的特性,可以直接将此时的LayoutParams实例直接强转成MarginLayoutParams实例;
所以下面这句在这里是不会报错的:
大家也可以为了安全起见利用instanceOf来做下判断,如下:
所以整体来讲,就是利用了类的多态特性!下面来看看MarginLayoutParams和generateLayoutParams()都做了什么。
写在前面:本部分涉及自定义控件属性的内容,如果对于TypedArray和自定义控件属性不明白的同学请先移动<PullScrollView详解(一)——自定义控件属性>
(1)generateLayoutParams()实现
首先,我们看看generateLayoutPararms()都做了什么吧,它是怎么得到布局值的:
从上面的代码中明显可以看出,generateLayoutParams()调用LayoutParams()产生布局信息,而LayoutParams()最终调用setBaseAttributes()来获得对应的宽,高属性。
这里是通过TypedArray对自定义的XML进行值提取的过程,难度不大,不再细讲。从这里也可以看到,generateLayoutParams生成的LayoutParams属性只有layout_width和layout_height的属性值。
2、MarginLayoutParams实现
下面再来看看MarginLayoutParams的具体实现,其实通过上面的过程,大家也应该想到,它也是通过TypeArray来解析自定义属性来获得用户的定义值的(大家看到长代码不要害怕,先列出完整代码,下面会分段讲):
这段代码分为两部分:
第一部分:提取layout_margin的值并设置
在这段代码中就是通过提取layout_margin的值来设置上,下,左,右边距的。
第二部分:如果用户没有设置layout_margin,而是单个设置的,那么就一个个提取,代码如下:
这里就是对layout_marginLeft、layout_marginRight、layout_marginTop、layout_marginBottom的值一个个提取的过程。难度不大,也没什么好讲的了。
从这里大家也可以看到为什么非要重写generateLayoutParams()函数了,就是因为默认的generateLayoutParams()函数只会提取layout_width、layout_height的值,只有MarginLayoutParams()才具有提取margin间距的功能!!!!
好啦,这篇就到这啦,下篇咱们就开始实现FlowLayout了。
源码内容:
1、《SimpleLayout》:第三部分对应源码:MyLinLayout的初步实现
2、《SimpleLayoutAdvance》:第四部分对应源码:添加上margin值的获取方法
如果本文有帮到你,记得加关注哦
我的示例
源码下载地址:http://download.csdn.net/detail/harvic880925/8928283
首发:http://blog.csdn.net/harvic880925/article/details/47029169
注意,View及ViewGroup基本相同,只是在ViewGroup中不仅要绘制自己还是绘制其中的子控件,而View则只需要绘制自己就可以了,所以我们这里就以ViewGroup为例来讲述整个绘制流程。
绘制流程分为三步:测量、布局、绘制
分别对应:onMeasure()、onLayout()、onDraw()
其中,他们三个的作用分别如下:
onMeasure():测量自己的大小,为正式布局提供建议。(注意,只是建议,至于用不用,要看onLayout);
onLayout():使用layout()函数对所有子控件布局;
onDraw():根据布局的位置绘图;
有关绘图的部分,大家可以参考我的系列博客《android
Graphics(一):概述及基本几何图形绘制》共有四篇,讲述了有关android 绘图的90%内容,大家可以参考。
这篇文章着重将内容放在分析onMeasure()和onLayout()上。
二、onMeasure与MeasureSpec
布局绘画涉及两个过程:测量过程和布局过程。测量过程通过measure方法实现,是View树自顶向下的遍历,每个View在循环过程中将尺寸细节往下传递,当测量过程完成之后,所有的View都存储了自己的尺寸。第二个过程则是通过方法layout来实现的,也是自顶向下的。在这个过程中,每个父View负责通过计算好的尺寸放置它的子View。 前面讲过,onMeasure()是用来测量当前控件大小的,给onLayout()提供数值参考,需要特别注意的是:测量完成以后通过setMeasuredDimension(int,int)设置给系统。
1、onMeasure
首先,看一下onMeasure()的声明:@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); }
这里我们主要关注传进来的两个参数:int widthMeasureSpec, int heightMeasureSpec
与这两个参数有关的是两个问题:意义和组成。即他们是怎么来的,表示什么意思;还有,他们是组成方式是怎样的。
我们先说他们的意义:
他们是父类传递过来给当前view的一个建议值,即想把当前view的尺寸设置为宽widthMeasureSpec,高heightMeasureSpec
有关他们的组成,我们就直接转到MeasureSpec部分。
2、MeasureSpec
虽然表面上看起来他们是int类型的数字,其实他们是由mode+size两部分组成的。 widthMeasureSpec和heightMeasureSpec转化成二进制数字表示,他们都是32位的。前两位代表mode(测量模式),后面30位才是他们的实际数值(size)。
(1)模式分类
它有三种模式:
①、UNSPECIFIED(未指定),父元素不对自元素施加任何束缚,子元素可以得到任意想要的大小;
②、EXACTLY(完全),父元素决定自元素的确切大小,子元素将被限定在给定的边界里而忽略它本身大小;
③、AT_MOST(至多),子元素至多达到指定大小的值。
其值是:
// 进位大小为2的30次方(int的大小为32位,所以进位30位就是要使用int的最高位和倒数第二位也就是32和31位做标志位) private static final int MODE_SHIFT = 30; // 运算遮罩,0x3为16进制,10进制为3,二进制为11。3向左进位30,就是11 00000000000(11后跟30个0) // (遮罩的作用是用1标注需要的值,0标注不要的值。因为1与任何数做与运算都得任何数,0与任何数做与运算都得0) private static final int MODE_MASK = 0x3 << MODE_SHIFT; // 0向左进位30,就是00 00000000000(00后跟30个0) public static final int UNSPECIFIED = 0 << MODE_SHIFT; // 1向左进位30,就是01 00000000000(01后跟30个0) public static final int EXACTLY = 1 << MODE_SHIFT; // 2向左进位30,就是10 00000000000(10后跟30个0) public static final int AT_MOST = 2 << MODE_SHIFT;
由于最前面两位代表模式,所以他们分别对应十进制的0,1,2;
(2)模式提取
现在我们知道了widthMeasureSpec和heightMeasureSpec是由模式和数值组成的,而且二进制的前两位代表模式,后28位代表数字。
我们先想想,如果我们自己来提取widthMeasureSpec和heightMeasureSpec中的模式和数值是怎么提取呢?
首先想到的肯定是通过MASK和与运算去掉不需要的部分而得到对应的模式或数值。
说到这大家可能会迷茫,我们写段代码来提取模式部分吧:
/** * 通过详细测量结果获得mode */ // mode = measureSpec & MODE_MASK; // MODE_MASK = 11 00000000000(11后跟30个0),原理是用MODE_MASK后30位的0替换掉measureSpec后30位中的1,再保留32和31位的mode值。 // 例如10 00..00100 & 11 00..00(11后跟30个0) = 10 00..00(AT_MOST),这样就得到了mode的值 public static int getMode(int measureSpec) { return (measureSpec & MODE_MASK); } /** * 通过详细测量结果获得size */ // size = measureSpec & ~MODE_MASK; // 原理同上,不过这次是将MODE_MASK取反,也就是变成了00 111111(00后跟30个1),将32,31替换成0也就是去掉mode,保留后30位的size public static int getSize(int measureSpec) { return (measureSpec & ~MODE_MASK); }
相信大家看了代码就应该清楚模式和数值提取的方法了吧,主要用到了MASK的与、非运算,难度不大,如果有问题,自行谷歌一下与、非运算方法吧。
(3)、MeasureSpec
上面我们自已实现了模式和数值的提取。但在强大的andorid面前,肯定有提供提取模式和数值的类。这个类就是MeasureSpec
下面两个函数就可以实现这个功能:
MeasureSpec.getMode(int spec) //获取MODE MeasureSpec.getSize(int spec) //获取数值
另外MODE的取值为:
MeasureSpec.AT_MOST MeasureSpec.EXACTLY MeasureSpec.UNSPECIFIED
通过下面的代码就可以分别获取widthMeasureSpec和heightMeasureSpec的MODE和数值
int measureWidth = MeasureSpec.getSize(widthMeasureSpec); int measureHeight = MeasureSpec.getSize(heightMeasureSpec); int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec); int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec);
其实大家通过查看代码可以知道,我们的实现就是MeasureSpec.getSize()和MeasureSpec.getMode()的实现代码。
(4)、模式有什么用呢
我们知道这里有三个模式:EXACTLY、AT_MOST、UNSPECIFIED
需要注意的是widthMeasureSpec和heightMeasureSpec各自都有它对应的模式,模式的由来分别来自于XML定义:
简单来说,XML布局和模式有如下对应关系大致如下:
wrap_content-> MeasureSpec.AT_MOST
match_parent -> MeasureSpec.EXACTLY
具体值 -> MeasureSpec.EXACTLY
当然,这也不是绝对的,子控件的模式还会受到父控件的模式有关,具体可以参考这篇文章《MeasureSpec之详解》。
例如,下面这个XML
<com.example.harvic.myapplication.FlowLayout android:layout_width="match_parent" android:layout_height="wrap_content"> </com.example.harvic.myapplication.FlowLayout>
那FlowLayout在onMeasure()中传值时widthMeasureSpec的模式就是 MeasureSpec.EXACTLY,即父窗口宽度值。heightMeasureSpec的模式就是 MeasureSpec.AT_MOST,即不确定的。
一定要注意是,当模式是MeasureSpec.EXACTLY时,我们就不必要设定我们计算的大小了,因为这个大小是用户指定的,我们不应更改。但当模式是MeasureSpec.AT_MOST时,也就是说用户将布局设置成了wrap_content,我们就需要将大小设定为我们计算的数值,因为用户根本没有设置具体值是多少,需要我们自己计算。
即,假如width和height是我们经过计算的控件所占的宽度和高度。那在onMeasure()中使用setMeasuredDimension()最后设置时,代码应该是这样的:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int measureWidth = MeasureSpec.getSize(widthMeasureSpec); int measureHeight = MeasureSpec.getSize(heightMeasureSpec); int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec); int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec); //经过计算,控件所占的宽和高分别对应width和height //计算过程,我们会在下篇细讲 ………… setMeasuredDimension((measureWidthMode == MeasureSpec.EXACTLY) ? measureWidth: width, (measureHeightMode == MeasureSpec.EXACTLY) ? measureHeight: height); }
三、onLayout()
1、概述
上面说了,onLayout()是实现所有子控件布局的函数。注意,是所有子控件!!!那它自己的布局怎么办?后面我们再讲,先讲讲在onLayout()中我们应该做什么。 我们先看看ViewGroup的onLayout()函数的默认行为是什么
在ViewGroup.java中
@Override protected abstract void onLayout(boolean changed, int l, int t, int r, int b);
是一个抽象方法,说明凡是派生自ViewGroup的类都必须自己去实现这个方法。像LinearLayout、RelativeLayout等布局,都是重写了这个方法,然后在内部按照各自的规则对子视图进行布局的。
2、实例
下面我们就举个例子来看一下有关onMeasure()和onLayout()的具体使用: 下面是效果图:
这个效果图主要有两点:
1、三个TextView竖直排列
2、背景的Layout宽度是match_parent,高度是wrap_content.
下面我们就看一下,代码上如何实现:
(1)、XML布局
首先我们看一下XML布局:(activity_main.xml)
<com.harvic.simplelayout.MyLinLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="#ff00ff" tools:context=".MainActivity"> <TextView android:text="第一个VIEW" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <TextView android:text="第二个VIEW" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <TextView android:text="第三个VIEW" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </com.harvic.simplelayout.MyLinLayout>
可见里面有三个TextView,然后自定义的MyLinLayout布局,宽度设为了match_parent,高度设为了wrap_content.
(2)、MyLinLayout实现:重写onMeasure()函数
我们前面讲过,onMeasure()的作用就是根据container内部的子控件计算自己的宽和高,最后通过setMeasuredDimension(int width,int height设置进去);
下面看看onMeasure()的完整代码,然后再逐步讲解:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int measureWidth = MeasureSpec.getSize(widthMeasureSpec); int measureHeight = MeasureSpec.getSize(heightMeasureSpec); int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec); int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec); int height = 0; int width = 0; int count = getChildCount(); for (int i=0;i<count;i++) { //测量子控件 View child = getChildAt(i); measureChild(child, widthMeasureSpec, heightMeasureSpec); //获得子控件的高度和宽度 int childHeight = child.getMeasuredHeight(); int childWidth = child.getMeasuredWidth(); //得到最大宽度,并且累加高度 height += childHeight; width = Math.max(childWidth, width); } setMeasuredDimension((measureWidthMode == MeasureSpec.EXACTLY) ? measureWidth: width, (measureHeightMode == MeasureSpec.EXACTLY) ? measureHeight: height); }
首先,是从父类传过来的建议宽度和高度值:widthMeasureSpec、heightMeasureSpec
从他们里面利用MeasureSpec提取宽高值和对应的模式:
int measureWidth = MeasureSpec.getSize(widthMeasureSpec); int measureHeight = MeasureSpec.getSize(heightMeasureSpec); int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec); int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec);
接下来就是通过测量它所有的子控件来决定它所占位置的大小:
int height = 0; int width = 0; int count = getChildCount(); for (int i=0;i<count;i++) { //测量子控件 View child = getChildAt(i); measureChild(child, widthMeasureSpec, heightMeasureSpec); //获得子控件的高度和宽度 int childHeight = child.getMeasuredHeight(); int childWidth = child.getMeasuredWidth(); //得到最大宽度,并且累加高度 height += childHeight; width = Math.max(childWidth, width); }
我们这里要计算的是整个VIEW当被设置成layout_width="wrap_content",layout_height="wrap_content"所占用的大小,因为我们是垂直排列其内部所有的VIEW,所以container所占宽度应该是各个TextVIew中的最大宽度,所占高度应该是所有控件的高度和。
最后,根据当前用户的设置来判断是否将计算出来的值设置进onMeasure()中,用它来计算当前container所在位置。
setMeasuredDimension((measureWidthMode == MeasureSpec.EXACTLY) ? measureWidth: width, (measureHeightMode == MeasureSpec.EXACTLY) ? measureHeight: height);
前面我们讲过,模式与XML布局的对应关系:
* wrap_content-> MeasureSpec.AT_MOST
* match_parent -> MeasureSpec.EXACTLY
* 具体值 -> MeasureSpec.EXACTLY
再看我们前面XML中针对MyLinLayout的设置:
<com.harvic.simplelayout.MyLinLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="#ff00ff" tools:context=".MainActivity">
所以这里的measureWidthMode应该是MeasureSpec.EXACTLY,measureHeightMode应该是MeasureSpec.AT_MOST;所以在最后利用setMeasuredDimension(width,height)来最终设置时,width使用的是从父类传过来的measureWidth,而高度则是我们自己计算的height.即实际的运算结果是这样的:
setMeasuredDimension(measureWidth,height);
总体来讲,onMeasure()中计算出的width和height,就是当XML布局设置为layout_width="wrap_content"、layout_height="wrap_content"时所占的宽和高;即整个container所占的最小矩形
(3)、MyLinLayout实现:重写onLayout()函数
在这部分,就是根据自己的意愿把内部的各个控件排列起来。我们要完成的是将所有的控件垂直排列;
先看完整的代码,然后再细讲:
protected void onLayout(boolean changed, int l, int t, int r, int b) { int top = 0; int count = getChildCount(); for (int i=0;i<count;i++) { View child = getChildAt(i); int childHeight = child.getMeasuredHeight(); int childWidth = child.getMeasuredWidth(); child.layout(0, top, childWidth, top + childHeight); top += childHeight; } }
最核心的代码,就是调用layout()函数设置子控件所在的位置:
int childHeight = child.getMeasuredHeight(); int childWidth = child.getMeasuredWidth(); child.layout(0, top, childWidth, top + childHeight); top += childHeight;
在这里top指的是控件的顶点,那bottom的坐标就是top+childHeight,我们从最左边开始布局,那么right的坐标就肯定是子控件的宽度值了childWidth.
到这里,这个例子就讲完了,源码会在文章底部给出,下面来讲一个非常容易混淆的问题。
(4)、getMeasuredWidth()与getWidth()
趁热打铁,就这个例子,我们讲一个很容易出错的问题:getMeasuredWidth()与getWidth()的区别。他们的值大部分时间都是相同的,但意义确是根本不一样的,我们就来简单分析一下。
区别主要体现在下面几点:
- 首先getMeasureWidth()方法在measure()过程结束后就可以获取到了,而getWidth()方法要在layout()过程结束后才能获取到。
- getMeasureWidth()方法中的值是通过setMeasuredDimension()方法来进行设置的,而getWidth()方法中的值则是通过layout(left,top,right,bottom)方法设置的。
还记得吗,我们前面讲过,setMeasuredDimension()提供的测量结果只是为布局提供建议,最终的取用与否要看layout()函数。大家再看看我们上面重写的MyLinLayout,是不是我们自己使用child.layout(left,top,right,bottom)来定义了各个子控件所应在的位置:
int childHeight = child.getMeasuredHeight(); int childWidth = child.getMeasuredWidth(); child.layout(0, top, childWidth, top + childHeight); top += childHeight;
从代码中可以看到,我们使用child.layout(0, top, childWidth, top + childHeight);来布局控件的位置,其中getWidth()的取值就是这里的右坐标减去左坐标的宽度;因为我们这里的宽度是直接使用的child.getMeasuredWidth()的值,当然会导致getMeasuredWidth()与getWidth()的值是一样的。如果我们在调用layout()的时候传进去的宽度值不与getMeasuredWidth()相同,那必然getMeasuredWidth()与getWidth()的值就不再一样了。
一定要注意的一点是:getMeasureWidth()方法在measure()过程结束后就可以获取到了,而getWidth()方法要在layout()过程结束后才能获取到。再重申一遍!!!!!
源码在文章底部给出
3、疑问:container自己什么时候被布局
前面我们说了,在派生自ViewGroup的container中,比如我们上面的MyLinLayout,在onLayout()中布局它所有的子控件。那它自己什么时候被布局呢?它当然也有父控件,它的布局也是在父控件中由它的父控件完成的,就这样一层一层地向上由各自的父控件完成对自己的布局。真到所有控件的最顶层结点,在所有的控件的最顶部有一个ViewRoot,它才是所有控件的最终祖先结点。那让我们来看看它是怎么来做的吧。
在它布局里,会调用它自己的一个layout()函数(不能被重载,代码位于View.Java):
/* final 标识符 , 不能被重载 , 参数为每个视图位于父视图的坐标轴 * @param l Left position, relative to parent * @param t Top position, relative to parent * @param r Right position, relative to parent * @param b Bottom position, relative to parent */ public final void layout(int l, int t, int r, int b) { boolean changed = setFrame(l, t, r, b); //设置每个视图位于父视图的坐标轴 if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) { if (ViewDebug.TRACE_HIERARCHY) { ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT); } onLayout(changed, l, t, r, b);//回调onLayout函数 ,设置每个子视图的布局 mPrivateFlags &= ~LAYOUT_REQUIRED; } mPrivateFlags &= ~FORCE_LAYOUT;
在SetFrame(l,t,r,b)就是设置自己的位置,设置结束以后才会调用onLayout(changed, l, t, r, b)来设置内部所有子控件的位置。
OK啦,到这里有关onMeasure()和onLayout()的内容就讲完啦,想必大家应该也对整个布局流程有了一个清楚的认识了,下面我们再看一个紧要的问题:如何得到自定义控件的左右间距margin值。
四、获取子控件Margin的方法
1、获取方法及示例
在这部分,大家先不必纠结这个例子为什么要这么写,我会先简单粗暴的教大家怎么先获取到margin值,然后再细讲为什么这样写,他们的原理是怎样的。如果要自定义ViewGroup支持子控件的layout_margin参数,则自定义的ViewGroup类必须重载generateLayoutParams()函数,并且在该函数中返回一个ViewGroup.MarginLayoutParams派生类对象,这样才能使用margin参数。
我们在上面MyLinLayout例子的基础上,添加上layout_margin参数;
(1)、首先,在XML中添加上layout_margin参数
<com.harvic.simplelayout.MyLinLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="#ff00ff" tools:context=".MainActivity"> <TextView android:text="第一个VIEW" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="10dp" android:background="#ff0000"/> <TextView android:text="第二个VIEW" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="20dp" android:background="#00ff00"/> <TextView android:text="第三个VIEW" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="30dp" android:background="#0000ff"/> </com.harvic.simplelayout.MyLinLayout>
我们在每个TextView中都添加了一layout_marginTop参数,而且值分别是10dp,20dp,30dp;背景也都分别改为了红,绿,蓝;
现在我们运行一上,看看效果:
从图中可以看到,根本没作用!!!这是为什么呢?因为测量和布局都是我们自己实现的,我们在onLayout()中没有根据Margin来布局,当然不会出现有关Margin的效果啦。需要特别注意的是,如果我们在onLayout()中根据margin来布局的话,那么我们在onMeasure()中计算container的大小时,也要加上margin,不然会导致container太小,而控件显示不全的问题。费话不多说,我们直接看代码实现。
(2)、重写generateLayoutParams()函数
重写代码如下:
@Override protected LayoutParams generateLayoutParams(LayoutParams p) { return new MarginLayoutParams(p); } @Override public LayoutParams generateLayoutParams(AttributeSet attrs) { return new MarginLayoutParams(getContext(), attrs); } @Override protected LayoutParams generateDefaultLayoutParams() { return new MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); }
在这里,我们重写了两个函数,一个是generateLayoutParams()函数,一个是generateDefaultLayoutParams()函数。直接返回对应的MarginLayoutParams()的实例。至于为什么要这么写,我们后面再讲,这里先把Margin信息获取到再说。
(3)、重写onMeasure()
让我们先看一下重写好的onMeasure()函数代码:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int measureWidth = MeasureSpec.getSize(widthMeasureSpec); int measureHeight = MeasureSpec.getSize(heightMeasureSpec); int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec); int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec); int height = 0; int width = 0; int count = getChildCount(); for (int i=0;i<count;i++) { View child = getChildAt(i); measureChild(child, widthMeasureSpec, heightMeasureSpec); MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); int childHeight = child.getMeasuredHeight()+lp.topMargin+lp.bottomMargin; int childWidth = child.getMeasuredWidth()+lp.leftMargin+lp.rightMargin; height += childHeight; width = Math.max(childWidth, width); } setMeasuredDimension((measureWidthMode == MeasureSpec.EXACTLY) ? measureWidth: width, (measureHeightMode == MeasureSpec.EXACTLY) ? measureHeight: height); }
最关键的地方是改了这句:
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); int childHeight = child.getMeasuredHeight()+lp.topMargin+lp.bottomMargin; int childWidth = child.getMeasuredWidth()+lp.leftMargin+lp.rightMargin;
通过 child.getLayoutParams()获取child对应的LayoutParams实例,将其强转成MarginLayoutParams;
然后在计算childHeight时添加上顶部间距和底部间距。计算childWidth时添加上左边间距和右边间距。
也就是说,我们在计算宽度和高度时不仅考虑到子控件的本身的大小还要考虑到子控件间的间距问题。
(4)、重写onLayout()函数
同样,我们在布局时仍然将间距加到控件里就好了,完整代码如下:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int top = 0;
int count = getChildCount();
for (int i=0;i<count;i++) {
View child = getChildAt(i);
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); int childHeight = child.getMeasuredHeight()+lp.topMargin+lp.bottomMargin; int childWidth = child.getMeasuredWidth()+lp.leftMargin+lp.rightMargin;
child.layout(0, top, childWidth, top + childHeight);
top += childHeight;
}
}
在这里同样在布局子控件时,添加上子控件间的间距,具体就不讲了,很容易理解。
最终的效果图如下:
从效果图中可以明显的看到每个ITEM都添加上了间距了。
源码在文章底部给出
2、原理
上面我们看了要重写generateDefaultLayoutParams()函数才能获取控件的margin间距。那为什么要重写呢?下面这句就为什么非要强转呢?MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
下面我们来看看这么做的原因。
首先,在container在初始化子控件时,会调用LayoutParams generateLayoutParams(LayoutParams p)来为子控件生成对应的布局属性,但默认只是生成layout_width和layout_height所以对应的布局参数,即在正常情况下的generateLayoutParams()函数生成的LayoutParams实例是不能够取到margin值的。即:
/** *从指定的XML中获取对应的layout_width和layout_height值 */ public LayoutParams generateLayoutParams(AttributeSet attrs) { return new LayoutParams(getContext(), attrs); } /* *如果要使用默认的构造方法,就生成layout_width="wrap_content"、layout_height="wrap_content"对应的参数 */ protected LayoutParams generateDefaultLayoutParams() { return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); }
所以,如果我们还需要margin相关的参数就只能重写generateLayoutParams()函数了:
public LayoutParams generateLayoutParams(AttributeSet attrs) { return new LayoutParams(getContext(), attrs); }
由于generateLayoutParams()的返回值是LayoutParams实例,而MarginLayoutParams是派生自LayoutParam的;所以根据类的多态的特性,可以直接将此时的LayoutParams实例直接强转成MarginLayoutParams实例;
所以下面这句在这里是不会报错的:
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
大家也可以为了安全起见利用instanceOf来做下判断,如下:
MarginLayoutParams lp = null if (child.getLayoutParams() instanceof MarginLayoutParams) { lp = (MarginLayoutParams) child.getLayoutParams(); ………… }
所以整体来讲,就是利用了类的多态特性!下面来看看MarginLayoutParams和generateLayoutParams()都做了什么。
3、MarginLayoutParams与generateLayoutParams()的实现
写在前面:本部分涉及自定义控件属性的内容,如果对于TypedArray和自定义控件属性不明白的同学请先移动<PullScrollView详解(一)——自定义控件属性>(1)generateLayoutParams()实现
首先,我们看看generateLayoutPararms()都做了什么吧,它是怎么得到布局值的:
//位于ViewGrop.java中
public LayoutParams generateLayoutParams(AttributeSet attrs) { return new LayoutParams(getContext(), attrs); }
public LayoutParams(Context c, AttributeSet attrs) {
TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout);
setBaseAttributes(a,
R.styleable.ViewGroup_Layout_layout_width,
R.styleable.ViewGroup_Layout_layout_height);
a.recycle();
}
protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) {
width = a.getLayoutDimension(widthAttr, "layout_width");
height = a.getLayoutDimension(heightAttr, "layout_height");
}
从上面的代码中明显可以看出,generateLayoutParams()调用LayoutParams()产生布局信息,而LayoutParams()最终调用setBaseAttributes()来获得对应的宽,高属性。
这里是通过TypedArray对自定义的XML进行值提取的过程,难度不大,不再细讲。从这里也可以看到,generateLayoutParams生成的LayoutParams属性只有layout_width和layout_height的属性值。
2、MarginLayoutParams实现
下面再来看看MarginLayoutParams的具体实现,其实通过上面的过程,大家也应该想到,它也是通过TypeArray来解析自定义属性来获得用户的定义值的(大家看到长代码不要害怕,先列出完整代码,下面会分段讲):
public MarginLayoutParams(Context c, AttributeSet attrs) { super(); TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_MarginLayout); int margin = a.getDimensionPixelSize( com.android.internal.R.styleable.ViewGroup_MarginLayout_layout_margin, -1); if (margin >= 0) { leftMargin = margin; topMargin = margin; rightMargin= margin; bottomMargin = margin; } else { leftMargin = a.getDimensionPixelSize( R.styleable.ViewGroup_MarginLayout_layout_marginLeft, UNDEFINED_MARGIN); rightMargin = a.getDimensionPixelSize( R.styleable.ViewGroup_MarginLayout_layout_marginRight, UNDEFINED_MARGIN); topMargin = a.getDimensionPixelSize( R.styleable.ViewGroup_MarginLayout_layout_marginTop, DEFAULT_MARGIN_RESOLVED); startMargin = a.getDimensionPixelSize( R.styleable.ViewGroup_MarginLayout_layout_marginStart, DEFAULT_MARGIN_RELATIVE); endMargin = a.getDimensionPixelSize( R.styleable.ViewGroup_MarginLayout_layout_marginEnd, DEFAULT_MARGIN_RELATIVE); } a.recycle(); }
这段代码分为两部分:
第一部分:提取layout_margin的值并设置
TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_MarginLayout); int margin = a.getDimensionPixelSize( com.android.internal.R.styleable.ViewGroup_MarginLayout_layout_margin, -1); if (margin >= 0) { leftMargin = margin; topMargin = margin; rightMargin= margin; bottomMargin = margin; } else { ………… }
在这段代码中就是通过提取layout_margin的值来设置上,下,左,右边距的。
第二部分:如果用户没有设置layout_margin,而是单个设置的,那么就一个个提取,代码如下:
leftMargin = a.getDimensionPixelSize( R.styleable.ViewGroup_MarginLayout_layout_marginLeft, UNDEFINED_MARGIN); rightMargin = a.getDimensionPixelSize( R.styleable.ViewGroup_MarginLayout_layout_marginRight, UNDEFINED_MARGIN); topMargin = a.getDimensionPixelSize( R.styleable.ViewGroup_MarginLayout_layout_marginTop, DEFAULT_MARGIN_RESOLVED); startMargin = a.getDimensionPixelSize( R.styleable.ViewGroup_MarginLayout_layout_marginStart, DEFAULT_MARGIN_RELATIVE); endMargin = a.getDimensionPixelSize( R.styleable.ViewGroup_MarginLayout_layout_marginEnd, DEFAULT_MARGIN_RELATIVE);
这里就是对layout_marginLeft、layout_marginRight、layout_marginTop、layout_marginBottom的值一个个提取的过程。难度不大,也没什么好讲的了。
从这里大家也可以看到为什么非要重写generateLayoutParams()函数了,就是因为默认的generateLayoutParams()函数只会提取layout_width、layout_height的值,只有MarginLayoutParams()才具有提取margin间距的功能!!!!
好啦,这篇就到这啦,下篇咱们就开始实现FlowLayout了。
源码内容:
1、《SimpleLayout》:第三部分对应源码:MyLinLayout的初步实现
2、《SimpleLayoutAdvance》:第四部分对应源码:添加上margin值的获取方法
如果本文有帮到你,记得加关注哦
我的示例
源码下载地址:http://download.csdn.net/detail/harvic880925/8928283
首发:http://blog.csdn.net/harvic880925/article/details/47029169
相关文章推荐
- 自定义控件之视图篇(一)—— 测量与布局
- android自定义控件系列教程----视图的测量和布局
- Android中自定义控件的测量及布局
- 自定义控件引起布局文件中的NullPointerException问题
- Android自定义控件系列 十:利用添加自定义布局来搞定触摸事件的分发,解决组合界面中特定控件响应特定方向的事件
- 自定义控件:流式布局
- 自定义控件格子布局:CellLayout
- 第一行代码 第三章 引入布局和自定义控件
- android#嵌入式布局并创建自定义控件
- Android自定义控件系列 十:利用添加自定义布局来搞定触摸事件的分发,解决组合界面中特定控件响应特定方向的事件
- Android自定义控件之流布局
- 坑!!关于通知栏中支持自定义布局,不支持自定义控件的问题
- 测量webview滑动监听的自定义控件
- 自定义控件实现图片布局
- Android基础之自定义控件、布局以及ListView控件
- Android应用程序窗口(Activity)的测量(Measure)、布局(Layout)和绘制(Draw)过程分析
- View测量、布局及绘制原理
- 自定义控件之-流式布局FlowLayout
- [置顶] 自定义控件:onMeasure 方法和测量原理的理解
- 自定义View,包括事件分发,滑动冲突,测量以及布局