Android开发之自定义控件---onMeasure详解

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android开发之自定义控件---onMeasure详解相关的知识,希望对你有一定的参考价值。

通过本篇博客你将学到以下知识点:

①自定义控件onMeasure的过程

②彻底理解MeasureSpec

③了解View的绘制流程

④对测量过程中需要的谷歌工程师给我们准备好的其它的一些方法的源码深入理解。

        为了响应文章的开头,我们从一个“Hello World!”的小例子说起,这个例子我们自定义一个View让它显示“Hello World!”非常简单,代码如下

 

[java] view plain copy
 
  1. package com.example.customviewpractice;  
  2.   
  3. import android.content.Context;  
  4. import android.graphics.Canvas;  
  5. import android.graphics.Color;  
  6. import android.graphics.Paint;  
  7. import android.util.AttributeSet;  
  8. import android.view.View;  
  9.   
  10. public class CustomView1 extends View {  
  11.   
  12.     private Paint mPaint;  
  13.     private String str = "Hello World!";  
  14.   
  15.     public CustomView1(Context context, AttributeSet attrs) {  
  16.         super(context, attrs);  
  17.         init();  
  18.     }  
  19.   
  20.     private void init() {  
  21.         // 实例化一个画笔工具  
  22.         mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);  
  23.         // 设置字体大小  
  24.         mPaint.setTextSize(50);  
  25.         // 设置画笔颜色  
  26.         mPaint.setColor(Color.RED);  
  27.   
  28.     }  
  29.   
  30.     // 重写onMeasure方法  
  31.     @Override  
  32.     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  33.         super.onMeasure(widthMeasureSpec, heightMeasureSpec);  
  34.     }  
  35.   
  36.     // 重写onDraw方法  
  37.     @Override  
  38.     protected void onDraw(Canvas canvas) {  
  39.         super.onDraw(canvas);  
  40.         /** 
  41.          * getWidth() / 2 - mPaint.measureText(str) / 2让文字在水平方向居中 
  42.          */  
  43.         canvas.drawText(str, getWidth() / 2 - mPaint.measureText(str) / 2,  
  44.                 getHeight()/2, mPaint);  
  45.     }  
  46.   
  47. }  

 

它的布局文件如下

 

[html] view plain copy
 
  1. <LinearLayout  
  2.     xmlns:android="http://schemas.android.com/apk/res/android"  
  3.     xmlns:tools="http://schemas.android.com/tools"  
  4.     android:layout_width="match_parent"  
  5.     android:layout_height="match_parent"  
  6.     android:orientation="vertical" >  
  7.   
  8.     <com.example.customviewpractice.CustomView1  
  9.         android:id="@+id/cus_textview"  
  10.         android:layout_width="wrap_content"  
  11.         android:layout_height="wrap_content"  
  12.         android:background="@android:color/darker_gray" />  
  13.   
  14. </LinearLayout>  

运行结果如下

 

技术分享

         这样一个大大的"Hello World!"呈现在我们面前,可能有的人会问到底怎样去自定义一个控件呢?别急我们慢慢的,一点一点的去学习,首先你可以想象一下,假如我要求你去画一个空心的圆,你会怎么做,首先你要拿张白纸,然后你会问我圆的半径多大?圆的位置在哪?圆的线条颜色是什么?圆的线条粗细是多少?等我把这些问题都告诉你之后,你就会明白要求,并按照这个要求去画一个圆。我们自定义控件呢,也是这样需要下面三个步骤:

①重写onMeasure(获得半径的大小)

②重写onLayout(获得圆的位置)

③重写onDraw(用实例化的画笔包括:颜色,粗细等去绘画)

待这三个方法都重写完后我们的自定义控件就完成了,为了讲的能够详细我们这一篇专门来讲解onMeasure以及和其相关的方法,首先我们需要明白的是Android给我提供了可以操纵控件测量的方法是onMeasure()方法,在上面的自定义控件中我们采用了其默认的实现

 

[java] view plain copy
 
  1.       @Override  
  2. rotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  3. super.onMeasure(widthMeasureSpec, heightMeasureSpec);  

看到这,可能大部分人都要问,这里的widthMeasureSpec和heightMeasureSpec是从何处来?要到哪里去?其实这两个参数是由View的父容器传递过来的测量要求,在上述自定义控件中也就是我们的LinearLayout,为什么这么说?这么说是有依据的我们都知道在Activity中可以调用其setContentView方法为界面填充一个布局

 

 

[java] view plain copy
 
  1. protected void onCreate(Bundle savedInstanceState) {  
  2.        super.onCreate(savedInstanceState);  
  3.        setContentView(R.layout.activity_main);  
  4.    }  

在setContentView方法中做了哪些事情呢?我们看看他的源码

 

 

[java] view plain copy
 
  1. public void setContentView(int layoutResID) {  
  2.         getWindow().setContentView(layoutResID);  
  3.     }  

我们看到它调用了getWindow方法,没什么可说的,跟着步骤去看getWindow方法的源码

 

 

[java] view plain copy
 
  1. public Window getWindow() {  
  2.         return mWindow;  
  3.     }  

 

这里返回一个Window实例,其本质是继承Window的PhoneWindow,所以在Acitivity中的setContentView中getWindow.setContentView()getWindow.setContentView()其实就是PhoneWindow.setContentView()我们来Look Look它的代码

 

[java] view plain copy
 
  1. public void setContentView(int layoutResID) {  
  2.         if (mContentParent == null) {  
  3.             installDecor();  
  4.         } else {  
  5.             mContentParent.removeAllViews();  
  6.         }  
  7.         mLayoutInflater.inflate(layoutResID, mContentParent);  
  8.         final Callback cb = getCallback();  
  9.         if (cb != null) {  
  10.             cb.onContentChanged();  
  11.         }  
  12.     }  

该方法首先会判断是否是第一次调用setContentView方法,如果是第一次调用则调用installDecor()方法,否则将mContentParent中的所有View移除掉

 

然后调用LayoutInflater将我们的布局文件加载进来并添加到mContentParent视图中。跟上节奏我们来看看installDecor()方法的源码

 

[java] view plain copy
 
  1. private void installDecor() {  
  2.         if (mDecor == null) {  
  3.             //mDecor为空,则创建一个Decor对象  
  4.             mDecor = generateDecor();  
  5.             mDecor.setIsRootNamespace(true);  
  6.         }  
  7.         if (mContentParent == null) {  
  8.             //generateLayout()方法会根据窗口的风格修饰,选择对应的修饰布局文件    
  9.             //并且将id为content(android:id="@+id/content")的FrameLayout赋值给mContentParent    
  10.             mContentParent = generateLayout(mDecor);  
  11.                           。。。省略部分代码。。。  
  12.         }  
  13.     }  

 

可以发现在这个方法中首先会去判断mDecor是否为空如果为空会调用generateDecor方法,它干了什么呢?

 

[java] view plain copy
 
  1. protected DecorView generateDecor() {  
  2.         return new DecorView(getContext(), -1);  
  3.     }  

可以看到它返回了一个DecorView,DecorView类是FrameLayout的子类,是一个内部类存在于PhoneWindow类中,这里我们知道它是FrameLayout的子类就ok了。

 

在installDecor方法中判断了mDecor是否为空后,接着会在该方法中判断mContentParent是否为空,如果为空就会调用generateLayout方法,我们来看看它做了什么。。。

 

[java] view plain copy
 
  1. protected ViewGroup generateLayout(DecorView decor) {  
  2.             。。。省略部分代码。。。  
  3.             View in = mLayoutInflater.inflate(layoutResource, null);  
  4.             decor.addView(in, new ViewGroup.LayoutParams(FILL_PARENT, FILL_PARENT));  
  5.             ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);  
  6.             。。。省略部分代码。。。  
  7.             return contentParent;  
  8.         }  

 

根据窗口的风格修饰类型为该窗口选择不同的窗口布局文件(根视图),这些窗口修饰布局文件指定一个用来存放Activity自定义布局文件的ViewGroup视图,一般为FrameLayout 其id 为: android:id="@android:id/content",并将其赋给mContentParent,到这里mContentParent和mDecor均已生成,而我们xml布局文件中的布局则会被添加至mContentParent。接着对上面的过程做一个简单的总结如下图

技术分享

我们用张图来说明下层次结构

技术分享

注:此图引自http://blog.csdn.net/qinjuning/article/details/7226787这位大神的博客。

所以说实际上我们在写xml布局文件的时候我们的根布局并不是我们能在xml文件中能看到的最上面的那个,而是FrameLayout,我们再用谷歌给我提供的hierarchyviewer这个工具来看看我们最开始的那个小例子的布局情况,看完你就明白了

技术分享
看到了吧,在LinearLayout的上面是FrameLayout。到这可能有的人会说你这是写的啥?跟自定义控件一点关系都没有,其实只有了解了上面过程我们才能更好的去理解自定义控件
到这里我们回到最初我们提出的问题widthMeasureSpec和heightMeasureSpec是从哪来?我们在上面提到是从其父View传递过来的,那么它的父View的这两个参数又是从哪来,这样一步一步我们就需要知道View绘制的时候是从儿开始的,其实担任此重任的是ViewRootImpl,绘制开始是从ViewRootImpl中的performTraversals()这个方法开始的,我们来看看源码,可能有的人会说又看源码,只有看源码才能学的更透彻,这里我们只看主要的代码,理解其流程即可,其实performTraversals()方法的代码很多,我们省略后如下

 

[java] view plain copy
 
  1. private void performTraversals() {  
  2.            
  3.              int desiredWindowWidth;  
  4.          int desiredWindowHeight;  
  5.          int childWidthMeasureSpec;  
  6.          int childHeightMeasureSpec;  
  7.            
  8.          。。。省略部分代码。。。  
  9.            
  10.              DisplayMetrics packageMetrics =  
  11.          mView.getContext().getResources().getDisplayMetrics();  
  12.          desiredWindowWidth = packageMetrics.widthPixels;  
  13.          desiredWindowHeight = packageMetrics.heightPixels;  
  14.            
  15.          。。。省略部分代码。。。  
  16.                    
  17.            childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);  
  18.            childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);  
  19.   
  20.         。。。省略部分代码。。。  
  21.                   
  22.        host.measure(childWidthMeasureSpec, childHeightMeasureSpec);  
  23.           
  24.          。。。省略部分代码。。。  
  25.                   
  26.         }  

我们清楚的看到在此调用了getRootMeasureSpec方法后会得到childWidthMeasureSpec和childHeightMeasureSpec,得到的这个数据作为参数传给host(这里的host是View)measure方法。在调用getRootMeasureSpec时需要两个参数desiredWindowWidth ,lp.width和desiredWindowHeight  , lp.height这里我们看到desiredWindowWidth 和desiredWindowHeight就是我们窗口的大小而lp.width和lp.height均为MATCH_PARENT,其在mWindowAttributes(WindowManager.LayoutParams类型)将值赋予给lp时就已被确定。参数搞明白后我们来看看getRootMeasureSpec的源码,看看它都是干了个啥。

 

 

[java] view plain copy
 
  1. /** 
  2.  * Figures out the measure spec for the root view in a window based on it‘s 
  3.  * layout params. 
  4.  * 
  5.  * @param windowSize 
  6.  *            The available width or height of the window 
  7.  * 
  8.  * @param rootDimension 
  9.  *            The layout params for one dimension (width or height) of the 
  10.  *            window. 
  11.  * 
  12.  * @return The measure spec to use to measure the root view. 
  13.  */  
  14. private int getRootMeasureSpec(int windowSize, int rootDimension) {  
  15.     int measureSpec;  
  16.     switch (rootDimension) {  
  17.   
  18.     case ViewGroup.LayoutParams.FILL_PARENT:  
  19.         // Window can‘t resize. Force root view to be windowSize.  
  20.         measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);  
  21.         break;  
  22.     case ViewGroup.LayoutParams.WRAP_CONTENT:  
  23.         // Window can resize. Set max size for root view.  
  24.         measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);  
  25.         break;  
  26.     default:  
  27.         // Window wants to be an exact size. Force root view to be that size.  
  28.         measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);  
  29.         break;  
  30.     }  
  31.     return measureSpec;  
  32. }  

上面三种情况的英文注释很简单自己翻译下即可理解。总之这个方法执行后不管是哪一种情况我们的根视图都是全屏的。在上面中大家看到MeasureSpec这个类有点陌生,MeasureSpec这个类的设计很精妙,对于学习自定义View也非常重要,理解它对于学习自定义控件非常有用接下来我们就花点篇幅来详细的讲解一下这个类,measurespec封装了父类传递给子类的测量要求,每个measurespec代表宽度或者高度的要求以及大小,也就是说一个measurespec包含size和mode。它有三种mode(模式)
 ①UNSPECIFIED:父View没有对子View施加任何约束。它可以是任何它想要的大小。 
 ②EXACTLY:父View已经确定了子View的确切尺寸。子View将被限制在给定的界限内,而忽略其本身的大小。 

 ③AT_MOST:子View的大小不能超过指定的大小

 

它有三个主要的方法:

getMode(imeasureSpec)它的作用就是根据规格提取出mode,这里的mode是上面的三种模式之一

getSize(int measureSpec)它的作用就是根据规格提取出size,这里的size就是我们所说的大小

makeMeasureSpec(int size, int mode)根据size和mode,创建一个测量要求。

说了这些可能大家仍然是一头雾水接下来我们看看它的源码,MeasureSpec是View的内部类,它的源码如下

 

[java] view plain copy
 
  1. public static class MeasureSpec {  
  2.         private static final int MODE_SHIFT = 30;  
  3.         //转化为二进制就是11向左移30位,其结果为:11 0000...(11后跟30个0)  
  4.         private static final int MODE_MASK  = 0x3 << MODE_SHIFT;  
  5.         /** 
  6.          * 下面就是MeasureSpec的三种模式 
  7.          */  
  8.         //0左移30位变为 :00  0000...(00后跟30个0)  
  9.         public static final int UNSPECIFIED = 0 << MODE_SHIFT;  
  10.         //01左移30位变为:01 0000...(01后跟30个0)  
  11.         public static final int EXACTLY     = 1 << MODE_SHIFT;  
  12.         //10左移30位变为:10 0000...(10后跟30个0)  
  13.         public static final int AT_MOST     = 2 << MODE_SHIFT;  
  14.   
  15.         //创建一个测量的规格其高位的前两位代表mode,后面30为代表size,即measureSpec=size+mode;  
  16.         public static int makeMeasureSpec(int size, int mode) {  
  17.             return size + mode;  
  18.         }  
  19.   
  20.         //与运算获得mode,这里为什么可以得到mode?因为从measureSpec=size+mode,而MODE_MASK=11 0000...(11后跟30个0)  
  21.         //我们都知道   & 运算的规则是"遇0为0,遇1不变",而MODE_MASK的前两位为11后面30为全为0,这样进行运算后就可以得到measureSpec的前两位,而刚好  
  22.         //这前两位就代表了mode。  
  23.         public static int getMode(int measureSpec) {  
  24.             return (measureSpec & MODE_MASK);  
  25.         }  
  26.         //这里的思想跟getMode方法是一样的,首先对MODE_MASK进行取反,得到的结果为00 1111...(00后跟30个1)& 运算的规则是"遇0为0,遇1不变",而此时~MODE_MASK  
  27.         //的前两位为0后面30为全为1,所以measureSpec&~MODE_MASK得到的结果去后面30位,这后面的30位就是我们的size  
  28.         public static int getSize(int measureSpec) {  
  29.             return (measureSpec & ~MODE_MASK);  
  30.         }  
  31.   
  32.         public static String toString(int measureSpec) {  
  33.             。。。内容省略。。。  
  34.         }  
  35.     }  

 

MeasureSpec这个类的设计是非常巧妙的,用int类型占有32位,它将其高2位作为mode,后30为作为size这样用32位就解决了size和mode的问题

看完的它的源码大家可能似懂非懂,那么我们就举个例子画个图,让你彻底理解它的设计思想。

假如现在我们的mode是EXACTLY,而size=101(5)那么size+mode的值为:

技术分享

这时候通过size+mode构造除了MeasureSpec对象及测量要求,当需要获得Mode的时候只需要用measureSpec与MODE_TASK相与即可如下图

技术分享

我们看到得到的值就是上面的mode,而如果想获得size的话只需要只需要measureSpec与~MODE_TASK相与即可如下图

技术分享

我们看到得到值就是上面的size。关于这个设计思想大家好好的,慢慢的体会下。

 

好了到这里我们应该对MeasureSpec有了一定的理解。这时返回去看看我们的getRootMeasureSpec方法,你是不是能看懂了?看懂后回到performTraversals方法,通过getRootMeasureSpec方法得到childWidthMeasureSpec和childHeightMeasureSpec后,我们看到在performTraversals方法中会调用host.measure(childWidthMeasureSpec,childHeightMeasureSpec),这样childWidthMeasureSpec和childHeightMeasureSpec这两个测量要求就一步一步的传下去并由当前View与其父容器共同决定其测量大小,在这里View与ViewGroup中的递归调用过程中有几个重要的方法,而对于View是measure方法,接着我们看看host.measure也就是View的measure方法的源码吧

 

[java] view plain copy
 
  1. public class View implements ... {  
  2.           
  3.     。。。省略了部分代码。。。  
  4.       
  5.      public final void measure(int widthMeasureSpec, int heightMeasureSpec) {  
  6.             //判断是否为强制布局,即带有“FORCE_LAYOUT”标记 以及 widthMeasureSpec或heightMeasureSpec发生了改变  
  7.             if ((mPrivateFlags & FORCE_LAYOUT) == FORCE_LAYOUT ||  
  8.                     widthMeasureSpec != mOldWidthMeasureSpec ||  
  9.                     heightMeasureSpec != mOldHeightMeasureSpec) {  
  10.                 //清除MEASURED_DIMENSION_SET标记   ,该标记会在onMeasure()方法后被设置  
  11.                 mPrivateFlags &= ~MEASURED_DIMENSION_SET;  
  12.   
  13.                 if (ViewDebug.TRACE_HIERARCHY) {  
  14.                     ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_MEASURE);  
  15.                 }  
  16.                 // 1、 测量该View本身的大小  
  17.                 onMeasure(widthMeasureSpec, heightMeasureSpec);  
  18.   
  19.                 if ((mPrivateFlags & MEASURED_DIMENSION_SET) != MEASURED_DIMENSION_SET) {  
  20.                     throw new IllegalStateException("onMeasure() did not set the"  
  21.                             + " measured dimension by calling"  
  22.                             + " setMeasuredDimension()");  
  23.                 }  
  24.               //下一步是layout了,添加LAYOUT_REQUIRED标记  
  25.                 mPrivateFlags |= LAYOUT_REQUIRED;  
  26.             }  
  27.             mOldWidthMeasureSpec = widthMeasureSpec;//保存值  
  28.             mOldHeightMeasureSpec = heightMeasureSpec;//保存值  
  29.         }     
  30.       
  31.     }  

看到了吧,在measure方法中调用了onMeasure方法,你是不是应该笑30分钟?终于见到我们的onMeasure方法了,这里的onMeasure就是我们重写的onMeasure,它接收两个参数widthMeasureSpec和heightMeasureSpec这两个参数由父View构建,表示父View对子View的测量要求。它有它的默认实现,即重写后我们什么都不做直接调用super.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方法,在这里它会调用getSuggestedMinimumWidth方法得到的数据传递给getDefaultSize方法,首先来看看getSuggestedMinimunWidth,getDefaultSize以及setMeasuredDimension这三个方法的源码吧

 

 

[java] view plain copy
 
  1. protected int getSuggestedMinimumWidth() {  
  2.         //获得android:minHeight这个属性的值,一般不设置此属性如果没有设置的话mMinWidth=0  
  3.         int suggestedMinWidth = mMinWidth;  
  4.         if (mBGDrawable != null) {  
  5.             //获得背景的宽度  
  6.             final int bgMinWidth = mBGDrawable.getMinimumWidth();  
  7.             //从背景的宽度和minHeight属性中选出一个最大的值作为返回值  
  8.             if (suggestedMinWidth < bgMinWidth) {  
  9.                 suggestedMinWidth = bgMinWidth;  
  10.             }  
  11.         }  
  12.         return suggestedMinWidth;  
  13.     }  
  14.     //在这里这里size是getSuggestedMinimumWidth方法的返回值,这也是默认的大小  
  15.     //measureSpec是父View传过来的measureSpec,测量要求  
  16.     public static int getDefaultSize(int size, int measureSpec) {  
  17.         int result = size;  
  18.         //获得测量的模式  
  19.         int specMode = MeasureSpec.getMode(measureSpec);  
  20.         //获得测量的大小  
  21.         int specSize =  MeasureSpec.getSize(measureSpec);  
  22.   
  23.         switch (specMode) {  
  24.         //模式为Unspecified及未指定大小  
  25.         case MeasureSpec.UNSPECIFIED:  
  26.             //将上面的size作为结果返回  
  27.             result = size;  
  28.             break;  
  29.         case MeasureSpec.AT_MOST://模式为At_Most,此时使用默认的大小size  
  30.         case MeasureSpec.EXACTLY://模式为Exactly,此时返回测量值  
  31.             result = specSize;  
  32.             break;  
  33.         }  
  34.         return result;  
  35.     }  
  36.     //为View设置宽和高  
  37.     protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {  
  38.         mMeasuredWidth = measuredWidth;  
  39.         mMeasuredHeight = measuredHeight;  
  40.   
  41.         mPrivateFlags |= MEASURED_DIMENSION_SET;  
  42.     }  

这只是一个自定义View的默认实现,如果想按照我们的要求来进行绘制的话,重写onMeasure需要添加我们自己的逻辑去实现,最终在onMeasure方法中会调用setMeasureDimenSion决定我们的View的大小,这也是我们重写onMeasure方法的最终目的。

 

上面这些是对于一个View的测量,android中在进行测量时有两种情况,一种是一个View如Button,ImaeView这中,不能包含子View的对于这种测量一下就ok了,另外一种就是ViewGroup像LinearLayout,FrameLayout这种可以包含子View的,对于这种我们就需要循环遍历每一个子View并为其设置大小,在自定义的ViewGroup中重写onMeasure如下的伪代码

 

[java] view plain copy
 
  1. //某个ViewGroup类型的视图    
  2.     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {    
  3.       //必须调用super.ononMeasure()或者直接调用setMeasuredDimension()方法设置该View大小,否则会报异常。    
  4.       super.onMeasure(widthMeasureSpec , heightMeasureSpec)    
  5.          //setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),    
  6.          //        getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));    
  7.              
  8.       //一、遍历每个子View    
  9.       for(int i = 0 ; i < getChildCount() ; i++){    
  10.         View child = getChildAt(i);    
  11.         //调用子View的onMeasure,设置他们的大小,  
  12.         child.onMeasure(childWidthMeasureSpec, childHeightMeasureSpec);    
  13.       }    
  14.       //二、直接调用ViewGroup中给我们提供好的measureChildren方法、  
  15.       measureChildren(widthMeasureSpec, heightMeasureSpec);  
  16.     }  

其实ViewGroup已经为我们提供了测量子View的方法,主要有measureChildren,measureChild和getMeasureSpec,下面我们来分别看看这三个方法都是干了个啥?

 

measureChildren方法的源码如下

 

[java] view plain copy
 
  1. //widthMeasureSpec和heightMeasureSpec:父View传过来的测量要求  
  2.     protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {  
  3.         final int size = mChildrenCount;  
  4.         final View[] children = mChildren;  
  5.         //遍历所有的View  
  6.         for (int i = 0; i < size; ++i) {  
  7.             final View child = children[i];  
  8.             //Gone掉的View排除  
  9.             if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {  
  10.                 measureChild(child, widthMeasureSpec, heightMeasureSpec);  
  11.             }  
  12.         }  
  13.     }  

可以看到在measureChildren方法中会遍历所有的View然后对每一个View(不包括gone的View)调用measureChild方法,顺其自然我们来看看measureChild方法的源码

 

 

[java] view plain copy
 
  1. protected void measureChild(View child, int parentWidthMeasureSpec,  
  2.             int parentHeightMeasureSpec) {  
  3.         // 获取子元素的布局参数  
  4.         final LayoutParams lp = child.getLayoutParams();  
  5.         //将父容器的测量规格以及上下和左右的边距还有子元素本身的布局参数传入getChildMeasureSpec方法计算最终测量要求   
  6.         final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,  
  7.                 mPaddingLeft + mPaddingRight, lp.width);  
  8.         final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,  
  9.                 mPaddingTop + mPaddingBottom, lp.height);  
  10.         // 将计算好的宽高详细测量值传入measure方法,完成最后的测量  
  11.         child.measure(childWidthMeasureSpec, childHeightMeasureSpec);  
  12.     }  

在measureChild方法中通过getChildMeasureSpec得到最终的测量要求,并将这个测量要求传递给childView的measure方法,就会按照View的那一套逻辑运行。在这里看到调用了getChildMeasureSpec方法我们来看看这个方法的源码

 

 

[java] view plain copy
 
  1. public static int getChildMeasureSpec(int spec, int padding, int childDimension) {  
  2.         //获取父View的测量模式  
  3.         int specMode = MeasureSpec.getMode(spec);  
  4.         //获取父View的测量大小  
  5.         int specSize = MeasureSpec.getSize(spec);  
  6.        //父View计算出的子View的大小,子View不一定用这个值  
  7.         int size = Math.max(0, specSize - padding);  
  8.         //声明变量用来保存实际计算的到的子View的size和mode即大小和模式  
  9.         int resultSize = 0;  
  10.         int resultMode = 0;  
  11.   
  12.         switch (specMode) {  
  13.          // Parent has imposed an exact size on us  
  14.         //如果父容器的模式是Exactly即确定的大小  
  15.         case MeasureSpec.EXACTLY:  
  16.             //子View的高度或宽度>0说明其实一个确切的值,因为match_parent和wrap_content的值是<0的  
  17.             if (childDimension >= 0) {  
  18.                 resultSize = childDimension;  
  19.                 resultMode = MeasureSpec.EXACTLY;  
  20.                 //子View的高度或宽度为match_parent  
  21.             } else if (childDimension == LayoutParams.MATCH_PARENT) {  
  22.                 // Child wants to be our size. So be it.  
  23.                 resultSize = size;//将size即父View的大小减去边距值所得到的值赋值给resultSize  
  24.                 resultMode = MeasureSpec.EXACTLY;//指定子View的测量模式为EXACTLY  
  25.                //子View的高度或宽度为wrap_content  
  26.             } else if (childDimension == LayoutParams.WRAP_CONTENT) {  
  27.                 // Child wants to determine its own size. It can‘t be  
  28.                 // bigger than us.  
  29.                 resultSize = size;//将size赋值给result  
  30.                 resultMode = MeasureSpec.AT_MOST;//指定子View的测量模式为AT_MOST  
  31.             }  
  32.             break;  
  33.          // Parent has imposed a maximum size on us  
  34.         //如果父容器的测量模式是AT_MOST  
  35.         case MeasureSpec.AT_MOST:  
  36.             if (childDimension >= 0) {  
  37.                 // Child wants a specific size... so be it  
  38.                 resultSize = childDimension;  
  39.                 resultMode = MeasureSpec.EXACTLY;  
  40.             } else if (childDimension == LayoutParams.MATCH_PARENT) {  
  41.                 // Child wants to be our size, but our size is not fixed.  
  42.                 // Constrain child to not be bigger than us.  
  43.                 resultSize = size;  
  44.                 // 因为父View的大小是受到限制值的限制,所以子View的大小也应该受到父容器的限制并且不能超过父View    
  45.                 resultMode = MeasureSpec.AT_MOST;  
  46.             } else if (childDimension == LayoutParams.WRAP_CONTENT) {  
  47.                  // Child wants to determine its own size. It can‘t be  
  48.                 // bigger than us.  
  49.                 resultSize = size;  
  50.                 resultMode = MeasureSpec.AT_MOST;  
  51.             }  
  52.             break;  
  53.          // Parent asked to see how big we want to be  
  54.         //如果父容器的测量模式是UNSPECIFIED即父容器的大小未受限制  
  55.         case MeasureSpec.UNSPECIFIED:  
  56.             //如果自View的宽和高是一个精确的值  
  57.             if (childDimension >= 0) {  
  58.                  // Child wants a specific size... let him have it  
  59.                 //子View的大小为精确值  
  60.                 resultSize = childDimension;  
  61.                 //测量的模式为EXACTLY  
  62.                 resultMode = MeasureSpec.EXACTLY;  
  63.                 //子View的宽或高为match_parent  
  64.             } else if (childDimension == LayoutParams.MATCH_PARENT) {  
  65.                 // Child wants to be our size... find out how big it should  
  66.                 // be  
  67.                 //resultSize=0;因为父View的大小是未定的,所以子View的大小也是未定的  
  68.                 resultSize = 0;  
  69.                 resultMode = MeasureSpec.UNSPECIFIED;  
  70.             } else if (childDimension == LayoutParams.WRAP_CONTENT) {  
  71.                 // Child wants to determine its own size.... find out how  
  72.                 // big it should be  
  73.                   
  74.                 resultSize = 0;  
  75.                 resultMode = MeasureSpec.UNSPECIFIED;  
  76.             }  
  77.             break;  
  78.         }  
  79.         //根据resultSize和resultMode调用makeMeasureSpec方法得到测量要求,并将其作为返回值  
  80.         return MeasureSpec.makeMeasureSpec(resultSize, resultMode);  
  81.     }  

我们经常说View的大小是由父View以及当前View共同决定的,这一点从上面这个方法也可以看出。但是这只是一个期望的大小,其大小的最终决定权由setMeasureDimenSion方法决定。

所以最终View的大小将受以下几个方面的影响(以下三点摘自:http://blog.csdn.net/qinjuning/article/details/8074262此博客,这是一个大神。。)

 1、父View的MeasureSpec属性;

 2、子View的LayoutParams属性;

 3、setMeasuredDimension()或者其它类似设定 mMeasuredWidth 和 mMeasuredHeight 值的方法。

关于View的测量过程就介绍完了,可能你一遍没有读懂,只要你认真的去看我相信你一定会有收获,如果你一遍就读懂了,千万别告诉我,我会伤心的,哈哈,因为我花了一周的时间才对onMeasure有了点理解。







以上是关于Android开发之自定义控件---onMeasure详解的主要内容,如果未能解决你的问题,请参考以下文章

android开发笔记之自定义开关按钮

Android自助餐之自定义控件从layout自定义控件

Android自定义View实战之自定义评价打分控件RatingBar,可以自定义星星大小和间距

android开发之自定义圆形ImagView

Android进阶之自定义View实战仿iOS UISwitch控件实现

Android进阶之自定义View实战仿iOS UISwitch控件实现