您的位置:首页 > 其它

继承ViewGroup重写onMeasure方法的详解

2015-04-02 11:11 375 查看
我们继承重写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类时,需要重写两个方法,分别是onMeasureonLayout。重写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方法如下:

[java] view
plaincopy

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函数的源码:



[java] view
plaincopy

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的默认实现很简单,源码如下:

[java] view
plaincopy

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的测量工作结束。

[java] view
plaincopy

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的大小,以尊重程序员的意图。

下面我们看一下具体的重写代码:

[java] view
plaincopy

/**

* 计算控件的大小

*/

@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;

}

这是一个重写的简单例子,已经经过测试了。我再贴一下这个类的代码吧:

[java] view
plaincopy

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;

}

}

}

希望大家能有所收获。所用到的知识上面已经讲过了,我对这部分知识目前理解的也还是不透彻,最近需要用到,从网上看了很多大神的文章边学边写的,等到我继续深入之后还会再给大家补充。

本文转载自:http://blog.csdn.net/sunmc1204953974/article/details/38454267
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: