继承ViewGroup重写onMeasure方法的详解

我们继承重写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 plain copy
  1. 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 plain copy
  1. public final void measure(int widthMeasureSpec, int heightMeasureSpec) {   
  2.         if ((mPrivateFlags & FORCE_LAYOUT) == FORCE_LAYOUT ||   
  3.                 widthMeasureSpec != mOldWidthMeasureSpec ||   
  4.                 heightMeasureSpec != mOldHeightMeasureSpec) {   
  5.    
  6.             // first clears the measured dimension flag   
  7.             mPrivateFlags &= ~MEASURED_DIMENSION_SET;   
  8.    
  9.             if (ViewDebug.TRACE_HIERARCHY) {   
  10.                 ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_MEASURE);   
  11.             }   
  12.    
  13.             // measure ourselves, this should set the measured dimension flag back   
  14.             onMeasure(widthMeasureSpec, heightMeasureSpec);   
  15.    
  16.             // flag not set, setMeasuredDimension() was not invoked, we raise   
  17.             // an exception to warn the developer   
  18.             if ((mPrivateFlags & MEASURED_DIMENSION_SET) != MEASURED_DIMENSION_SET) {   
  19.                 throw new IllegalStateException("onMeasure() did not set the"   
  20.                         + " measured dimension by calling"   
  21.                         + " setMeasuredDimension()");   
  22.             }   
  23.    
  24.             mPrivateFlags |= LAYOUT_REQUIRED;   
  25.         }   
  26.    
  27.         mOldWidthMeasureSpec = widthMeasureSpec;   
  28.         mOldHeightMeasureSpec = heightMeasureSpec;   
  29.     }   


measure的过程是固定的,而measure中调用了onMeasure函数,因此真正有变数的是onMeasure函数,onMeasure的默认实现很简单,源码如下:


[java]  view plain copy
  1. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {   
  2.         setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),   
  3.                 getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));   
  4.     }   

    onMeasure默认的实现仅仅调用了setMeasuredDimension,setMeasuredDimension函数是一个很关键的函数,它对View的成员变量mMeasuredWidth和mMeasuredHeight变量赋值,而measure的主要目的就是对View树中的每个View的mMeasuredWidth和mMeasuredHeight进行赋值。一旦这两个变量被赋值,则意味着该View的测量工作结束。


[java]  view plain copy
  1. protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {   
  2.         mMeasuredWidth = measuredWidth;   
  3.         mMeasuredHeight = measuredHeight;   
  4.    
  5.         mPrivateFlags |= MEASURED_DIMENSION_SET;   
  6.     }   

    对于非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 plain copy
  1. /** 
  2.      * 计算控件的大小 
  3.      */  
  4.     @Override  
  5.     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  6.         super.onMeasure(widthMeasureSpec, heightMeasureSpec);  
  7.         int measureWidth = measureWidth(0, widthMeasureSpec);  
  8.         int measureHeight = measureHeight(0, heightMeasureSpec);  
  9.         // 计算自定义的ViewGroup中所有子控件的大小  
  10.         // 首先判断params.width的值是多少,有三种情况。  
  11.         //  
  12.         // 如果是大于零的话,及传递的就是一个具体的值,那么,构造MeasupreSpec的时候可以直接用EXACTLY。  
  13.         //  
  14.         // 如果为-1的话,就是MatchParent的情况,那么,获得父View的宽度,再用EXACTLY来构造MeasureSpec。  
  15.         //  
  16.         // 如果为-2的话,就是wrapContent的情况,那么,构造MeasureSpec的话直接用一个负数就可以了。  
  17.         // measureChildren(widthMeasureSpec, heightMeasureSpec);  
  18.         for (int i = 0; i < getChildCount(); i++) {  
  19.             View v = getChildAt(i);  
  20.   
  21.             int widthSpec = 0;  
  22.             int heightSpec = 0;  
  23.             LayoutParams params = v.getLayoutParams();  
  24.             if (params.width > 0) {  
  25.                 widthSpec = MeasureSpec.makeMeasureSpec(params.width,  
  26.                         MeasureSpec.EXACTLY);  
  27.             } else if (params.width == -1) {  
  28.                 widthSpec = MeasureSpec.makeMeasureSpec(measureWidth,  
  29.                         MeasureSpec.EXACTLY);  
  30.             } else if (params.width == -2) {  
  31.                 widthSpec = MeasureSpec.makeMeasureSpec(measureWidth,  
  32.                         MeasureSpec.AT_MOST);  
  33.             }  
  34.   
  35.             if (params.height > 0) {  
  36.                 heightSpec = MeasureSpec.makeMeasureSpec(params.height,  
  37.                         MeasureSpec.EXACTLY);  
  38.             } else if (params.height == -1) {  
  39.                 heightSpec = MeasureSpec.makeMeasureSpec(measureHeight,  
  40.                         MeasureSpec.EXACTLY);  
  41.             } else if (params.height == -2) {  
  42.                 heightSpec = MeasureSpec.makeMeasureSpec(measureWidth,  
  43.                         MeasureSpec.AT_MOST);  
  44.             }  
  45.             v.measure(widthSpec, heightSpec);  
  46.   
  47.         }  
  48.         // 设置自定义的控件MyViewGroup的大小  
  49.         setMeasuredDimension(measureWidth, measureHeight);  
  50.     }  
  51.   
  52.     private int measureWidth(int size, int pWidthMeasureSpec) {  
  53.         int result = size;  
  54.         int widthMode = MeasureSpec.getMode(pWidthMeasureSpec);// 得到模式  
  55.         int widthSize = MeasureSpec.getSize(pWidthMeasureSpec);// 得到尺寸  
  56.   
  57.         switch (widthMode) {  
  58.         /** 
  59.          * mode共有三种情况,取值分别为MeasureSpec.UNSPECIFIED, MeasureSpec.EXACTLY, 
  60.          * MeasureSpec.AT_MOST。 
  61.          *  
  62.          *  
  63.          * MeasureSpec.EXACTLY是精确尺寸, 
  64.          * 当我们将控件的layout_width或layout_height指定为具体数值时如andorid 
  65.          * :layout_width="50dip",或者为FILL_PARENT是,都是控件大小已经确定的情况,都是精确尺寸。 
  66.          *  
  67.          *  
  68.          * MeasureSpec.AT_MOST是最大尺寸, 
  69.          * 当控件的layout_width或layout_height指定为WRAP_CONTENT时 
  70.          * ,控件大小一般随着控件的子空间或内容进行变化,此时控件尺寸只要不超过父控件允许的最大尺寸即可 
  71.          * 。因此,此时的mode是AT_MOST,size给出了父控件允许的最大尺寸。 
  72.          *  
  73.          *  
  74.          * MeasureSpec.UNSPECIFIED是未指定尺寸,这种情况不多,一般都是父控件是AdapterView, 
  75.          * 通过measure方法传入的模式。 
  76.          */  
  77.         case MeasureSpec.AT_MOST:  
  78.         case MeasureSpec.EXACTLY:  
  79.             result = widthSize;  
  80.             break;  
  81.         }  
  82.         return result;  
  83.     }  
  84.   
  85.     private int measureHeight(int size, int pHeightMeasureSpec) {  
  86.         int result = size;  
  87.   
  88.         int heightMode = MeasureSpec.getMode(pHeightMeasureSpec);  
  89.         int heightSize = MeasureSpec.getSize(pHeightMeasureSpec);  
  90.   
  91.         switch (heightMode) {  
  92.         case MeasureSpec.AT_MOST:  
  93.         case MeasureSpec.EXACTLY:  
  94.             result = heightSize;  
  95.             break;  
  96.         }  
  97.         return result;  
  98.     }  

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

[java]  view plain copy
  1. package com.example.component;  
  2.   
  3. import android.content.Context;  
  4. import android.util.AttributeSet;  
  5. import android.view.View;  
  6. import android.view.ViewGroup;  
  7.   
  8. public class MyLayout extends ViewGroup {  
  9.   
  10. // 三种默认构造器  
  11. <span style="white-space:pre">    </span>public MyLayout(Context context) {  
  12. <span style="white-space:pre">        </span>super(context);  
  13. <span style="white-space:pre">    </span>}  
  14.   
  15.     public MyLayout(Context context, AttributeSet attrs) {  
  16.         super(context, attrs);  
  17.     }  
  18.   
  19.     public MyLayout(Context context, AttributeSet attrs, int defStyle) {  
  20.         super(context, attrs, defStyle);  
  21.     }  
  22.   
  23.     /** 
  24.      * 计算控件的大小 
  25.      */  
  26.     @Override  
  27.     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  28.         super.onMeasure(widthMeasureSpec, heightMeasureSpec);  
  29.         int measureWidth = measureWidth(0, widthMeasureSpec);  
  30.         int measureHeight = measureHeight(0, heightMeasureSpec);  
  31.         // 计算自定义的ViewGroup中所有子控件的大小  
  32.         // 首先判断params.width的值是多少,有三种情况。  
  33.         //  
  34.         // 如果是大于零的话,及传递的就是一个具体的值,那么,构造MeasupreSpec的时候可以直接用EXACTLY。  
  35.         //  
  36.         // 如果为-1的话,就是MatchParent的情况,那么,获得父View的宽度,再用EXACTLY来构造MeasureSpec。  
  37.         //  
  38.         // 如果为-2的话,就是wrapContent的情况,那么,构造MeasureSpec的话直接用一个负数就可以了。  
  39.         // measureChildren(widthMeasureSpec, heightMeasureSpec);  
  40.         for (int i = 0; i < getChildCount(); i++) {  
  41.             View v = getChildAt(i);  
  42.   
  43.             int widthSpec = 0;  
  44.             int heightSpec = 0;  
  45.             LayoutParams params = v.getLayoutParams();  
  46.             if (params.width > 0) {  
  47.                 widthSpec = MeasureSpec.makeMeasureSpec(params.width,  
  48.                         MeasureSpec.EXACTLY);  
  49.             } else if (params.width == -1) {  
  50.                 widthSpec = MeasureSpec.makeMeasureSpec(measureWidth,  
  51.                         MeasureSpec.EXACTLY);  
  52.             } else if (params.width == -2) {  
  53.                 widthSpec = MeasureSpec.makeMeasureSpec(measureWidth,  
  54.                         MeasureSpec.AT_MOST);  
  55.             }  
  56.   
  57.             if (params.height > 0) {  
  58.                 heightSpec = MeasureSpec.makeMeasureSpec(params.height,  
  59.                         MeasureSpec.EXACTLY);  
  60.             } else if (params.height == -1) {  
  61.                 heightSpec = MeasureSpec.makeMeasureSpec(measureHeight,  
  62.                         MeasureSpec.EXACTLY);  
  63.             } else if (params.height == -2) {  
  64.                 heightSpec = MeasureSpec.makeMeasureSpec(measureWidth,  
  65.                         MeasureSpec.AT_MOST);  
  66.             }  
  67.             v.measure(widthSpec, heightSpec);  
  68.   
  69.         }  
  70.         // 设置自定义的控件MyLayout的大小  
  71.         setMeasuredDimension(measureWidth, measureHeight);  
  72.     }  
  73.   
  74.     private int measureWidth(int size, int pWidthMeasureSpec) {  
  75.         int result = size;  
  76.         int widthMode = MeasureSpec.getMode(pWidthMeasureSpec);// 得到模式  
  77.         int widthSize = MeasureSpec.getSize(pWidthMeasureSpec);// 得到尺寸  
  78.   
  79.         switch (widthMode) {  
  80.         /** 
  81.          * mode共有三种情况,取值分别为MeasureSpec.UNSPECIFIED, MeasureSpec.EXACTLY, 
  82.          * MeasureSpec.AT_MOST。 
  83.          *  
  84.          *  
  85.          * MeasureSpec.EXACTLY是精确尺寸, 
  86.          * 当我们将控件的layout_width或layout_height指定为具体数值时如andorid 
  87.          * :layout_width="50dip",或者为FILL_PARENT是,都是控件大小已经确定的情况,都是精确尺寸。 
  88.          *  
  89.          *  
  90.          * MeasureSpec.AT_MOST是最大尺寸, 
  91.          * 当控件的layout_width或layout_height指定为WRAP_CONTENT时 
  92.          * ,控件大小一般随着控件的子空间或内容进行变化,此时控件尺寸只要不超过父控件允许的最大尺寸即可 
  93.          * 。因此,此时的mode是AT_MOST,size给出了父控件允许的最大尺寸。 
  94.          *  
  95.          *  
  96.          * MeasureSpec.UNSPECIFIED是未指定尺寸,这种情况不多,一般都是父控件是AdapterView, 
  97.          * 通过measure方法传入的模式。 
  98.          */  
  99.         case MeasureSpec.AT_MOST:  
  100.         case MeasureSpec.EXACTLY:  
  101.             result = widthSize;  
  102.             break;  
  103.         }  
  104.         return result;  
  105.     }  
  106.   
  107.     private int measureHeight(int size, int pHeightMeasureSpec) {  
  108.         int result = size;  
  109.   
  110.         int heightMode = MeasureSpec.getMode(pHeightMeasureSpec);  
  111.         int heightSize = MeasureSpec.getSize(pHeightMeasureSpec);  
  112.   
  113.         switch (heightMode) {  
  114.         case MeasureSpec.AT_MOST:  
  115.         case MeasureSpec.EXACTLY:  
  116.             result = heightSize;  
  117.             break;  
  118.         }  
  119.         return result;  
  120.     }  
  121.   
  122.     /** 
  123.      * 覆写onLayout,其目的是为了指定视图的显示位置,方法执行的前后顺序是在onMeasure之后,因为视图肯定是只有知道大小的情况下, 
  124.      * 才能确定怎么摆放 
  125.      */  
  126.     @Override  
  127.     protected void onLayout(boolean changed, int l, int t, int r, int b) {  
  128.         // 记录总高度  
  129.         int mTotalHeight = 0;  
  130.         // 遍历所有子视图  
  131.         int childCount = getChildCount();  
  132.         for (int i = 0; i < childCount; i++) {  
  133.             View childView = getChildAt(i);  
  134.   
  135.             // 获取在onMeasure中计算的视图尺寸  
  136.             int measureHeight = childView.getMeasuredHeight();  
  137.             int measuredWidth = childView.getMeasuredWidth();  
  138.   
  139.             childView.layout(l, mTotalHeight, measuredWidth, mTotalHeight  
  140.                     + measureHeight);  
  141.   
  142.             mTotalHeight += measureHeight;  
  143.   
  144.         }  
  145.     }  
  146. }  

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


本文转载自:http://blog.csdn.net/sunmc1204953974/article/details/38454267

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值