Android · 2015年4月1日 0

Android LayoutInflater原理分析,带你一步步深入了解View(一)

转载:http://blog.csdn.net/guolin_blog/article/details/12921889

有段时间没写博客了,感觉都有些生疏了呢。最近繁忙的工作终于告一段落,又有时间写文章了,接下来还会继续坚持每一周篇的节奏。

有不少朋友跟我反应,都希望我可以写一篇关于View的文章,讲一讲View的工作原理以及自定义View的方法。没错,承诺过的文章我是一定要兑现的,而且在View这个话题上我还准备多写几篇,尽量能将这个知识点讲得透彻一些。那么今天就从LayoutInflater开始讲起吧。

相信接触Android久一点的朋友对于LayoutInflater一定不会陌生,都会知道它主要是用于加载布局的。而刚接触Android的朋友可能对LayoutInflater不怎么熟悉,因为加载布局的任务通常都是在Activity中调用setContentView()方法来完成的。其实setContentView()方法的内部也是使用LayoutInflater来加载布局的,只不过这部分源码是internal的,不太容易查看到。那么今天我们就来把LayoutInflater的工作流程仔细地剖析一遍,也许还能解决掉某些困扰你心头多年的疑惑。

先来看一下LayoutInflater的基本用法吧,它的用法非常简单,首先需要获取到LayoutInflater的实例,有两种方法可以获取到,第一种写法如下:

  1. LayoutInflater layoutInflater = LayoutInflater.from(context);

当然,还有另外一种写法也可以完成同样的效果:

  1. LayoutInflater layoutInflater = (LayoutInflater) context
  2.         .getSystemService(Context.LAYOUT_INFLATER_SERVICE);

其实第一种就是第二种的简单写法,只是Android给我们做了一下封装而已。得到了LayoutInflater的实例之后就可以调用它的inflate()方法来加载布局了,如下所示:

  1. layoutInflater.inflate(resourceId, root);

inflate()方法一般接收两个参数,第一个参数就是要加载的布局id,第二个参数是指给该布局的外部再嵌套一层父布局,如果不需要就直接传null。这样就成功成功创建了一个布局的实例,之后再将它添加到指定的位置就可以显示出来了。

下面我们就通过一个非常简单的小例子,来更加直观地看一下LayoutInflater的用法。比如说当前有一个项目,其中MainActivity对应的布局文件叫做activity_main.xml,代码如下所示:

  1. <LinearLayout xmlns:android=“http://schemas.android.com/apk/res/android”
  2.     android:id=“@+id/main_layout”
  3.     android:layout_width=“match_parent”
  4.     android:layout_height=“match_parent” >
  5. </LinearLayout>

这个布局文件的内容非常简单,只有一个空的LinearLayout,里面什么控件都没有,因此界面上应该不会显示任何东西。

那么接下来我们再定义一个布局文件,给它取名为button_layout.xml,代码如下所示:

  1. <Button xmlns:android=“http://schemas.android.com/apk/res/android”
  2.     android:layout_width=“wrap_content”
  3.     android:layout_height=“wrap_content”
  4.     android:text=“Button” >
  5. </Button>

这个布局文件也非常简单,只有一个Button按钮而已。现在我们要想办法,如何通过LayoutInflater来将button_layout这个布局添加到主布局文件的LinearLayout中。根据刚刚介绍的用法,修改MainActivity中的代码,如下所示:

  1. public class MainActivity extends Activity {
  2.     private LinearLayout mainLayout;
  3.     @Override
  4.     protected void onCreate(Bundle savedInstanceState) {
  5.         super.onCreate(savedInstanceState);
  6.         setContentView(R.layout.activity_main);
  7.         mainLayout = (LinearLayout) findViewById(R.id.main_layout);
  8.         LayoutInflater layoutInflater = LayoutInflater.from(this);
  9.         View buttonLayout = layoutInflater.inflate(R.layout.button_layout, null);
  10.         mainLayout.addView(buttonLayout);
  11.     }
  12. }

可以看到,这里先是获取到了LayoutInflater的实例,然后调用它的inflate()方法来加载button_layout这个布局,最后调用LinearLayout的addView()方法将它添加到LinearLayout中。

现在可以运行一下程序,结果如下图所示:

Button在界面上显示出来了!说明我们确实是借助LayoutInflater成功将button_layout这个布局添加到LinearLayout中了。LayoutInflater技术广泛应用于需要动态添加View的时候,比如在ScrollView和ListView中,经常都可以看到LayoutInflater的身影。

当然,仅仅只是介绍了如何使用LayoutInflater显然是远远无法满足大家的求知欲的,知其然也要知其所以然,接下来我们就从源码的角度上看一看LayoutInflater到底是如何工作的。

不管你是使用的哪个inflate()方法的重载,最终都会辗转调用到LayoutInflater的如下代码中:

  1. public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {
  2.     synchronized (mConstructorArgs) {
  3.         final AttributeSet attrs = Xml.asAttributeSet(parser);
  4.         mConstructorArgs[0] = mContext;
  5.         View result = root;
  6.         try {
  7.             int type;
  8.             while ((type = parser.next()) != XmlPullParser.START_TAG &&
  9.                     type != XmlPullParser.END_DOCUMENT) {
  10.             }
  11.             if (type != XmlPullParser.START_TAG) {
  12.                 throw new InflateException(parser.getPositionDescription()
  13.                         + “: No start tag found!”);
  14.             }
  15.             final String name = parser.getName();
  16.             if (TAG_MERGE.equals(name)) {
  17.                 if (root == null || !attachToRoot) {
  18.                     throw new InflateException(“merge can be used only with a valid “
  19.                             + “ViewGroup root and attachToRoot=true”);
  20.                 }
  21.                 rInflate(parser, root, attrs);
  22.             } else {
  23.                 View temp = createViewFromTag(name, attrs);
  24.                 ViewGroup.LayoutParams params = null;
  25.                 if (root != null) {
  26.                     params = root.generateLayoutParams(attrs);
  27.                     if (!attachToRoot) {
  28.                         temp.setLayoutParams(params);
  29.                     }
  30.                 }
  31.                 rInflate(parser, temp, attrs);
  32.                 if (root != null && attachToRoot) {
  33.                     root.addView(temp, params);
  34.                 }
  35.                 if (root == null || !attachToRoot) {
  36.                     result = temp;
  37.                 }
  38.             }
  39.         } catch (XmlPullParserException e) {
  40.             InflateException ex = new InflateException(e.getMessage());
  41.             ex.initCause(e);
  42.             throw ex;
  43.         } catch (IOException e) {
  44.             InflateException ex = new InflateException(
  45.                     parser.getPositionDescription()
  46.                     + “: “ + e.getMessage());
  47.             ex.initCause(e);
  48.             throw ex;
  49.         }
  50.         return result;
  51.     }
  52. }

从这里我们就可以清楚地看出,LayoutInflater其实就是使用Android提供的pull解析方式来解析布局文件的。不熟悉pull解析方式的朋友可以网上搜一下,教程很多,我就不细讲了,这里我们注意看下第23行,调用了createViewFromTag()这个方法,并把节点名和参数传了进去。看到这个方法名,我们就应该能猜到,它是用于根据节点名来创建View对象的。确实如此,在createViewFromTag()方法的内部又会去调用createView()方法,然后使用反射的方式创建出View的实例并返回。

当然,这里只是创建出了一个根布局的实例而已,接下来会在第31行调用rInflate()方法来循环遍历这个根布局下的子元素,代码如下所示:

  1. private void rInflate(XmlPullParser parser, View parent, final AttributeSet attrs)
  2.         throws XmlPullParserException, IOException {
  3.     final int depth = parser.getDepth();
  4.     int type;
  5.     while (((type = parser.next()) != XmlPullParser.END_TAG ||
  6.             parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
  7.         if (type != XmlPullParser.START_TAG) {
  8.             continue;
  9.         }
  10.         final String name = parser.getName();
  11.         if (TAG_REQUEST_FOCUS.equals(name)) {
  12.             parseRequestFocus(parser, parent);
  13.         } else if (TAG_INCLUDE.equals(name)) {
  14.             if (parser.getDepth() == 0) {
  15.                 throw new InflateException(“<include /> cannot be the root element”);
  16.             }
  17.             parseInclude(parser, parent, attrs);
  18.         } else if (TAG_MERGE.equals(name)) {
  19.             throw new InflateException(“<merge /> must be the root element”);
  20.         } else {
  21.             final View view = createViewFromTag(name, attrs);
  22.             final ViewGroup viewGroup = (ViewGroup) parent;
  23.             final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
  24.             rInflate(parser, view, attrs);
  25.             viewGroup.addView(view, params);
  26.         }
  27.     }
  28.     parent.onFinishInflate();
  29. }

可以看到,在第21行同样是createViewFromTag()方法来创建View的实例,然后还会在第24行递归调用rInflate()方法来查找这个View下的子元素,每次递归完成后则将这个View添加到父布局当中。

这样的话,把整个布局文件都解析完成后就形成了一个完整的DOM结构,最终会把最顶层的根布局返回,至此inflate()过程全部结束。

比较细心的朋友也许会注意到,inflate()方法还有个接收三个参数的方法重载,结构如下:

  1. inflate(int resource, ViewGroup root, boolean attachToRoot)

那么这第三个参数attachToRoot又是什么意思呢?其实如果你仔细去阅读上面的源码应该可以自己分析出答案,这里我先将结论说一下吧,感兴趣的朋友可以再阅读一下源码,校验我的结论是否正确。

1. 如果root为null,attachToRoot将失去作用,设置任何值都没有意义。

2. 如果root不为null,attachToRoot设为true,则会在加载的布局文件的最外层再嵌套一层root布局。

3. 如果root不为null,attachToRoot设为false,则root参数失去作用。

4. 在不设置attachToRoot参数的情况下,如果root不为null,attachToRoot参数默认为true。

好了,现在对LayoutInflater的工作原理和流程也搞清楚了,你该满足了吧。额。。。。还嫌这个例子中的按钮看起来有点小,想要调大一些?那简单的呀,修改button_layout.xml中的代码,如下所示:

  1. <Button xmlns:android=“http://schemas.android.com/apk/res/android”
  2.     android:layout_width=“300dp”
  3.     android:layout_height=“80dp”
  4.     android:text=“Button” >
  5. </Button>

这里我们将按钮的宽度改成300dp,高度改成80dp,这样够大了吧?现在重新运行一下程序来观察效果。咦?怎么按钮还是原来的大小,没有任何变化!是不是按钮仍然不够大,再改大一点呢?还是没有用!

其实这里不管你将Button的layout_width和layout_height的值修改成多少,都不会有任何效果的,因为这两个值现在已经完全失去了作用。平时我们经常使用layout_width和layout_height来设置View的大小,并且一直都能正常工作,就好像这两个属性确实是用于设置View的大小的。而实际上则不然,它们其实是用于设置View在布局中的大小的,也就是说,首先View必须存在于一个布局中,之后如果将layout_width设置成match_parent表示让View的宽度填充满布局,如果设置成wrap_content表示让View的宽度刚好可以包含其内容,如果设置成具体的数值则View的宽度会变成相应的数值。这也是为什么这两个属性叫作layout_width和layout_height,而不是width和height。

再来看一下我们的button_layout.xml吧,很明显Button这个控件目前不存在于任何布局当中,所以layout_width和layout_height这两个属性理所当然没有任何作用。那么怎样修改才能让按钮的大小改变呢?解决方法其实有很多种,最简单的方式就是在Button的外面再嵌套一层布局,如下所示:

  1. <RelativeLayout xmlns:android=“http://schemas.android.com/apk/res/android”
  2.     android:layout_width=“match_parent”
  3.     android:layout_height=“match_parent” >
  4.     <Button
  5.         android:layout_width=“300dp”
  6.         android:layout_height=“80dp”
  7.         android:text=“Button” >
  8.     </Button>
  9. </RelativeLayout>

可以看到,这里我们又加入了一个RelativeLayout,此时的Button存在与RelativeLayout之中,layout_width和layout_height属性也就有作用了。当然,处于最外层的RelativeLayout,它的layout_width和layout_height则会失去作用。现在重新运行一下程序,结果如下图所示:

OK!按钮的终于可以变大了,这下总算是满足大家的要求了吧。

看到这里,也许有些朋友心中会有一个巨大的疑惑。不对呀!平时在Activity中指定布局文件的时候,最外层的那个布局是可以指定大小的呀,layout_width和layout_height都是有作用的。确实,这主要是因为,在setContentView()方法中,Android会自动在布局文件的最外层再嵌套一个FrameLayout,所以layout_width和layout_height属性才会有效果。那么我们来证实一下吧,修改MainActivity中的代码,如下所示:

  1. public class MainActivity extends Activity {
  2.     private LinearLayout mainLayout;
  3.     @Override
  4.     protected void onCreate(Bundle savedInstanceState) {
  5.         super.onCreate(savedInstanceState);
  6.         setContentView(R.layout.activity_main);
  7.         mainLayout = (LinearLayout) findViewById(R.id.main_layout);
  8.         ViewParent viewParent = mainLayout.getParent();
  9.         Log.d(“TAG”“the parent of mainLayout is “ + viewParent);
  10.     }
  11. }

可以看到,这里通过findViewById()方法,拿到了activity_main布局中最外层的LinearLayout对象,然后调用它的getParent()方法获取它的父布局,再通过Log打印出来。现在重新运行一下程序,结果如下图所示:

非常正确!LinearLayout的父布局确实是一个FrameLayout,而这个FrameLayout就是由系统自动帮我们添加上的。

说到这里,虽然setContentView()方法大家都会用,但实际上Android界面显示的原理要比我们所看到的东西复杂得多。任何一个Activity中显示的界面其实主要都由两部分组成,标题栏和内容布局。标题栏就是在很多界面顶部显示的那部分内容,比如刚刚我们的那个例子当中就有标题栏,可以在代码中控制让它是否显示。而内容布局就是一个FrameLayout,这个布局的id叫作content,我们调用setContentView()方法时所传入的布局其实就是放到这个FrameLayout中的,这也是为什么这个方法名叫作setContentView(),而不是叫setView()。

最后再附上一张Activity窗口的组成图吧,以便于大家更加直观地理解:

相信每个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()方法可以发现如下代码:

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

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

  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()方法里面的代码吧,如下所示:

  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()方法来获取视图的大小,如下所示:

  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()方法来去测量子视图的大小,如下所示:

  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()方法来测量相应子视图的大小,如下所示:

  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);
  9. }

