现在的位置: 首页 > 综合 > 正文

Android视图绘制流程完全解析,带你一步步深入了解View(二)

2014年02月07日 ⁄ 综合 ⁄ 共 5511字 ⁄ 字号 评论关闭

转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/16330267

在上一篇文章中,我带着大家一起剖析了一下LayoutInflater的工作原理,可以算是对View进行深入了解的第一步吧。那么本篇文章中,我们将继续对View进行深入探究,看一看它的绘制流程到底是什么样的。如果你还没有看过我的上一篇文章,可以先去阅读 Android
LayoutInflater原理分析,带你一步步深入了解View(一) 
 。

相信每个Android程序员都知道,我们每天的开发工作当中都在不停地跟View打交道,Android中的任何一个布局、任何一个控件其实都是直接或间接继承自View的,如TextView、Button、ImageView、ListView等。这些控件虽然是Android系统本身就提供好的,我们只需要拿过来使用就可以了,但你知道它们是怎样被绘制到屏幕上的吗?多知道一些总是没有坏处的,那么我们赶快进入到本篇文章的正题内容吧。

要知道,任何一个视图都不可能凭空突然出现在屏幕上,它们都是要经过非常科学的绘制流程后才能显示出来的。每一个视图的绘制过程都必须经历三个最主要的阶段,即onMeasure()、onLayout()和onDraw(),下面我们逐个对这三个阶段展开进行探讨。

一. onMeasure()

measure是测量的意思,那么onMeasure()方法顾名思义就是用于测量视图的大小的。View系统的绘制流程会从ViewRoot的performTraversals()方法中开始,在其内部调用View的measure()方法。measure()方法接收两个参数,widthMeasureSpec和heightMeasureSpec,这两个值分别用于确定视图的宽度和高度的规格和大小。

MeasureSpec的值由specSize和specMode共同组成的,其中specSize记录的是大小,specMode记录的是规格。specMode一共有三种类型,如下所示:

1. EXACTLY

表示父视图希望子视图的大小应该是由specSize的值来决定的,系统默认会按照这个规则来设置子视图的大小,开发人员当然也可以按照自己的意愿设置成任意的大小。

2. AT_MOST

表示子视图最多只能是specSize中指定的大小,开发人员应该尽可能小得去设置这个视图,并且保证不会超过specSize。系统默认会按照这个规则来设置子视图的大小,开发人员当然也可以按照自己的意愿设置成任意的大小。

3. UNSPECIFIED

表示开发人员可以将视图按照自己的意愿设置成任意的大小,没有任何限制。这种情况比较少见,不太会用到。

那么你可能会有疑问了,widthMeasureSpec和heightMeasureSpec这两个值又是从哪里得到的呢?通常情况下,这两个值都是由父视图经过计算后传递给子视图的,说明父视图会在一定程度上决定子视图的大小。但是最外层的根视图,它的widthMeasureSpec和heightMeasureSpec又是从哪里得到的呢?这就需要去分析ViewRoot中的源码了,观察performTraversals()方法可以发现如下代码:

[java] view
plain
copy在CODE上查看代码片派生到我的代码片

  1. childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);  
  2. childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);  

可以看到,这里调用了getRootMeasureSpec()方法去获取widthMeasureSpec和heightMeasureSpec的值,注意方法中传入的参数,其中lp.width和lp.height在创建ViewGroup实例的时候就被赋值了,它们都等于MATCH_PARENT。然后看下getRootMeasureSpec()方法中的代码,如下所示:

[java] view
plain
copy在CODE上查看代码片派生到我的代码片

  1. private int getRootMeasureSpec(int windowSize, int rootDimension) {  
  2.     int measureSpec;  
  3.     switch (rootDimension) {  
  4.     case ViewGroup.LayoutParams.MATCH_PARENT:  
  5.         measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);  
  6.         break;  
  7.     case ViewGroup.LayoutParams.WRAP_CONTENT:  
  8.         measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);  
  9.         break;  
  10.     default:  
  11.         measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);  
  12.         break;  
  13.     }  
  14.     return measureSpec;  
  15. }  

可以看到,这里使用了MeasureSpec.makeMeasureSpec()方法来组装一个MeasureSpec,当rootDimension参数等于MATCH_PARENT的时候,MeasureSpec的specMode就等于EXACTLY,当rootDimension等于WRAP_CONTENT的时候,MeasureSpec的specMode就等于AT_MOST。并且MATCH_PARENT和WRAP_CONTENT时的specSize都是等于windowSize的,也就意味着根视图总是会充满全屏的。

介绍了这么多MeasureSpec相关的内容,接下来我们看下View的measure()方法里面的代码吧,如下所示:

[java] view
plain
copy在CODE上查看代码片派生到我的代码片

  1. public final void measure(int widthMeasureSpec, int heightMeasureSpec) {  
  2.     if ((mPrivateFlags & FORCE_LAYOUT) == FORCE_LAYOUT ||  
  3.             widthMeasureSpec != mOldWidthMeasureSpec ||  
  4.             heightMeasureSpec != mOldHeightMeasureSpec) {  
  5.         mPrivateFlags &= ~MEASURED_DIMENSION_SET;  
  6.         if (ViewDebug.TRACE_HIERARCHY) {  
  7.             ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_MEASURE);  
  8.         }  
  9.         onMeasure(widthMeasureSpec, heightMeasureSpec);  
  10.         if ((mPrivateFlags & MEASURED_DIMENSION_SET) != MEASURED_DIMENSION_SET) {  
  11.             throw new IllegalStateException("onMeasure() did not set the"  
  12.                     + " measured dimension by calling"  
  13.                     + " setMeasuredDimension()");  
  14.         }  
  15.         mPrivateFlags |= LAYOUT_REQUIRED;  
  16.     }  
  17.     mOldWidthMeasureSpec = widthMeasureSpec;  
  18.     mOldHeightMeasureSpec = heightMeasureSpec;  
  19. }  

注意观察,measure()这个方法是final的,因此我们无法在子类中去重写这个方法,说明Android是不允许我们改变View的measure框架的。然后在第9行调用了onMeasure()方法,这里才是真正去测量并设置View大小的地方,默认会调用getDefaultSize()方法来获取视图的大小,如下所示:

[java] view
plain
copy在CODE上查看代码片派生到我的代码片

  1. public static int getDefaultSize(int size, int measureSpec) {  
  2.     int result = size;  
  3.     int specMode = MeasureSpec.getMode(measureSpec);  
  4.     int specSize = MeasureSpec.getSize(measureSpec);  
  5.     switch (specMode) {  
  6.     case MeasureSpec.UNSPECIFIED:  
  7.         result = size;  
  8.         break;  
  9.     case MeasureSpec.AT_MOST:  
  10.     case MeasureSpec.EXACTLY:  
  11.         result = specSize;  
  12.         break;  
  13.     }  
  14.     return result;  
  15. }  

这里传入的measureSpec是一直从measure()方法中传递过来的。然后调用MeasureSpec.getMode()方法可以解析出specMode,调用MeasureSpec.getSize()方法可以解析出specSize。接下来进行判断,如果specMode等于AT_MOST或EXACTLY就返回specSize,这也是系统默认的行为。之后会在onMeasure()方法中调用setMeasuredDimension()方法来设定测量出的大小,这样一次measure过程就结束了。

当然,一个界面的展示可能会涉及到很多次的measure,因为一个布局中一般都会包含多个子视图,每个视图都需要经历一次measure过程。ViewGroup中定义了一个measureChildren()方法来去测量子视图的大小,如下所示:

[java] view
plain
copy在CODE上查看代码片派生到我的代码片

  1. protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {  
  2.     final int size = mChildrenCount;  
  3.     final View[] children = mChildren;  
  4.     for (int i = 0; i < size; ++i) {  
  5.         final View child = children[i];  
  6.         if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {  
  7.             measureChild(child, widthMeasureSpec, heightMeasureSpec);  
  8.         }  
  9.     }  
  10. }  

这里首先会去遍历当前布局下的所有子视图,然后逐个调用measureChild()方法来测量相应子视图的大小,如下所示:

[java] view
plain
copy在CODE上查看代码片派生到我的代码片

  1. protected void measureChild(View child, int parentWidthMeasureSpec,  
  2.         int parentHeightMeasureSpec) {  
  3.     final LayoutParams lp = child.getLayoutParams();  
  4.     final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,  
  5.             mPaddingLeft + mPaddingRight, lp.width);  
  6.     final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,  
  7.             mPaddingTop + mPaddingBottom, lp.height);  
  8.     child.measure(childWidthMeasureSpec, childHeightMeasureSpec);  

抱歉!评论已关闭.