原文 http://yifeiyuan.me/2015/10/12/%E8%87%AA%E5%AE%9A%E4%B9%89View%E7%9A%84onMeasure%E3%80%81onLayout/
自定义View有几个非常重要的流程:
这里来学习一下onMeasure(重点讲),onLayout(大致了解),另外这里也侧重ViewGroup,因为vp比较难,如果把vp弄懂了,view应该也不在话下.
先讲几个知识点:
setMeasuredDimension(int measuredWidth, int measuredHeight)
设置大小.setMeasuredDimension
调用后,可以通过getMeasuredHeight()
,getMeasuredWidth()
获得测量的宽高getWidth
,getHeight
获取宽高,与之前的getMeasuredXXX
不同,他们可能不相等. 而测量我们需要MeasureSpec
来帮助,它字面意思就是测量规则,它包括测量模式以及大小,它是一个32位的int值,它的高2位
是测量的模式,低30位
是测量的大小.
MeasureSpec.getMode(int measureSpec)
获得MeasureSpec.getSize(int measureSpec)
获得模式有三种:
1 2 |
* The parent has determined an exact size for the child. The child is going to be * given those bounds regardless of how big it wants to be. |
EXACTLY值为:0
父View告诉你,你应该多少大小.
当XMl里的宽高属性为具体值
或者为match_parent
,为EXACTLY.
例:
1 2 |
android:layout_width="200dp" android:layout_height="match_parent" |
1
|
* The child can be as large as it wants up to the specified size.
|
AT_MOST值为:-2147483648
子控件大小最多为多少,在xml里配置的属性为wrap_content
的时候.
如果自定义View要支持wrap_content必须重写onMeasure,否则大小可能为0
1 2 |
* The parent has not imposed any constraint on the child. It can be whatever size * it wants. |
值为 1073741824
想多大多大,一般见不到,一般自定义View才用.
新建一个ViewGroup,重写onMeasure并打印日志.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
public class MyViewGroup extends ViewGroup { @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); logModeAndSize(widthMeasureSpec); logModeAndSize(heightMeasureSpec); Log.d(TAG, "onMeasure: height"+getMeasuredHeight()+";width:"+getMeasuredWidth()); } } //打印测量模式和大小 private void logModeAndSize(int measureSpec) { switch (MeasureSpec.getMode(measureSpec)) { case MeasureSpec.UNSPECIFIED: Log.d(TAG, "UNSPECIFIED: "+MeasureSpec.getSize(measureSpec)); break; case MeasureSpec.AT_MOST: Log.d(TAG, "AT_MOST: "+MeasureSpec.getSize(measureSpec)); break; case MeasureSpec.EXACTLY: Log.d(TAG, "EXACTLY: "+MeasureSpec.getSize(measureSpec)); break; } } |
match_parent
和wrap_content
1 2 3 4 |
<yifeiyuan.practice.practicedemos.customview.MyViewGroup
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
|
得到日志:
1 2 3 |
D/MyViewGroup: EXACTLY: 1080 D/MyViewGroup: AT_MOST: 1692 D/MyViewGroup: onMeasure: height1692;width:1080 |
可以看到match_parent
对应模式是EXACTLY
wrap_content
对应模式是AT_MOST
顺带一提,如果是继承View,在这里的效果也是一样的.
然而,如果继承LinearLayout,效果则不一样,可以看到测量后,高度为0了
1 2 3 |
D/MyViewGroup: EXACTLY: 1080 D/MyViewGroup: AT_MOST: 1692 D/MyViewGroup: onMeasure: height0;width:1080 |
1 2 3 4 5 |
<yifeiyuan.practice.practicedemos.customview.MyViewGroup
android:layout_width="400dp"
android:layout_height="wrap_content"
android:background="#00ff00"
/>
|
log:
1 2 3 |
D/MyViewGroup: EXACTLY: 1200 D/MyViewGroup: AT_MOST: 1692 D/MyViewGroup: onMeasure: height1692;width:1200 |
可以看到具体值对应EXACTLY模式,这里View和ViewGroup也是一样.
知道测量的规则后,其实可以得出比较模板化的代码:
适用于自定义View:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
private int measureWidth(int widthMeasureSpec){ int result = 0; int size = MeasureSpec.getSize(widthMeasureSpec); int mode = MeasureSpec.getMode(widthMeasureSpec); if (mode == MeasureSpec.EXACTLY){ result = size; }else{ result = 100;// 实际上需要自己计算 if (mode==MeasureSpec.AT_MOST){ //至多模式,别超过了 result = Math.min(result, size); } } return result; } |
ViewGroup
如果是自定义ViewGroup,那就各有不同了,每个ViewGroup都不一样,不过大致流程也差不多,就是测量子View再决定自己的大小.
简单的例子如下,把所有子View的高度之和当做自己的高度:
1 2 3 4 5 6 7 8 |
int childcount = getChildCount(); int height = 0; for (int i = 0; i < childcount; i++) { View child = getChildAt(i); child.measure(widthMeasureSpec, heightMeasureSpec); height += child.getMeasuredHeight(); } setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), height); |
OK,差不多该知道的知识点也知道了,实践一下
实现一个类似垂直的LinearLayout.
在xml里引用之前自定义的ViewGroup,并添加几个宽度高度背景色都不一样的View
包括wrap_content
,match_parent
,xxxdp
,可见,不可见各种情况.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
<yifeiyuan.practice.practicedemos.customview.MyViewGroup android:layout_width="300dp" android:layout_height="wrap_content" android:background="#0000ff" > <TextView android:id="@+id/tv1" android:layout_width="wrap_content" android:layout_height="60dp" android:background="#88ff33" android:text="我只是个TextView" android:textColor="#ffffff" android:textSize="20sp" tools:background="#333333" /> <TextView android:id="@+id/tv2" android:layout_width="100dp" android:layout_height="wrap_content" android:background="#ff0000" android:text="Hello,ViewGroup" android:textColor="#ffffff" android:textSize="20sp" /> <TextView android:id="@+id/tv3" android:layout_width="match_parent" android:layout_height="200dp" android:background="#ee00ee" android:text="Hello,MatchParent" android:textColor="#ffffff" android:textSize="20sp" /> <TextView android:id="@+id/tv4" android:layout_width="match_parent" android:layout_height="100dp" android:background="#44ff33" android:text="Hello,MatchParent" android:textColor="#ffffff" android:textSize="20sp" android:visibility="gone" /> <TextView android:layout_width="match_parent" android:layout_height="100dp" android:background="#ff00ff" android:text="Hello,MatchParent" android:textColor="#ffffff" android:textSize="20sp" android:visibility="invisible" /> <View android:layout_width="50dp" android:layout_height="20dp" android:background="#000000" /> </yifeiyuan.practice.practicedemos.customview.MyViewGroup> |
主要的思路是根据LayoutParams,给子View生成MeasureSpec规则,去测量各个子View的宽高,最终决定自己的宽高.
需要注意的是:记得处理不可见的状态,因为GONE
掉的View是没有宽高的,所以跳过它,提高效率.
Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int height = 0;//group的计算高度 int width = 0;//宽度 int heightSize = MeasureSpec.getSize(heightMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int childcount = getChildCount(); for (int i = 0; i < childcount; i++) { View child = getChildAt(i); //gone 的就无视掉 if (child.getVisibility() == GONE) { continue; } LayoutParams lp = child.getLayoutParams(); int widthSpec = 0; int heightSpec = 0; //根据LayoutParams,给子View生成MeasureSpec规则 if (lp.width == LayoutParams.WRAP_CONTENT) { widthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.AT_MOST); } else if (lp.width == LayoutParams.MATCH_PARENT) { widthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY); } else { //其实xml里不会出现这样的情况 widthSpec = MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY); } if (lp.height == LayoutParams.WRAP_CONTENT) { heightSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.AT_MOST); } else if (lp.height == LayoutParams.MATCH_PARENT) { heightSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY); } else { heightSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY); } child.measure(widthSpec, heightSpec); //把所有的子View的高度加起来,就是高度 height += child.getMeasuredHeight(); // 拿子View中的最大宽度当自己的宽度,保证所有子View能够显示全 width = Math.max(width, child.getMeasuredWidth()); Log.d(TAG, "onMeasure: i:" + i + ",width:" + child.getMeasuredWidth() + ",height:" + child.getMeasuredHeight()); } // 再根据父view给自己的spec,处理自己的宽高 // 这里没有显式处理Unspecified,其实已经计算了宽高,当做UNSPECIFIED的值了 if (MeasureSpec.EXACTLY == widthMode) { width = widthSize; }else if (MeasureSpec.AT_MOST == widthMode) { width = Math.min(width, widthSize); } if (MeasureSpec.EXACTLY == heightMode) { height = heightSize; }else if (MeasureSpec.AT_MOST == heightMode) { height = Math.min(height, heightSize); } //一定要记得调用 setMeasuredDimension(width, height); Log.d(TAG, "onMeasure: height" + getMeasuredHeight() + ";width:" + getMeasuredWidth()); } |
注意:
测量完毕后我们需要布局.
onLayout相对简单,只要记录一下总高度,挨个放就行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int childcount = getChildCount(); int height = 0; for (int i = 0; i < childcount; i++) { View child = getChildAt(i); if (child.getVisibility() == GONE) { continue; } child.layout(l,t+height,l+child.getMeasuredWidth(),t+height+child.getMeasuredHeight()); height += child.getMeasuredHeight(); Log.d(TAG, "onLayout: i:" + i + ",width:" + child.getMeasuredWidth() + ",height:" + child.getMeasuredHeight()); } } |
因为我在onMeasure,onLayout都打印了日志,来看一次循环的日志:
1 2 3 4 5 6 7 8 9 10 11 |
D/MyViewGroup: onMeasure: i:0,width:486,height:180 D/MyViewGroup: onMeasure: i:1,width:300,height:152 D/MyViewGroup: onMeasure: i:2,width:900,height:600 D/MyViewGroup: onMeasure: i:4,width:900,height:300 D/MyViewGroup: onMeasure: i:5,width:150,height:60 D/MyViewGroup: onMeasure: height1292;width:900 D/MyViewGroup: onLayout: i:0,width:486,height:180 D/MyViewGroup: onLayout: i:1,width:300,height:152 D/MyViewGroup: onLayout: i:2,width:900,height:600 D/MyViewGroup: onLayout: i:4,width:900,height:300 D/MyViewGroup: onLayout: i:5,width:150,height:60 |
可以看到测量的效果还是符合实际情况的~
OK,挺好,跟预期效果一样~~
这里已经讲了onMeasure,onLayout的用法,其实并不难,只是需要耐心,仔细.
看完了,相信对大家掌握自定义ViewGroup也有所帮助.
虽然没有处理padding,margin值,但是相信这些你可以搞定.