可以看到,在第4行和第6行分别调用了getChildMeasureSpec()方法来去计算子视图的MeasureSpec,计算的依据就是布局文件中定义的MATCH_PARENT、WRAP_CONTENT等值,这个方法的内部细节就不再贴出。然后在第8行调用子视图的measure()方法,并把计算出的MeasureSpec传递进去,之后的流程就和前面所介绍的一样了。

当然,onMeasure()方法是可以重写的,也就是说,如果你不想使用系统默认的测量方式,可以按照自己的意愿进行定制,比如:

  1. public class MyView extends View {
  2.     ……
  3.     @Override
  4.     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  5.         setMeasuredDimension(200200);
  6.     }
  7. }

这样的话就把View默认的测量流程覆盖掉了,不管在布局文件中定义MyView这个视图的大小是多少,最终在界面上显示的大小都将会是200*200。

需要注意的是,在setMeasuredDimension()方法调用之后,我们才能使用getMeasuredWidth()和getMeasuredHeight()来获取视图测量出的宽高,以此之前调用这两个方法得到的值都会是0。

由此可见,视图大小的控制是由父视图、布局文件、以及视图本身共同完成的,父视图会提供给子视图参考的大小,而开发人员可以在XML文件中指定视图的大小,然后视图本身会对最终的大小进行拍板。

到此为止,我们就把视图绘制流程的第一阶段分析完了。

二. onLayout()

measure过程结束后,视图的大小就已经测量好了,接下来就是layout的过程了。正如其名字所描述的一样,这个方法是用于给视图进行布局的,也就是确定视图的位置。ViewRoot的performTraversals()方法会在measure结束后继续执行,并调用View的layout()方法来执行此过程,如下所示:

  1. host.layout(00, host.mMeasuredWidth, host.mMeasuredHeight);

layout()方法接收四个参数,分别代表着左、上、右、下的坐标,当然这个坐标是相对于当前视图的父视图而言的。可以看到,这里还把刚才测量出的宽度和高度传到了layout()方法中。那么我们来看下layout()方法中的代码是什么样的吧,如下所示:

  1. public void layout(int l, int t, int r, int b) {
  2.     int oldL = mLeft;
  3.     int oldT = mTop;
  4.     int oldB = mBottom;
  5.     int oldR = mRight;
  6.     boolean changed = setFrame(l, t, r, b);
  7.     if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {
  8.         if (ViewDebug.TRACE_HIERARCHY) {
  9.             ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT);
  10.         }
  11.         onLayout(changed, l, t, r, b);
  12.         mPrivateFlags &= ~LAYOUT_REQUIRED;
  13.         if (mOnLayoutChangeListeners != null) {
  14.             ArrayList<OnLayoutChangeListener> listenersCopy =
  15.                     (ArrayList<OnLayoutChangeListener>) mOnLayoutChangeListeners.clone();
  16.             int numListeners = listenersCopy.size();
  17.             for (int i = 0; i < numListeners; ++i) {
  18.                 listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
  19.             }
  20.         }
  21.     }
  22.     mPrivateFlags &= ~FORCE_LAYOUT;
  23. }

在layout()方法中,首先会调用setFrame()方法来判断视图的大小是否发生过变化,以确定有没有必要对当前的视图进行重绘,同时还会在这里把传递过来的四个参数分别赋值给mLeft、mTop、mRight和mBottom这几个变量。接下来会在第11行调用onLayout()方法,正如onMeasure()方法中的默认行为一样,也许你已经迫不及待地想知道onLayout()方法中的默认行为是什么样的了。进入onLayout()方法,咦?怎么这是个空方法,一行代码都没有?!

没错,View中的onLayout()方法就是一个空方法,因为onLayout()过程是为了确定视图在布局中所在的位置,而这个操作应该是由布局来完成的,即父视图决定子视图的显示位置。既然如此,我们来看下ViewGroup中的onLayout()方法是怎么写的吧,代码如下:

  1. @Override
  2. protected abstract void onLayout(boolean changed, int l, int t, int r, int b);

可以看到,ViewGroup中的onLayout()方法竟然是一个抽象方法,这就意味着所有ViewGroup的子类都必须重写这个方法。没错,像LinearLayout、RelativeLayout等布局,都是重写了这个方法,然后在内部按照各自的规则对子视图进行布局的。由于LinearLayout和RelativeLayout的布局规则都比较复杂,就不单独拿出来进行分析了,这里我们尝试自定义一个布局,借此来更深刻地理解onLayout()的过程。

自定义的这个布局目标很简单,只要能够包含一个子视图,并且让子视图正常显示出来就可以了。那么就给这个布局起名叫做SimpleLayout吧,代码如下所示:

  1. public class SimpleLayout extends ViewGroup {
  2.     public SimpleLayout(Context context, AttributeSet attrs) {
  3.         super(context, attrs);
  4.     }
  5.     @Override
  6.     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  7.         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
  8.         if (getChildCount() > 0) {
  9.             View childView = getChildAt(0);
  10.             measureChild(childView, widthMeasureSpec, heightMeasureSpec);
  11.         }
  12.     }
  13.     @Override
  14.     protected void onLayout(boolean changed, int l, int t, int r, int b) {
  15.         if (getChildCount() > 0) {
  16.             View childView = getChildAt(0);
  17.             childView.layout(00, childView.getMeasuredWidth(), childView.getMeasuredHeight());
  18.         }
  19.     }
  20. }

代码非常的简单,我们来看下具体的逻辑吧。你已经知道,onMeasure()方法会在onLayout()方法之前调用,因此这里在onMeasure()方法中判断SimpleLayout中是否有包含一个子视图,如果有的话就调用measureChild()方法来测量出子视图的大小。

接着在onLayout()方法中同样判断SimpleLayout是否有包含一个子视图,然后调用这个子视图的layout()方法来确定它在SimpleLayout布局中的位置,这里传入的四个参数依次是0、0、childView.getMeasuredWidth()和childView.getMeasuredHeight(),分别代表着子视图在SimpleLayout中左上右下四个点的坐标。其中,调用childView.getMeasuredWidth()和childView.getMeasuredHeight()方法得到的值就是在onMeasure()方法中测量出的宽和高。

这样就已经把SimpleLayout这个布局定义好了,下面就是在XML文件中使用它了,如下所示:

  1. <com.example.viewtest.SimpleLayout xmlns:android=“http://schemas.android.com/apk/res/android”
  2.     android:layout_width=“match_parent”
  3.     android:layout_height=“match_parent” >
  4.     <ImageView
  5.         android:layout_width=“wrap_content”
  6.         android:layout_height=“wrap_content”
  7.         android:src=“@drawable/ic_launcher”
  8.         />
  9. </com.example.viewtest.SimpleLayout>

可以看到,我们能够像使用普通的布局文件一样使用SimpleLayout,只是注意它只能包含一个子视图,多余的子视图会被舍弃掉。这里SimpleLayout中包含了一个ImageView,并且ImageView的宽高都是wrap_content。现在运行一下程序,结果如下图所示:

OK!ImageView成功已经显示出来了,并且显示的位置也正是我们所期望的。如果你想改变ImageView显示的位置,只需要改变childView.layout()方法的四个参数就行了。

在onLayout()过程结束后,我们就可以调用getWidth()方法和getHeight()方法来获取视图的宽高了。说到这里,我相信很多朋友长久以来都会有一个疑问,getWidth()方法和getMeasureWidth()方法到底有什么区别呢?它们的值好像永远都是相同的。其实它们的值之所以会相同基本都是因为布局设计者的编码习惯非常好,实际上它们之间的差别还是挺大的。

首先getMeasureWidth()方法在measure()过程结束后就可以获取到了,而getWidth()方法要在layout()过程结束后才能获取到。另外,getMeasureWidth()方法中的值是通过setMeasuredDimension()方法来进行设置的,而getWidth()方法中的值则是通过视图右边的坐标减去左边的坐标计算出来的。

观察SimpleLayout中onLayout()方法的代码,这里给子视图的layout()方法传入的四个参数分别是0、0、childView.getMeasuredWidth()和childView.getMeasuredHeight(),因此getWidth()方法得到的值就是childView.getMeasuredWidth() – 0 = childView.getMeasuredWidth() ,所以此时getWidth()方法和getMeasuredWidth() 得到的值就是相同的,但如果你将onLayout()方法中的代码进行如下修改:

  1. @Override
  2. protected void onLayout(boolean changed, int l, int t, int r, int b) {
  3.     if (getChildCount() > 0) {
  4.         View childView = getChildAt(0);
  5.         childView.layout(00200200);
  6.     }
  7. }

这样getWidth()方法得到的值就是200 – 0 = 200,不会再和getMeasuredWidth()的值相同了。当然这种做法充分不尊重measure()过程计算出的结果,通常情况下是不推荐这么写的。getHeight()与getMeasureHeight()方法之间的关系同上,就不再重复分析了。

到此为止,我们把视图绘制流程的第二阶段也分析完了。

三. onDraw()

measure和layout的过程都结束后,接下来就进入到draw的过程了。同样,根据名字你就能够判断出,在这里才真正地开始对视图进行绘制。ViewRoot中的代码会继续执行并创建出一个Canvas对象,然后调用View的draw()方法来执行具体的绘制工作。draw()方法内部的绘制过程总共可以分为六步,其中第二步和第五步在一般情况下很少用到,因此这里我们只分析简化后的绘制过程。代码如下所示:

  1. public void draw(Canvas canvas) {
  2.     if (ViewDebug.TRACE_HIERARCHY) {
  3.         ViewDebug.trace(this, ViewDebug.HierarchyTraceType.DRAW);
  4.     }
  5.     final int privateFlags = mPrivateFlags;
  6.     final boolean dirtyOpaque = (privateFlags & DIRTY_MASK) == DIRTY_OPAQUE &&
  7.             (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
  8.     mPrivateFlags = (privateFlags & ~DIRTY_MASK) | DRAWN;
  9.     // Step 1, draw the background, if needed
  10.     int saveCount;
  11.     if (!dirtyOpaque) {
  12.         final Drawable background = mBGDrawable;
  13.         if (background != null) {
  14.             final int scrollX = mScrollX;
  15.             final int scrollY = mScrollY;
  16.             if (mBackgroundSizeChanged) {
  17.                 background.setBounds(00,  mRight – mLeft, mBottom – mTop);
  18.                 mBackgroundSizeChanged = false;
  19.             }
  20.             if ((scrollX | scrollY) == 0) {
  21.                 background.draw(canvas);
  22.             } else {
  23.                 canvas.translate(scrollX, scrollY);
  24.                 background.draw(canvas);
  25.                 canvas.translate(-scrollX, -scrollY);
  26.             }
  27.         }
  28.     }
  29.     final int viewFlags = mViewFlags;
  30.     boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
  31.     boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
  32.     if (!verticalEdges && !horizontalEdges) {
  33.         // Step 3, draw the content
  34.         if (!dirtyOpaque) onDraw(canvas);
  35.         // Step 4, draw the children
  36.         dispatchDraw(canvas);
  37.         // Step 6, draw decorations (scrollbars)
  38.         onDrawScrollBars(canvas);
  39.         // we’re done…
  40.         return;
  41.     }
  42. }

可以看到,第一步是从第9行代码开始的,这一步的作用是对视图的背景进行绘制。这里会先得到一个mBGDrawable对象,然后根据layout过程确定的视图位置来设置背景的绘制区域,之后再调用Drawable的draw()方法来完成背景的绘制工作。那么这个mBGDrawable对象是从哪里来的呢?其实就是在XML中通过android:background属性设置的图片或颜色。当然你也可以在代码中通过setBackgroundColor()、setBackgroundResource()等方法进行赋值。

接下来的第三步是在第34行执行的,这一步的作用是对视图的内容进行绘制。可以看到,这里去调用了一下onDraw()方法,那么onDraw()方法里又写了什么代码呢?进去一看你会发现,原来又是个空方法啊。其实也可以理解,因为每个视图的内容部分肯定都是各不相同的,这部分的功能交给子类来去实现也是理所当然的。

第三步完成之后紧接着会执行第四步,这一步的作用是对当前视图的所有子视图进行绘制。但如果当前的视图没有子视图,那么也就不需要进行绘制了。因此你会发现View中的dispatchDraw()方法又是一个空方法,而ViewGroup的dispatchDraw()方法中就会有具体的绘制代码。

以上都执行完后就会进入到第六步,也是最后一步,这一步的作用是对视图的滚动条进行绘制。那么你可能会奇怪,当前的视图又不一定是ListView或者ScrollView,为什么要绘制滚动条呢?其实不管是Button也好,TextView也好,任何一个视图都是有滚动条的,只是一般情况下我们都没有让它显示出来而已。绘制滚动条的代码逻辑也比较复杂,这里就不再贴出来了,因为我们的重点是第三步过程。

通过以上流程分析,相信大家已经知道,View是不会帮我们绘制内容部分的,因此需要每个视图根据想要展示的内容来自行绘制。如果你去观察TextView、ImageView等类的源码,你会发现它们都有重写onDraw()这个方法,并且在里面执行了相当不少的绘制逻辑。绘制的方式主要是借助Canvas这个类,它会作为参数传入到onDraw()方法中,供给每个视图使用。Canvas这个类的用法非常丰富,基本可以把它当成一块画布,在上面绘制任意的东西,那么我们就来尝试一下吧。

这里简单起见,我只是创建一个非常简单的视图,并且用Canvas随便绘制了一点东西,代码如下所示:

  1. public class MyView extends View {
  2.     private Paint mPaint;
  3.     public MyView(Context context, AttributeSet attrs) {
  4.         super(context, attrs);
  5.         mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
  6.     }
  7.     @Override
  8.     protected void onDraw(Canvas canvas) {
  9.         mPaint.setColor(Color.YELLOW);
  10.         canvas.drawRect(00, getWidth(), getHeight(), mPaint);
  11.         mPaint.setColor(Color.BLUE);
  12.         mPaint.setTextSize(20);
  13.         String text = “Hello View”;
  14.         canvas.drawText(text, 0, getHeight() / 2, mPaint);
  15.     }
  16. }

可以看到,我们创建了一个自定义的MyView继承自View,并在MyView的构造函数中创建了一个Paint对象。Paint就像是一个画笔一样,配合着Canvas就可以进行绘制了。这里我们的绘制逻辑比较简单,在onDraw()方法中先是把画笔设置成黄色,然后调用Canvas的drawRect()方法绘制一个矩形。然后在把画笔设置成蓝色,并调整了一下文字的大小,然后调用drawText()方法绘制了一段文字。

就这么简单,一个自定义的视图就已经写好了,现在可以在XML中加入这个视图,如下所示:

  1. <LinearLayout xmlns:android=“http://schemas.android.com/apk/res/android”
  2.     android:layout_width=“match_parent”
  3.     android:layout_height=“match_parent” >
  4.     <com.example.viewtest.MyView
  5.         android:layout_width=“200dp”
  6.         android:layout_height=“100dp”
  7.         />
  8. </LinearLayout>

将MyView的宽度设置成200dp,高度设置成100dp,然后运行一下程序,结果如下图所示:

图中显示的内容也正是MyView这个视图的内容部分了。由于我们没给MyView设置背景,因此这里看不出来View自动绘制的背景效果。

当然了Canvas的用法还有很多很多,这里我不可能把Canvas的所有用法都列举出来,剩下的就要靠大家自行去研究和学习了。

Share this: