我们继承重写ViewGroup的目的是要做自定义控件,所以我们有必要先看一下安卓View的绘制过程:
首先当Activity获得焦点时,它将被要求绘制自己的布局,Android framework将会处理绘制过程,Activity只需提供它的布局的根节点。
绘制过程从布局的根节点开始,从根节点开始测量和绘制整个layout tree,绘画通过遍历整个树来完成,不可见的区域的View被放弃。
每一个ViewGroup 负责要求它的每一个孩子被绘制,每一个View负责绘制自己。
因为整个树是按顺序遍历的,所以父节点会先被绘制,而兄弟节点会按照它们在树中出现的顺序被绘制。
绘制是两个过程:一个measure过程和一个layout过程。
1.测量过程
是在measure(int, int)中实现的,是从树的顶端由上到下进行的。
在这个递归过程中,每一个View会把自己的dimension specifications传递下去。
在measure过程的最后,每一个View都存储好了自己的measurements,即测量结果。
2.布局过程
发生在 layout(int, int, int, int)中,仍然是从上到下进行。
在这一遍中,每一个parent都会负责用测量过程中得到的尺寸,把自己的所有孩子放在正确的地方。
所以在继承ViewGroup类时,需要重写两个方法,分别是onMeasure和onLayout。重写ViewGroup的过程大致是两个:
1)测量过程>>>onMeasure(int widthMeasureSpec, int heightMeasureSpec)
传入的参数是本View的可见长和宽,通过这个方法循环测量所有View的尺寸并且存储在View里面;
2)布局过程>>>onLayout(boolean changed, int l, int t, int r, int b)
传入的参数是View可见区域的上下左右四边的位置,在这个方法里面可以通过layout来放置子View;
我们先来看一下测量的过程,也就是该如何重写onMeasure方法,重写之前我们先要了解这个方法:
onMeasure方法
onMeasure方法是测量view和它的内容,决定measured width和measured height的,子类可以覆写onMeasure来提供更加准确和有效的测量。
注意:在覆写onMeasure方法的时候,必须调用 setMeasuredDimension(int,int)
来存储这个View经过测量得到的measured width and height。
如果没有这么做,将会由measure(int, int)方法抛出一个IllegalStateException。
并且覆写onMeasure方法的时候,子类有责任确保measured height and width至少为这个View的最小height和width。getSuggestedMinimumHeight()
and getSuggestedMinimumWidth()
onMeasure方法如下:
protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec)
其中两个参数如下:
widthMeasureSpec
heightMeasureSpec
传入的参数是两个int分别是parent提出的水平和垂直的空间要求。
这两个要求是按照View.MeasureSpec类来进行编码的。参见View.MeasureSpec这个类的说明:
两个参数分别代表宽度和高度的MeasureSpec,android2.2文档中对于MeasureSpec中的说明是:
一个MeasureSpec封装了从父容器传递给子容器的布局需求.每一个MeasureSpec代表了一个宽度,或者高度的说明.一个MeasureSpec是一个大小跟模式的组合值.
这个类包装了从parent传递下来的布局要求,传递给这个child。
简单地说就是每一个MeasureSpec代表了对宽度或者高度的一个要求。
每一个MeasureSpec有一个尺寸(size)和一个模式(mode)构成。
MeasureSpecs这个类提供了把一个<size, mode>的元组包装进一个int型的方法,从而减少对象分配。当然也提供了逆向的解析方法,从int值中解出size和mode。我们先看三种模式:
有三种模式:
UNSPECIFIED
这说明parent没有对child强加任何限制,child可以是它想要的任何尺寸,子容器想要多大就多大。
EXACTLY
Parent为child决定了一个绝对尺寸,child将会被赋予这些边界限制,不管child自己想要多大,子容器应当服从这些边界。
AT_MOST
Child可以是自己任意的大小,但是有个绝对尺寸的上限,即子容器可以是声明大小内的任意大小。
当我们设置width或height为fill_parent时,容器在布局时调用子view的measure方法传入的模式是EXACTLY,因为子view会占据剩余容器的空间,所以它大小是确定的。而当设置为wrap_content时,容器传进去的是AT_MOST, 表示子view的大小最多是多少,这样子view会根据这个上限来设置自己的尺寸。当子view的大小设置为精确值时,容器传入的是EXACTLY, 而MeasureSpec的UNSPECIFIED模式目前还没有发现在什么情况下使用。
View的onMeasure方法默认行为是当模式为UNSPECIFIED时,设置尺寸为mMinWidth(通常为0)或者背景drawable的最小尺寸,当模式为EXACTLY或者AT_MOST时,尺寸设置为传入的MeasureSpec的大小。
具体取出模式或者值的方法:
根据提供的测量值(格式)提取模式(上述三个模式之一)
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
根据提供的测量值(格式)提取大小值(这个大小也就是我们通常所说的大小)
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
而合成则可以使用下面的方法:
根据提供的大小值和模式创建一个测量值(格式)
MeasureSpec.makeMeasureSpec(size, MeasureSpec.AT_MOST);
我们回忆一下之前一开始讲过的View绘制过程
当一个View的measure()方法返回的时候,它的getMeasuredWidth和getMeasuredHeight方法的值一定是被设置好的。它所有的子节点同样被设置好。一个View的测量宽和测量高一定要遵循父View的约束,这保证了在测量过程结束的时候,所有的父View可以接受子View的测量值。一个父View或许会多次调用子View的measure()方法。举个例子,父View会使用不明确的尺寸去丈量看看子View到底需要多大,当子View总的尺寸太大或者太小的时候会再次使用实际的尺寸去调用onMeasure().
下面我们来看看具体代码:
我们来看View类中measure和onMeasure函数的源码:
public final void measure(int widthMeasureSpec, int heightMeasureSpec) { if ((mPrivateFlags & FORCE_LAYOUT) == FORCE_LAYOUT || widthMeasureSpec != mOldWidthMeasureSpec || heightMeasureSpec != mOldHeightMeasureSpec) { // first clears the measured dimension flag mPrivateFlags &= ~MEASURED_DIMENSION_SET; if (ViewDebug.TRACE_HIERARCHY) { ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_MEASURE); } // measure ourselves, this should set the measured dimension flag back onMeasure(widthMeasureSpec, heightMeasureSpec); // flag not set, setMeasuredDimension() was not invoked, we raise // an exception to warn the developer if ((mPrivateFlags & MEASURED_DIMENSION_SET) != MEASURED_DIMENSION_SET) { throw new IllegalStateException("onMeasure() did not set the" + " measured dimension by calling" + " setMeasuredDimension()"); } mPrivateFlags |= LAYOUT_REQUIRED; } mOldWidthMeasureSpec = widthMeasureSpec; mOldHeightMeasureSpec = heightMeasureSpec; }
measure的过程是固定的,而measure中调用了onMeasure函数,因此真正有变数的是onMeasure函数,onMeasure的默认实现很简单,源码如下:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); }
onMeasure默认的实现仅仅调用了setMeasuredDimension,setMeasuredDimension函数是一个很关键的函数,它对View的成员变量mMeasuredWidth和mMeasuredHeight变量赋值,而measure的主要目的就是对View树中的每个View的mMeasuredWidth和mMeasuredHeight进行赋值。一旦这两个变量被赋值,则意味着该View的测量工作结束。
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) { mMeasuredWidth = measuredWidth; mMeasuredHeight = measuredHeight; mPrivateFlags |= MEASURED_DIMENSION_SET; }
对于非ViewGroup的View而言,通过调用上面默认的measure——>onMeasure,即可完成View的测量,当然你也可以重载onMeasure,并调用setMeasuredDimension来设置任意大小的布局,但一般不这么做。
对于ViewGroup的子类而言,往往会重载onMeasure函数负责其children的measure工作,重载时不要忘记调用setMeasuredDimension来设置自身的mMeasuredWidth和mMeasuredHeight。如果我们在layout的时候不需要依赖子视图的大小,那么不重载onMeasure也可以,但是必须重载onLayout来安排子视图的位置。
ViewGroup中定义了measureChildren, measureChild, measureChildWithMargins来对子视图进行测量,measureChildren内部只是循环调用measureChild,measureChild和measureChildWithMargins的区别就是是否把margin和padding也作为子视图的大小。
getChildMeasureSpec的总体思路就是通过其父视图提供的MeasureSpec参数得到specMode和specSize,并根据计算出来的specMode以及子视图的childDimension(layout_width和layout_height中定义的)来计算自身的measureSpec,如果其本身包含子视图,则计算出来的measureSpec将作为调用其子视图measure函数的参数,同时也作为自身调用setMeasuredDimension的参数,如果其不包含子视图则默认情况下最终会调用onMeasure的默认实现,并最终调用到setMeasuredDimension,而该函数的参数正是这里计算出来的。
总结:从上面的描述看出,决定权最大的就是View的设计者,因为设计者可以通过调用setMeasuredDimension决定视图的最终大小,例如调用setMeasuredDimension(100, 100)将视图的mMeasuredWidth和mMeasuredHeight设置为100,100,那么父视图提供的大小以及程序员在xml中设置的layout_width和layout_height将完全不起作用,当然良好的设计一般会根据子视图的measureSpec来设置mMeasuredWidth和mMeasuredHeight的大小,以尊重程序员的意图。
下面我们看一下具体的重写代码:
/** * 计算控件的大小 */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int measureWidth = measureWidth(0, widthMeasureSpec); int measureHeight = measureHeight(0, heightMeasureSpec); // 计算自定义的ViewGroup中所有子控件的大小 // 首先判断params.width的值是多少,有三种情况。 // // 如果是大于零的话,及传递的就是一个具体的值,那么,构造MeasupreSpec的时候可以直接用EXACTLY。 // // 如果为-1的话,就是MatchParent的情况,那么,获得父View的宽度,再用EXACTLY来构造MeasureSpec。 // // 如果为-2的话,就是wrapContent的情况,那么,构造MeasureSpec的话直接用一个负数就可以了。 // measureChildren(widthMeasureSpec, heightMeasureSpec); for (int i = 0; i < getChildCount(); i++) { View v = getChildAt(i); int widthSpec = 0; int heightSpec = 0; LayoutParams params = v.getLayoutParams(); if (params.width > 0) { widthSpec = MeasureSpec.makeMeasureSpec(params.width, MeasureSpec.EXACTLY); } else if (params.width == -1) { widthSpec = MeasureSpec.makeMeasureSpec(measureWidth, MeasureSpec.EXACTLY); } else if (params.width == -2) { widthSpec = MeasureSpec.makeMeasureSpec(measureWidth, MeasureSpec.AT_MOST); } if (params.height > 0) { heightSpec = MeasureSpec.makeMeasureSpec(params.height, MeasureSpec.EXACTLY); } else if (params.height == -1) { heightSpec = MeasureSpec.makeMeasureSpec(measureHeight, MeasureSpec.EXACTLY); } else if (params.height == -2) { heightSpec = MeasureSpec.makeMeasureSpec(measureWidth, MeasureSpec.AT_MOST); } v.measure(widthSpec, heightSpec); } // 设置自定义的控件MyViewGroup的大小 setMeasuredDimension(measureWidth, measureHeight); } private int measureWidth(int size, int pWidthMeasureSpec) { int result = size; int widthMode = MeasureSpec.getMode(pWidthMeasureSpec);// 得到模式 int widthSize = MeasureSpec.getSize(pWidthMeasureSpec);// 得到尺寸 switch (widthMode) { /** * 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方法传入的模式。 */ case MeasureSpec.AT_MOST: case MeasureSpec.EXACTLY: result = widthSize; break; } return result; } private int measureHeight(int size, int pHeightMeasureSpec) { int result = size; int heightMode = MeasureSpec.getMode(pHeightMeasureSpec); int heightSize = MeasureSpec.getSize(pHeightMeasureSpec); switch (heightMode) { case MeasureSpec.AT_MOST: case MeasureSpec.EXACTLY: result = heightSize; break; } return result; }
这是一个重写的简单例子,已经经过测试了。我再贴一下这个类的代码吧:
package com.example.component; import android.content.Context; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; public class MyLayout extends ViewGroup { // 三种默认构造器 <span style="white-space:pre"> </span>public MyLayout(Context context) { <span style="white-space:pre"> </span>super(context); <span style="white-space:pre"> </span>} public MyLayout(Context context, AttributeSet attrs) { super(context, attrs); } public MyLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } /** * 计算控件的大小 */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int measureWidth = measureWidth(0, widthMeasureSpec); int measureHeight = measureHeight(0, heightMeasureSpec); // 计算自定义的ViewGroup中所有子控件的大小 // 首先判断params.width的值是多少,有三种情况。 // // 如果是大于零的话,及传递的就是一个具体的值,那么,构造MeasupreSpec的时候可以直接用EXACTLY。 // // 如果为-1的话,就是MatchParent的情况,那么,获得父View的宽度,再用EXACTLY来构造MeasureSpec。 // // 如果为-2的话,就是wrapContent的情况,那么,构造MeasureSpec的话直接用一个负数就可以了。 // measureChildren(widthMeasureSpec, heightMeasureSpec); for (int i = 0; i < getChildCount(); i++) { View v = getChildAt(i); int widthSpec = 0; int heightSpec = 0; LayoutParams params = v.getLayoutParams(); if (params.width > 0) { widthSpec = MeasureSpec.makeMeasureSpec(params.width, MeasureSpec.EXACTLY); } else if (params.width == -1) { widthSpec = MeasureSpec.makeMeasureSpec(measureWidth, MeasureSpec.EXACTLY); } else if (params.width == -2) { widthSpec = MeasureSpec.makeMeasureSpec(measureWidth, MeasureSpec.AT_MOST); } if (params.height > 0) { heightSpec = MeasureSpec.makeMeasureSpec(params.height, MeasureSpec.EXACTLY); } else if (params.height == -1) { heightSpec = MeasureSpec.makeMeasureSpec(measureHeight, MeasureSpec.EXACTLY); } else if (params.height == -2) { heightSpec = MeasureSpec.makeMeasureSpec(measureWidth, MeasureSpec.AT_MOST); } v.measure(widthSpec, heightSpec); } // 设置自定义的控件MyLayout的大小 setMeasuredDimension(measureWidth, measureHeight); } private int measureWidth(int size, int pWidthMeasureSpec) { int result = size; int widthMode = MeasureSpec.getMode(pWidthMeasureSpec);// 得到模式 int widthSize = MeasureSpec.getSize(pWidthMeasureSpec);// 得到尺寸 switch (widthMode) { /** * 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方法传入的模式。 */ case MeasureSpec.AT_MOST: case MeasureSpec.EXACTLY: result = widthSize; break; } return result; } private int measureHeight(int size, int pHeightMeasureSpec) { int result = size; int heightMode = MeasureSpec.getMode(pHeightMeasureSpec); int heightSize = MeasureSpec.getSize(pHeightMeasureSpec); switch (heightMode) { case MeasureSpec.AT_MOST: case MeasureSpec.EXACTLY: result = heightSize; break; } return result; } /** * 覆写onLayout,其目的是为了指定视图的显示位置,方法执行的前后顺序是在onMeasure之后,因为视图肯定是只有知道大小的情况下, * 才能确定怎么摆放 */ @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { // 记录总高度 int mTotalHeight = 0; // 遍历所有子视图 int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { View childView = getChildAt(i); // 获取在onMeasure中计算的视图尺寸 int measureHeight = childView.getMeasuredHeight(); int measuredWidth = childView.getMeasuredWidth(); childView.layout(l, mTotalHeight, measuredWidth, mTotalHeight + measureHeight); mTotalHeight += measureHeight; } } }
希望大家能有所收获。所用到的知识上面已经讲过了,我对这部分知识目前理解的也还是不透彻,最近需要用到,从网上看了很多大神的文章边学边写的,等到我继续深入之后还会再给大家补充。
我也是学生,还请大家多多指教,这次篇幅有些长了,下一次如果时间充裕,我在学习onLayout方法和学习一些例子的时候还会和大家分享的!