测量控件尺寸(宽度、高度)是开发自定义控件的第一步,只有确定尺寸后才能开始画(利用canvas在画布上画,我们所使用的控件实际上都是这样画上去的)。当然,这个尺寸是需要根据控件的各个部分计算出来的,比如:padding、文字大小,间距等。


非容器控件的onMeasure

下面我们就来看看如何给非容器控件(即直接extends View)这只尺寸的:

1. @Override
2. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
3. setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec));
4. }

通过重写onMeasure()方法来设置尺寸。这个方法实际是由容器控件(LinearLayout、RelativeLayout)来调用的,以便容器控件知道我需要分配给你多大空间或尺寸。

计算完尺寸后,最终调用setMeasuredDimension()来设置。比如,我要创建个 宽度是200px,高度是100px的控件,就可以setMeasuredDimension(200, 100)。

注意setMeasuredDimension()接收的值是px值,而不是dp值,所以我们不可能像上面那样将尺寸写固定的。如何做那:

参数 int widthMeasureSpec, int heightMeasureSpec 是指明控件可获得的空间以及关于这个空间描述的元数据,是与布局文件中android:layout_width、android:layout_height相联系的。如何使用:

01.    /**
02. * 计算组件宽度
03. */
04. private int measureWidth(int widthMeasureSpec) {
05. int result;
06. int specMode = MeasureSpec.getMode(widthMeasureSpec);
07. int specSize = MeasureSpec.getSize(widthMeasureSpec);
08.  
09. if (specMode == MeasureSpec.EXACTLY) {//精确模式
10. result = specSize;
11. else {
12. result = getDefaultWidth();//最大尺寸模式,getDefaultWidth方法需要我们根据控件实际需要自己实现
13. if (specMode == MeasureSpec.AT_MOST) {
14. result = Math.min(result, specSize);
15. }
16. }
17. return result;
18. }

调用MeasureSpec.getMode(measureSpec),可以获得设置尺寸的mode(模式)。

mode共有三种情况,取值分别为:

 MeasureSpec.UNSPECIFIED,MeasureSpec.EXACTLY,MeasureSpec.AT_MOST。


MeasureSpec.EXACTLY是精确尺寸,当我们将控件的layout_width或layout_height指定为具体数值时如andorid:layout_width="50dip",或者为FILL_PARENT是,都是控件大小已经确定的情况,都是精确尺寸。

MeasureSpec.AT_MOST是最大尺寸,当控件的layout_width或layout_height指定为WRAP_CONTENT时,控件大小一般随着控件的子空间或内容进行变化,此时控件尺寸只要不超过父控件允许的最大尺寸即可。因此,此时的mode是AT_MOST,size给出了父控件允许的最大尺寸。

MeasureSpec.UNSPECIFIED是未指定尺寸,这种情况不多,一般都是父控件是AdapterView,通过measure方法传入的模式。


measureWidth() 方法是个固定实现,几乎不用改, 唯一需要我们实现的就是getDefaultWidth()。即,当mode处于最大模式下,此时父容器会将他所能给你的或者说目前剩余的最大空间给你。你只能在这个空间内设定控件尺寸。一个简单的类似TextView的getDefaultWidth()的实现:

1. private void getDefaultWidth(){
2. int txtWidth = (int)this.paint.measureText(this.text);
3. return txtWidth + this.paddingLeft + this.paddingRight;
4. }


上面说了宽度的测量,高度同理:

01.    /**
02. * 计算组件高度
03. */
04. private int measureHeight(int measureSpec) {
05. int result;
06. int specMode = MeasureSpec.getMode(measureSpec);
07. int specSize = MeasureSpec.getSize(measureSpec);
08.  
09. if (specMode == MeasureSpec.EXACTLY) {
10. result = specSize;
11. else {
12. result = getDefaultHeight();
13. if (specMode == MeasureSpec.AT_MOST) {
14. result = Math.min(result, specSize);
15. }
16. }
17. return result;
18. }

有了上面的实现,我们就可以在布局文件中使用控件时,通过android:layout_width、android:layout_height来自定义控件的宽度、高度。


容器控件的onMeasure


容器控件一般是继承ViewGroup,ViewGroup是个抽象类,本身没有实现onMeasure,但是他的子类都有各自的实现,通常他们都是通过measureChildWithMargins函数或者其他类似于measureChild的函数来遍历测量子View,被GONE的子View将不参与测量,当所有的子View都测量完毕后,才根据父View传递过来的模式和大小来最终决定自身的大小.

在测量子View时,会先获取子View的LayoutParams,从中取出宽高,如果是大于0,将会以精确的模式加上其值组合成MeasureSpec传递子View,如果是小于0,将会把自身的大小或者剩余的大小传递给子View,其模式判定在前面表中有对应关系.

ViewGroup一般都在测量完所有子View后才会调用setMeasuredDimension()设置自身大小。



==========================================================================================

欢迎加入我们的技术交流群:
Android群: 66756039
JavaEE群:  361579846 


Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