【朝花夕拾】Android自定义View篇之(一)View绘制流程

前言

       转载请申明转自【https://www.cnblogs.com/andy-songwei/p/10955062.html】谢谢!

       自定义View、多线程、网络,被认为是Android开发者必须牢固掌握的最基础的三大基本功。Android View的绘制流程原理又是学好自定义View的理论基础,所以掌握好View的绘制原理是Android开发进阶中无法绕过的一道坎。而关乎到原理性的东西往往又让很多初学者感到难以下手,所以真正掌握的人并不多。本文采用非常浅显的语言,从顺着Android源码的思路,对View的整个绘制流程进行近乎“地毯式搜索”般的方式,对其中的关键流程和知识点进行查证和分析,以图让初级程序员都能轻松读懂。本文最大的特点,就是最大限度地向源码要答案,从源码中追流程的来龙去脉,在注释中查功能的点点滴滴,所有的结论都尽量在源码和注释中找根据。

       为了能对其中的重难点分析透彻,文中贴出了大量的源码依据以及源码中的注释,并对重要的注释进行了翻译和讲解,所以文章会比较长。讲解该知识点的文章普遍都非常长,所以希望读者能够秉承程序员吃苦耐劳的精神,攻克这个难关。本文中的源码是基于API26的,即Android8.0系统版本,主要内容大致如下:

 

 

一、View绘制的三个流程

       我们知道,在自定义View的时候一般需要重写父类的onMeasure()、onLayout()、onDraw()三个方法,来完成视图的展示过程。当然,这三个暴露给开发者重写的方法只不过是整个绘制流程的冰山一角,更多复杂的幕后工作,都让系统给代劳了。一个完整的绘制流程包括measure、layout、draw三个步骤,其中:

     measure:测量。系统会先根据xml布局文件和代码中对控件属性的设置,来获取或者计算出每个View和ViewGrop的尺寸,并将这些尺寸保存下来。

     layout:布局。根据测量出的结果以及对应的参数,来确定每一个控件应该显示的位置。

     draw:绘制。确定好位置后,就将这些控件绘制到屏幕上。

 

二、Android视图层次结构简介  

       在介绍View绘制流程之前,咱们先简单介绍一下Android视图层次结构以及DecorView,因为View的绘制流程的入口和DecorView有着密切的联系。

 

       咱们平时看到的视图,其实存在如上的嵌套关系。上图是针对比较老的Android系统版本中制作的,新的版本中会略有出入,还有一个状态栏,但整体上没变。我们平时在Activity中setContentView(...)中对应的layout内容,对应的是上图中ViewGrop的树状结构,实际上添加到系统中时,会再裹上一层FrameLayout,就是上图中最里面的浅蓝色部分了。

       这里咱们再通过一个实例来继续查看。AndroidStudio工具中提供了一个布局视察器工具,通过Tools > Android > Layout Inspector可以查看具体某个Activity的布局情况。下图中,左边树状结构对应了右边的可视图,可见DecorView是整个界面的根视图,对应右边的红色框,是整个屏幕的大小。黄色边框为状态栏部分;那个绿色边框中有两个部分,一个是白框中的ActionBar,对应了上图中紫色部分的TitleActionBar部分,即标题栏,平时咱们可以在Activity中将其隐藏掉;另外一个蓝色边框部分,对应上图中最里面的蓝色部分,即ContentView部分。下图中左边有两个蓝色框,上面那个中有个“contain_layout”,这个就是Activity中setContentView中设置的layout.xml布局文件中的最外层父布局,咱们能通过layout布局文件直接完全操控的也就是这一块,当其被add到视图系统中时,会被系统裹上ContentFrameLayout(显然是FrameLayout的子类),这也就是为什么添加layout.xml视图的方法叫setContentView(...)而不叫setView(...)的原因。

 

 

三、故事开始的地方

        如果对Activity的启动流程有一定了解的话,应该知道这个启动过程会在ActivityThread.java类中完成,在启动Activity的过程中,会调用到handleResumeActivity(...)方法,关于视图的绘制过程最初就是从这个方法开始的。

 

  1、View绘制起源UML时序图

       整个调用链如下图所示,直到ViewRootImpl类中的performTraversals()中,才正式开始绘制流程了,所以一般都是以该方法作为正式绘制的源头。

图3.1 View绘制起源UML时序图

 

   2、handleResumeActivity()方法

       在这咱们先大致看看ActivityThread类中的handleResumeActivity方法,咱们这里只贴出关键代码:

 1 //===========ActivityThread.java==========
 2 final void handleResumeActivity(...) {
 3     ......
 4     //跟踪代码后发现其初始赋值为mWindow = new PhoneWindow(this, window, activityConfigCallback);
 5     r.window = r.activity.getWindow(); 
 6        //从PhoneWindow实例中获取DecorView  
 7     View decor = r.window.getDecorView();
 8     ......
 9     //跟踪代码后发现,vm值为上述PhoneWindow实例中获取的WindowManager。
10     ViewManager wm = a.getWindowManager();
11     ......
12     //当前window的属性,从代码跟踪来看是PhoneWindow窗口的属性
13     WindowManager.LayoutParams l = r.window.getAttributes();
14     ......
15     wm.addView(decor, l);
16     ......
17 }

       上述代码第8行中,ViewManager是一个接口,addView是其中定义个一个空方法,WindowManager是其子类,WindowManagerImpl是WindowManager的实现类(顺便啰嗦一句,这种方式叫做面向接口编程,在父类中定义,在子类中实现,在Java中很常见)。第4行代码中的r.window的值可以根据Activity.java的如下代码得知,其值为PhoneWindow实例。

 1 //===============Activity.java=============
 2 private Window mWindow;
 3 public Window getWindow() {
 4    return mWindow;
 5 }
 6 
 7 final void attach(...){
 8    ......
 9    mWindow = new PhoneWindow(this, window, activityConfigCallback);
10    ......
11 }

 

3、两个重要参数分析

       之所以要在这里特意分析handleResumeActivity()方法,除了因为它是整个绘制流程的最初源头外,还有就是addView的两个参数比较重要,它们经过一层一层传递后进入到ViewRootImpl中,在后面分析绘制中要用到。这里再看看这两个参数的相关信息:

    (1)参数decor

 1 //==========PhoneWindow.java===========
 2 // This is the top-level view of the window, containing the window decor.
 3 private DecorView mDecor;
 4 ......
 5 public PhoneWindow(...){
 6    ......
 7    mDecor = (DecorView) preservedWindow.getDecorView();
 8    ......
 9 }
10 
11 @Override
12 public final View getDecorView() {
13    ......
14    return mDecor;
15 }

可见decor参数表示的是DecorView实例。注释中也有说明:这是window的顶级视图,包含了window的decor。

    (2)参数l

 1 //===================Window.java===================
 2 //The current window attributes.
 3     private final WindowManager.LayoutParams mWindowAttributes =
 4         new WindowManager.LayoutParams();
 5 ......
 6 public final WindowManager.LayoutParams getAttributes() {
 7         return mWindowAttributes;
 8     }
 9 ......
10 
11 
12 //==========WindowManager.java的内部类LayoutParams extends ViewGroup.LayoutParams=============
13 public LayoutParams() {
14             super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
15             ......
16         }
17 
18 
19 //==============ViewGroup.java内部类LayoutParams====================
20 public LayoutParams(int width, int height) {
21             this.width = width;
22             this.height = height;
23         }

该参数表示l的是PhoneWindow的LayoutParams属性,其width和height值均为LayoutParams.MATCH_PARENT。

 

        在源码中,WindowPhone和DecorView通过组合方式联系在一起的,而DecorView是整个View体系的根View。在前面handleResumeActivity(...)方法代码片段中,当Actiivity启动后,就通过第14行的addView方法,来间接调用ViewRootImpl类中的performTraversals(),从而实现视图的绘制。

 

四、主角登场 

   无疑,performTraversals()方法是整个过程的主角,它把控着整个绘制的流程。该方法的源码有大约800行,这里咱们仅贴出关键的流程代码,如下所示:
 1 // =====================ViewRootImpl.java=================
 2 private void performTraversals() {
 3    ......
 4    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
 5    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);      
 6    ......
 7    // Ask host how big it wants to be
 8    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
 9    ......
10    performLayout(lp, mWidth, mHeight);
11    ......
12    performDraw();
13 }

 上述代码中就是一个完成的绘制流程,对应上了第一节中提到的三个步骤:

      1)performMeasure():从根节点向下遍历View树,完成所有ViewGroup和View的测量工作,计算出所有ViewGroup和View显示出来需要的高度和宽度;

      2)performLayout():从根节点向下遍历View树,完成所有ViewGroup和View的布局计算工作,根据测量出来的宽高及自身属性,计算出所有ViewGroup和View显示在屏幕上的区域;

      3)performDraw():从根节点向下遍历View树,完成所有ViewGroup和View的绘制工作,根据布局过程计算出的显示区域,将所有View的当前需显示的内容画到屏幕上。

咱们后续就是通过对这三个方法来展开研究整个绘制过程。

 

五、measure过程分析

       这三个绘制流程中,measure是最复杂的,这里会花较长的篇幅来分析它。本节会先介绍整个流程中很重要的两个类MeasureSpec和ViewGroup.LayoutParams类,然后介绍ViewRootImpl、View及ViewGroup中测量流程涉及到的重要方法,最后简单梳理DecorView测量的整个流程并链接一个测量实例分析整个测量过程。

 

  1、MeasureSpec简介

       这里咱们直接上源码吧,先直接通过源码和注释认识一下它,如果看不懂也没关系,在后面使用的时候再回头来看看。

 1 /**
 2      * A MeasureSpec encapsulates the layout requirements passed from parent to child.
 3      * Each MeasureSpec represents a requirement for either the width or the height.
 4      * A MeasureSpec is comprised of a size and a mode. There are three possible
 5      * modes:
 6      * <dl>
 7      * <dt>UNSPECIFIED</dt>
 8      * <dd>
 9      * The parent has not imposed any constraint on the child. It can be whatever size
10      * it wants.
11      * </dd>
12      *
13      * <dt>EXACTLY</dt>
14      * <dd>
15      * The parent has determined an exact size for the child. The child is going to be
16      * given those bounds regardless of how big it wants to be.
17      * </dd>
18      *
19      * <dt>AT_MOST</dt>
20      * <dd>
21      * The child can be as large as it wants up to the specified size.
22      * </dd>
23      * </dl>
24      *
25      * MeasureSpecs are implemented as ints to reduce object allocation. This class
26      * is provided to pack and unpack the &lt;size, mode&gt; tuple into the int.
27      */
28     public static class MeasureSpec {
29         private static final int MODE_SHIFT = 30;
30         private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
31         ......
32         /**
33          * Measure specification mode: The parent has not imposed any constraint
34          * on the child. It can be whatever size it wants.
35          */
36         public static final int UNSPECIFIED = 0 << MODE_SHIFT;
37 
38         /**
39          * Measure specification mode: The parent has determined an exact size
40          * for the child. The child is going to be given those bounds regardless
41          * of how big it wants to be.
42          */
43         public static final int EXACTLY     = 1 << MODE_SHIFT;
44 
45         /**
46          * Measure specification mode: The child can be as large as it wants up
47          * to the specified size.
48          */
49         public static final int AT_MOST     = 2 << MODE_SHIFT;
50         ......
51        /**
52          * Creates a measure specification based on the supplied size and mode.
53          *...... 
54          *@return the measure specification based on size and mode        
55          */
56         public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
57                                           @MeasureSpecMode int mode) {
58             if (sUseBrokenMakeMeasureSpec) {
59                 return size + mode;
60             } else {
61                 return (size & ~MODE_MASK) | (mode & MODE_MASK);
62             }
63             ......
64             
65         }
66         ......
67         /**
68          * Extracts the mode from the supplied measure specification.
69          *......
70          */
71         @MeasureSpecMode
72         public static int getMode(int measureSpec) {
73             //noinspection ResourceType
74             return (measureSpec & MODE_MASK);
75         }
76 
77         /**
78          * Extracts the size from the supplied measure specification.
79          *......
80          * @return the size in pixels defined in the supplied measure specification
81          */
82         public static int getSize(int measureSpec) {
83             return (measureSpec & ~MODE_MASK);
84         }
85         ......
86 }

 从这段代码中,咱们可以得到如下的信息:

    1)MeasureSpec概括了从父布局传递给子view布局要求。每一个MeasureSpec代表了宽度或者高度要求,它由size(尺寸)和mode(模式)组成。

    2)有三种可能的mode:UNSPECIFIED、EXACTLY、AT_MOST

    3)UNSPECIFIED:未指定尺寸模式。父布局没有对子view强加任何限制。它可以是任意想要的尺寸。(笔者注:这个在工作中极少碰到,据说一般在系统中才会用到,后续会讲得很少)

    4)EXACTLY:精确值模式。父布局决定了子view的准确尺寸。子view无论想设置多大的值,都将限定在那个边界内。(笔者注:也就是layout_width属性和layout_height属性为具体的数值,如50dp,或者设置为match_parent,设置为match_parent时也就明确为和父布局有同样的尺寸,所以这里不要以为笔者搞错了。当明确为精确的尺寸后,其也就被给定了一个精确的边界)

    5)AT_MOST:最大值模式。子view可以一直大到指定的值。(笔者注:也就是其宽高属性设置为wrap_content,那么它的最大值也不会超过父布局给定的值,所以称为最大值模式)

    6)MeasureSpec被实现为int型来减少对象分配。该类用于将size和mode元组装包和拆包到int中。(笔者注:也就是将size和mode组合或者拆分为int型数据)

    7)分析代码可知,一个MeasureSpec的模式如下所示,int长度为32位置,高2位表示mode,后30位用于表示size

           

     8)UNSPECIFIED、EXACTLY、AT_MOST这三个mode的示意图如下所示:

              

    9)makeMeasureSpec(int mode,int size)用于将mode和size打包成一个int型的MeasureSpec。

    10)getSize(int measureSpec)方法用于从指定的measureSpec值中获取其size。

    11)getMode(int measureSpec)方法用户从指定的measureSpec值中获取其mode。

 

  2、ViewGroup.LayoutParams简介

   该类的源码及注释分析如下所示。

 1 //============================ViewGroup.java===============================
 2 /**
 3      * LayoutParams are used by views to tell their parents how they want to be
 4      * laid out. 
 5      *......
 6      * <p>
 7      * The base LayoutParams class just describes how big the view wants to be
 8      * for both width and height. For each dimension, it can specify one of:
 9      * <ul>
10      * <li>FILL_PARENT (renamed MATCH_PARENT in API Level 8 and higher), which
11      * means that the view wants to be as big as its parent (minus padding)
12      * <li> WRAP_CONTENT, which means that the view wants to be just big enough
13      * to enclose its content (plus padding)
14      * <li> an exact number
15      * </ul>
16      * There are subclasses of LayoutParams for different subclasses of
17      * ViewGroup. For example, AbsoluteLayout has its own subclass of
18      * LayoutParams which adds an X and Y value.</p>
19      * ......
20      * @attr ref android.R.styleable#ViewGroup_Layout_layout_height
21      * @attr ref android.R.styleable#ViewGroup_Layout_layout_width
22      */
23     public static class LayoutParams {
24         ......
25 
26         /**
27          * Special value for the height or width requested by a View.
28          * MATCH_PARENT means that the view wants to be as big as its parent,
29          * minus the parent's padding, if any. Introduced in API Level 8.
30          */
31         public static final int MATCH_PARENT = -1;
32 
33         /**
34          * Special value for the height or width requested by a View.
35          * WRAP_CONTENT means that the view wants to be just large enough to fit
36          * its own internal content, taking its own padding into account.
37          */
38         public static final int WRAP_CONTENT = -2;
39 
40         /**
41          * Information about how wide the view wants to be. Can be one of the
42          * constants FILL_PARENT (replaced by MATCH_PARENT
43          * in API Level 8) or WRAP_CONTENT, or an exact size.
44          */
45         public int width;
46 
47         /**
48          * Information about how tall the view wants to be. Can be one of the
49          * constants FILL_PARENT (replaced by MATCH_PARENT
50          * in API Level 8) or WRAP_CONTENT, or an exact size.
51          */
52         public int height;
53         ......
54 }

 这对其中重要的信息做一些翻译和整理:

    1)LayoutParams被view用于告诉它们的父布局它们想要怎样被布局。(笔者注:字面意思就是布局参数)

    2)该LayoutParams基类仅仅描述了view希望宽高有多大。对于每一个宽或者高,可以指定为以下三种值中的一个:MATCH_PARENT,WRAP_CONTENT,an exact number。(笔者注:FILL_PARENT从API8开始已经被MATCH_PARENT取代了,所以下文就只提MATCH_PARENT)

    3)MATCH_PARENT:意味着该view希望和父布局尺寸一样大,如果父布局有padding,则要减去该padding值。

    4)WRAP_CONTENT:意味着该view希望其大小为仅仅足够包裹住其内容即可,如果自己有padding,则要加上该padding值。

    5)对ViewGroup不同的子类,也有相应的LayoutParams子类。 

    6)其width和height属性对应着layout_width和layout_height属性。

 

  3、View测量的基本流程及重要方法分析

       View体系的测量是从DecorView这个根view开始递归遍历的,而这个View体系树中包含了众多的叶子view和ViewGroup的子类容器。这一小节中会从ViewRootImpl.performMeasure()开始,分析测量的基本流程。

     (1)ViewRootImpl.performMeasure()方法

       跟踪源码,进入到performMeasure方法分析,这里仅贴出关键流程代码。

1 //=============ViewRootImpl.java==============
2 private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
3        ......
4        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
5        ......
6 }

 这个mView是谁呢?跟踪代码可以找到给它赋值的地方:

1 //========================ViewRootImpl.java======================
2 public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
3       ......
4       mView = view;
5       ......
6 
7       mWindowAttributes.copyFrom(attrs);
8       ......
9 }

       看到这里,是不是有些似曾相识呢?在第二节的绘制流程中提到过,这里setView的参数view和attrs是ActivityThread类中addView方法传递过来的,所以咱们这里可以确定mView指的是DecorView了。上述performMeasure()中,其实就是DecorView在执行measure()操作。如果您这存在“mView不是View类型的吗,怎么会指代DecorView作为整个View体系的根view呢”这样的疑惑,那这里就啰嗦一下,DecorView extends FrameLayout extends ViewGroup extends View,通过这个继承链可以看到,DecorView是一个容器,但ViewGroup也是View的子类,View是所有控件的基类,所以这里View类型的mView指代DecorView是没毛病的。

    (2)View.measure()方法

       尽管mView就是DecorView,但是由于measure()方法是final型的,View子类都不能重写该方法,所以这里追踪measure()的时候就直接进入到View类中了,这里贴出关键流程代码:

 1 //===========================View.java===============================
 2 /**
 3      * <p>
 4      * This is called to find out how big a view should be. The parent
 5      * supplies constraint information in the width and height parameters.
 6      * </p>
 7      *
 8      * <p>
 9      * The actual measurement work of a view is performed in
10      * {@link #onMeasure(int, int)}, called by this method. Therefore, only
11      * {@link #onMeasure(int, int)} can and must be overridden by subclasses.
12      * </p>
13      *
14      *
15      * @param widthMeasureSpec Horizontal space requirements as imposed by the
16      *        parent
17      * @param heightMeasureSpec Vertical space requirements as imposed by the
18      *        parent
19      *
20      * @see #onMeasure(int, int)
21      */
22 public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
23       ......
24       // measure ourselves, this should set the measured dimension flag back
25       onMeasure(widthMeasureSpec, heightMeasureSpec);
26       ......
27 }

        这里面注释提供了很多信息,这简单翻译并整理一下:

        1)该方法被调用,用于找出view应该多大。父布局在witdh和height参数中提供了限制信息;

        2)一个view的实际测量工作是在被本方法所调用的onMeasure(int,int)方法中实现的。所以,只有onMeasure(int,int)可以并且必须被子类重写(笔者注:这里应该指的是,ViewGroup的子类必须重写该方法,才能绘制该容器内的子view。如果是自定义一个子控件,extends View,那么并不是必须重写该方法);

        3)参数widthMeasureSpec:父布局加入的水平空间要求;

        4)参数heightMeasureSpec:父布局加入的垂直空间要求。

       系统将其定义为一个final方法,可见系统不希望整个测量流程框架被修改。

    (3)View.onMeasure()方法

       在上述方法体内看到onMeasure(int,int)方法时,是否有一丝慰藉呢?终于看到咱们最熟悉的身影了,很亲切吧!咱们编写自定义View时,基本上都会重写的方法!咱们看看其源码:

 1 //===========================View.java===============================
 2 /**
 3      * <p>
 4      * Measure the view and its content to determine the measured width and the
 5      * measured height. This method is invoked by {@link #measure(int, int)} and
 6      * should be overridden by subclasses to provide accurate and efficient
 7      * measurement of their contents.
 8      * </p>
 9      *
10      * <p>
11      * <strong>CONTRACT:</strong> When overriding this method, you
12      * <em>must</em> call {@link #setMeasuredDimension(int, int)} to store the
13      * measured width and height of this view. Failure to do so will trigger an
14      * <code>IllegalStateException</code>, thrown by
15      * {@link #measure(int, int)}. Calling the superclass'
16      * {@link #onMeasure(int, int)} is a valid use.
17      * </p>
18      *
19      * <p>
20      * The base class implementation of measure defaults to the background size,
21      * unless a larger size is allowed by the MeasureSpec. Subclasses should
22      * override {@link #onMeasure(int, int)} to provide better measurements of
23      * their content.
24      * </p>
25      *
26      * <p>
27      * If this method is overridden, it is the subclass's responsibility to make
28      * sure the measured height and width are at least the view's minimum height
29      * and width ({@link #getSuggestedMinimumHeight()} and
30      * {@link #getSuggestedMinimumWidth()}).
31      * </p>
32      *
33      * @param widthMeasureSpec horizontal space requirements as imposed by the parent.
34      *                         The requirements are encoded with
35      *                         {@link android.view.View.MeasureSpec}.
36      * @param heightMeasureSpec vertical space requirements as imposed by the parent.
37      *                         The requirements are encoded with
38      *                         {@link android.view.View.MeasureSpec}.
39      *
40      * @see #getMeasuredWidth()
41      * @see #getMeasuredHeight()
42      * @see #setMeasuredDimension(int, int)
43      * @see #getSuggestedMinimumHeight()
44      * @see #getSuggestedMinimumWidth()
45      * @see android.view.View.MeasureSpec#getMode(int)
46      * @see android.view.View.MeasureSpec#getSize(int)
47      */
48     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
49         setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
50                 getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
51     }

       函数体内也就一句代码而已,注释却写了这么一大堆,可见这个方法的重要性了。这里翻译和整理一下这些注释:

      1)测量该view以及它的内容来决定测量的宽度和高度。该方法被measure(int,int)(笔者注:就是前面提到过的那个方法)调用,并且应该被子类重写来提供准确而且有效的对它们的内容的测量。

      2)当重写该方法时,您必须调用setMeasuredDimension(int,int)来存储该view测量出的宽和高。如果不这样做将会触发IllegalStateException,由measure(int,int)抛出。调用基类的onMeasure(int,int)方法是一个有效的方法。

      3)测量的基类实现默认为背景的尺寸,除非更大的尺寸被MeasureSpec所允许。子类应该重写onMeasure(int,int)方法来提供对内容更好的测量。

      4)如果该方法被重写,子类负责确保测量的高和宽至少是该view的mininum高度和mininum宽度值(链接getSuggestedMininumHeight()和getSuggestedMininumWidth());

      5) widthMeasureSpec:父布局加入的水平空间要求。该要求被编码到android.view.View.MeasureSpec中。

      6)heightMeasureSpec:父布局加入的垂直空间要求。该要求被编码到android.view.View.MeasureSpec中。

       注释中最后提到了7个方法,这些方法后面会再分析。注释中花了不少的篇幅对该方法进行说明,但读者恐怕对其中的一些信息表示有些懵吧,比如MeasureSpec是什么,mininum高度和mininum宽度值是怎么回事等,MeasureSpec在本节的开头介绍过,可以回头再看看,其它的后面会作进一步的阐述,到时候咱们再回头来看看这些注释。

        注意:容器类控件都是ViewGroup的子类,如FrameLayout、LinearLayout等,都会重写onMeasure方法,根据自己的特性来进行测量;如果是叶子节点view,即最里层的控件,如TextView等,也可能会重写onMeasure方法,所以当流程走到onMeasure(...)时,流程可能就会切到那些重写的onMeasure()方法中去。最后通过从根View到叶子节点的遍历和递归,最终还是会在叶子view中调用setMeasuredDimension(...)来实现最终的测量。

    (4)View.setMeasuredDimension()方法

      继续看setMeasuredDimension方法:

 1 /**
 2      * <p>This method must be called by {@link #onMeasure(int, int)} to store the
 3      * measured width and measured height. Failing to do so will trigger an
 4      * exception at measurement time.</p>
 5      *
 6      * @param measuredWidth The measured width of this view.  May be a complex
 7      * bit mask as defined by {@link #MEASURED_SIZE_MASK} and
 8      * {@link #MEASURED_STATE_TOO_SMALL}.
 9      * @param measuredHeight The measured height of this view.  May be a complex
10      * bit mask as defined by {@link #MEASURED_SIZE_MASK} and
11      * {@link #MEASURED_STATE_TOO_SMALL}.
12      */
13     protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
14         ......
15         setMeasuredDimensionRaw(measuredWidth, measuredHeight);
16     }

   这里需要重点关注注释中对参数的说明:

       measuredWidth:该view被测量出宽度值。

       measuredHeight:该view被测量出的高度值。

      到这个时候才正式明确提到宽度和高度,通过getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),参数由widthMeasureSpec变成了measuredWidth,即由“父布局加入的水平空间要求”转变为了view的宽度,measuredHeigh也是一样。咱们先继续追踪源码分析width的值:

 1 /**
 2      * Returns the suggested minimum width that the view should use. This
 3      * returns the maximum of the view's minimum width
 4      * and the background's minimum width
 5      *  ({@link android.graphics.drawable.Drawable#getMinimumWidth()}).
 6      * <p>
 7      * When being used in {@link #onMeasure(int, int)}, the caller should still
 8      * ensure the returned width is within the requirements of the parent.
 9      *
10      * @return The suggested minimum width of the view.
11      */
12     protected int getSuggestedMinimumWidth() {
13         return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
14     }

       这个方法是干嘛用的呢?注释的翻译如下:

      1)返回建议该view应该使用的最小宽度值。该方法返回了view的最小宽度值和背景的最小宽度值(链接android.graphics.drawable.Drawable#getMinimumWidth())之间的最大值。

      2)当在onMeasure(int,int)使用时,调用者应该仍然确保返回的宽度值在父布局的要求之内。

      3)返回值:view的建议最小宽度值。

      这其中提到的"mininum width“指的是在xml布局文件中该view的“android:minWidth"属性值,“background's minimum width”值是指“android:background”的宽度。该方法的返回值就是两者之间较大的那一个值,用来作为该view的最小宽度值,现在应该很容易理解了吧,当一个view在layout文件中同时设置了这两个属性时,为了两个条件都满足,自然要选择值大一点的那个了。

 1 /**
 2      * Utility to return a default size. Uses the supplied size if the
 3      * MeasureSpec imposed no constraints. Will get larger if allowed
 4      * by the MeasureSpec.
 5      *
 6      * @param size Default size for this view
 7      * @param measureSpec Constraints imposed by the parent
 8      * @return The size this view should be.
 9      */
10     public static int getDefaultSize(int size, int measureSpec) {
11         int result = size;
12         int specMode = MeasureSpec.getMode(measureSpec);
13         int specSize = MeasureSpec.getSize(measureSpec);
14 
15         switch (specMode) {
16         case MeasureSpec.UNSPECIFIED:
17             result = size;
18             break;
19         case MeasureSpec.AT_MOST:
20         case MeasureSpec.EXACTLY:
21             result = specSize;
22             break;
23         }
24         return result;
25     }

       通过本节开头的介绍,您应该对MeasureSpec有了一个比较明确的认识了,再看看getDefaultSize(int size,int measureSpec)方法,就很容易理解了。正如其注释中所说,如果父布局没有施加任何限制,即MeasureSpec的mode为UNSPECIFIED,那么返回值为参数中提供的size值。如果父布局施加了限制,则返回的默认尺寸为保存在参数measureSpec中的specSize值。所以到目前为止,需要绘制的宽和高值就被确定下来了。只是,我们还需要明确这两个值最初是从哪里传过来的,后面我们还会顺藤摸瓜,找到这两个尺寸的出处。

       既然宽度值measuredWidth和高度值measuredHeight已经确定下来,我们继续追踪之前的setMeasuredDimension(int measuredWidth, int measuredHeight)方法,其内部最后调用了如下的方法:

 1 /**
 2      * ......
 3      * @param measuredWidth The measured width of this view.  May be a complex
 4      * bit mask as defined by {@link #MEASURED_SIZE_MASK} and
 5      * {@link #MEASURED_STATE_TOO_SMALL}.
 6      * @param measuredHeight The measured height of this view.  May be a complex
 7      * bit mask as defined by {@link #MEASURED_SIZE_MASK} and
 8      * {@link #MEASURED_STATE_TOO_SMALL}.
 9      */
10     private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
11         mMeasuredWidth = measuredWidth;
12         mMeasuredHeight = measuredHeight;
13         ......
14     }

       到目前为止,View中的成员变量mMeasureWidth和mMeasureHeight就被赋值了,这也就意味着,View的测量就结束了。前面讲onMeasure()方法时介绍过,View子类(包括ViewGroup子类)通常会重写onMeasure(),当阅读FrameLayout、LinearLayout、TextView等重写的onMeasure()方法时,会发现它们最终都会调用setMeasuredDimension() 方法,从而完成测量。这里可以对应上前面介绍View.onMeasure()时,翻译注释的第2)点以及setMeasuredDimension()方法的注释说明。

    (5)getMeasureWidth()方法

       在View的onMeasure()方法的注释中提到了该方法,这里顺便也介绍一下。

1 //==================View.java==============
2 public static final int MEASURED_SIZE_MASK = 0x00ffffff;
3 /**
4  * ......
5  * @return The raw measured width of this view.
6  */
7 public final int getMeasuredWidth() {
8    return mMeasuredWidth & MEASURED_SIZE_MASK;
9 }

       获取原始的测量宽度值,一般会拿这个方法和layout执行后getWidth()方法做比较。该方法需要在setMeasuredDimension()方法执行后才有效,否则返回值为0。

    (6)getMeasureHeight()方法

       在View的onMeasure()方法的注释中提到了该方法,这里顺便也介绍一下。

1 //==================View.java==============
2 /**
3   * ......
4   * @return The raw measured height of this view.
5   */
6 public final int getMeasuredHeight() {
7    return mMeasuredHeight & MEASURED_SIZE_MASK;
8 }

       获取原始的测量高度值,一般会拿这个方法和layout执行后getHeight()方法做比较。该方法需要在setMeasuredDimension()方法执行后才有效,否则返回值为0。

 

  4、performMeasure()方法中RootMeasureSpec参数来源分析

       前面讲到getDefaultSize(int size,int measureSpec)方法时提到过,要找到其中measureSpec的来源。事实上,根据View体系的不断往下遍历和递归中,前面流程中传入getDefaultSize()方法中的值是根据上一次的值变动的,所以咱们需要找到最初参数值。根据代码往回看,可以看到前文performTraversals()源码部分第三行和第四行中,该参数的来源。咱们先看看传入performMeasure(int,int)的childWidthMeasureSpec是怎么来的。

int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);

       getRootMeasureSpec(int,int)方法的完整源码如下所示:

 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 static int getRootMeasureSpec(int windowSize, int rootDimension) {
15         int measureSpec;
16         switch (rootDimension) {
17 
18         case ViewGroup.LayoutParams.MATCH_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     }

 照例先翻译一下注释

      1)基于window的layout params,在window中为root view 找出measure spec。(笔者注:也就是找出DecorView的MeasureSpec,这里的window也就是PhoneWindow了)

      2)参数windowSize:window的可用宽度和高度值。

      3)参数rootDimension:window的宽/高的layout param值。

      4)返回值:返回用于测量root view的MeasureSpec。    

       如果不清楚LayoutParams类,可以看看本节开头的介绍。在getRootMeasureSpec(int,int)中,MeasureSpec.makeMeasureSpec方法在前面介绍MeasureSpec类的时候提到过,就是将size和mode组合成一个MeasureSpec值。这里我们可以看到ViewGroup.LayoutParam的width/height值和MeasureSpec的mode值存在如下的对应关系:

       我们再继续看看windowSize和rootDimension的实际参数mWidth和lp.width的来历。

 1 //===========================ViewRootImpl.java=======================
 2 ......
 3 final Rect mWinFrame; // frame given by window manager.
 4 ......
 5 private void performTraversals() {
 6     ......
 7     Rect frame = mWinFrame;
 8     ......
 9     mWidth = frame.width();
10     ......
11 }

       从源码中对mWinFrame的注释来看,是由WindowManager提供的,该矩形正好是整个屏幕(这里暂时还没有在源码中找到明确的证据,后续找到后再补上)。在文章【Android图形系统(三)-View绘制流程】的“2.2 窗口布局阶段”中有提到,WindowManagerService服务计算Activity窗口的大小,并将Activity窗口的大小保存在成员变量mWinFrame中。对Activity窗口大小计算的详情,有兴趣的可以阅读一下大神罗升阳的博文【Android窗口管理服务WindowManagerService计算Activity窗口大小的过程分析】。

 1 //=================================ViewRootImpl.java================================
 2 ......
 3 final WindowManager.LayoutParams mWindowAttributes = new WindowManager.LayoutParams();
 4 ......
 5 public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
 6     ......
 7     mWindowAttributes.copyFrom(attrs);
 8     ......
 9 }
10 private void performTraversals() {
11      ......
12      WindowManager.LayoutParams lp = mWindowAttributes;
13      ......     
14 }

       第5行setView方法,在上一节中讲过,其中的参数就是ActivityThread类中传过来的,attrs是PhoneWindow的LayoutParams值,在第三节中就专门讲过这个参数,其width和height属性值均为LayoutParams.MATCH_PARENT。结合getRootMeasureSpec(int windowSize, int rootDimension)方法,可以得出如下结果:

      

       此时,我们就得到了DecorView的MeasureSpec了,后面的递归操作就是在此基础上不断将测量要求从父布局传递到子view。

 

  5、ViewGroup中辅助重写onMeasure的几个重要方法介绍

        前面我们介绍的很多方法都是View类中提供的,ViewGroup中也提供了一些方法用于辅助ViewGroup子类容器的测量。这里重点介绍三个方法:measureChild(...)、measureChildWithMargins(...)和measureChildWithMargins(...)方法。

    (1)measureChild()方法和measureChildWithMargins()方法

 1 //================ViewGroup.java===============
 2 /**
 3      * Ask one of the children of this view to measure itself, taking into
 4      * account both the MeasureSpec requirements for this view and its padding.
 5      * The heavy lifting is done in getChildMeasureSpec.
 6      *
 7      * @param child The child to measure
 8      * @param parentWidthMeasureSpec The width requirements for this view
 9      * @param parentHeightMeasureSpec The height requirements for this view
10      */
11     protected void measureChild(View child, int parentWidthMeasureSpec,
12             int parentHeightMeasureSpec) {
13         final LayoutParams lp = child.getLayoutParams();
14 
15         final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
16                 mPaddingLeft + mPaddingRight, lp.width);
17         final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
18                 mPaddingTop + mPaddingBottom, lp.height);
19 
20         child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
21     }

 

 1 //===================ViewGroup.java===================
 2 /**
 3      * Ask one of the children of this view to measure itself, taking into
 4      * account both the MeasureSpec requirements for this view and its padding
 5      * and margins. The child must have MarginLayoutParams The heavy lifting is
 6      * done in getChildMeasureSpec.
 7      *
 8      * @param child The child to measure
 9      * @param parentWidthMeasureSpec The width requirements for this view
10      * @param widthUsed Extra space that has been used up by the parent
11      *        horizontally (possibly by other children of the parent)
12      * @param parentHeightMeasureSpec The height requirements for this view
13      * @param heightUsed Extra space that has been used up by the parent
14      *        vertically (possibly by other children of the parent)
15      */
16     protected void measureChildWithMargins(View child,
17             int parentWidthMeasureSpec, int widthUsed,
18             int parentHeightMeasureSpec, int heightUsed) {
19         final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
20 
21         final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
22                 mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
23                         + widthUsed, lp.width);
24         final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
25                 mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
26                         + heightUsed, lp.height);
27 
28         child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
29     }

        对比这两个方法可以发现,它们非常相似,从注释上来看,后者在前者的基础上增加了已经使用的宽高和margin值。其实它们的功能都是一样的,最后都是生成子View的MeasureSpec,并传递给子View继续测量,即最后一句代码child.measure(childWidthMeasureSpec, childHeightMeasureSpec)。一般根据容器自身的需要来选择其中一个,比如,在FrameLayout和LinearLayout中重写的onMeasure方法中调用的就是后者,而AbsoluteLayout中就是间接地调用的前者。而RelativeLayout中,两者都没有调用,而是自己写了一套方法,不过该方法和后者方法仅略有差别,但基本功能还是一样,读者可以自己去看看它们的源码,这里就不贴出来了。

    (2)getChildMeasureSpec()方法

       前两个方法中都用到了这个方法,它很重要,它用于将父布局传递来的MeasureSpec和其子view的LayoutParams,整合为一个最有可能的子View的MeasureSpec。

 1 //==================ViewGroup.java====================
 2  /**
 3      * Does the hard part of measureChildren: figuring out the MeasureSpec to
 4      * pass to a particular child. This method figures out the right MeasureSpec
 5      * for one dimension (height or width) of one child view.
 6      *
 7      * The goal is to combine information from our MeasureSpec with the
 8      * LayoutParams of the child to get the best possible results. For example,
 9      * if the this view knows its size (because its MeasureSpec has a mode of
10      * EXACTLY), and the child has indicated in its LayoutParams that it wants
11      * to be the same size as the parent, the parent should ask the child to
12      * layout given an exact size.
13      *
14      * @param spec The requirements for this view
15      * @param padding The padding of this view for the current dimension and
16      *        margins, if applicable
17      * @param childDimension How big the child wants to be in the current
18      *        dimension
19      * @return a MeasureSpec integer for the child
20      */
21     public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
22         int specMode = MeasureSpec.getMode(spec);
23         int specSize = MeasureSpec.getSize(spec);
24 
25         int size = Math.max(0, specSize - padding);
26 
27         int resultSize = 0;
28         int resultMode = 0;
29 
30         switch (specMode) {
31         // Parent has imposed an exact size on us
32         case MeasureSpec.EXACTLY:
33             if (childDimension >= 0) {
34                 resultSize = childDimension;
35                 resultMode = MeasureSpec.EXACTLY;
36             } else if (childDimension == LayoutParams.MATCH_PARENT) {
37                 // Child wants to be our size. So be it.
38                 resultSize = size;
39                 resultMode = MeasureSpec.EXACTLY;
40             } else if (childDimension == LayoutParams.WRAP_CONTENT) {
41                 // Child wants to determine its own size. It can't be
42                 // bigger than us.
43                 resultSize = size;
44                 resultMode = MeasureSpec.AT_MOST;
45             }
46             break;
47 
48         // Parent has imposed a maximum size on us
49         case MeasureSpec.AT_MOST:
50             if (childDimension >= 0) {
51                 // Child wants a specific size... so be it
52                 resultSize = childDimension;
53                 resultMode = MeasureSpec.EXACTLY;
54             } else if (childDimension == LayoutParams.MATCH_PARENT) {
55                 // Child wants to be our size, but our size is not fixed.
56                 // Constrain child to not be bigger than us.
57                 resultSize = size;
58                 resultMode = MeasureSpec.AT_MOST;
59             } else if (childDimension == LayoutParams.WRAP_CONTENT) {
60                 // Child wants to determine its own size. It can't be
61                 // bigger than us.
62                 resultSize = size;
63                 resultMode = MeasureSpec.AT_MOST;
64             }
65             break;
66 
67         // Parent asked to see how big we want to be
68         case MeasureSpec.UNSPECIFIED:
69             if (childDimension >= 0) {
70                 // Child wants a specific size... let him have it
71                 resultSize = childDimension;
72                 resultMode = MeasureSpec.EXACTLY;
73             } else if (childDimension == LayoutParams.MATCH_PARENT) {
74                 // Child wants to be our size... find out how big it should
75                 // be
76                 resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
77                 resultMode = MeasureSpec.UNSPECIFIED;
78             } else if (childDimension == LayoutParams.WRAP_CONTENT) {
79                 // Child wants to determine its own size.... find out how
80                 // big it should be
81                 resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
82                 resultMode = MeasureSpec.UNSPECIFIED;
83             }
84             break;
85         }
86         //noinspection ResourceType
87         return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
88     }

 咱们依然先翻译和整理一下开头的注释:

      1)处理measureChildren的困难部分:计算出Measure传递给指定的child。该方法计算出一个子view的宽或高的正确MeasureSpec。

      2)其目的是组合来自我们MeasureSpec的信息和child的LayoutParams来得到最有可能的结果。比如:如果该view知道它的尺寸(因为它的MeasureSpec的mode为EXACTLY),并且它的child在它的LayoutParams中表示它想和父布局有一样大,那么父布局应该要求该child按照精确的尺寸进行布局。

      3)参数spec:对该view的要求(笔者注:父布局对当前child的MeasureSpec要求)

      4)参数padding:该view宽/高的padding和margins值,如果可应用的话。

      5)参数childDimension:该child在宽/高上希望多大。

      6)返回:返回该child的MeasureSpec整数。

       如果明白了前文中对MeasureSpec的介绍后,这一部分的代码应该就容易理解了,specMode的三种值,LayoutParams的width和height的三种值,以及和layout_width、layout_height之间的关对应关系,在文章的开头已经介绍过了,不明白的可以再回头复习一下。specMode和specSize分别是父布局传下来的要求,size的值是父布局尺寸要求减去其padding值,最小不会小于0。代码最后就是将重新得到的mode和size组合生成一个新的MeasureSpec,传递给子View,一直递归下去,该方法也在前面讲过。本段代码重难点就是这里新mode和新size值的确定,specMode和childDimension各有3种值,所以最后会有9种组合。如果对这段代码看不明白的,可以看看笔者对这段代码的解释(width和height同理,这里以width为例):

  • 如果specMode的值为MeasureSpec.EXACTLY,即父布局对子view的尺寸要求是一个精确值,这有两种情况,父布局中layout_width属性值被设置为具体值,或者match_parent,它们都被定义为精确值。针对childDimension的值

          i)childDimension也为精确值时。它是LayoutParams中width属性,是一个具体值,不包括match_parent情况,这个一定要和MeasureSpec中的精确值EXACTLY区别开来。此时resultSize为childDimension的精确值,resultMode理所当然为MeasureSpec.EXACTLY。这里不知道读者会不会又疑问,如果子View的layout_width值比父布局的大,那这个结论还成立吗?按照我们的经验,似乎不太能理解,因为子view的宽度再怎么样也不会比父布局大。事实上,我们平时经验看到的,是最后布局后绘制出来的结果,而当前步骤为测量值,是有差别的。读者可以自定义一个View,将父布局layout_width设置为100px,该自定义的子view则设置为200px,然后在子view中重写的onMeasure方法中打印出getMeasuredWidth()值看看,其值一定是200。甚至如果子view设置的值超过屏幕尺寸,其打印值也是设置的值。

        ii)childDimension值为LayoutParams.MATCH_PARENT时。这个容易理解,它的尺寸和父布局一样,也是个精确值,所以resultSize为前面求出的size值,由父布局决定,resultMode为MeasureSpec.EXACTLY。

        iii)childDimension值为LayoutParams.WRAP_CONTENT时。当子view的layout_width被设置为wrap_content时,即使最后我们肉眼看到屏幕上真正显示出来的控件很小,但在测量时和父布局一样的大小。这一点仍然可以通过打印getMeasuredWidth值来理解。所以一定不要被“经验”所误。所以resultSize值为size大小,resultMode为MeasureSpec.AT_MOST。

  • 如果specMode值为MeasureSpec.AT_MOST。其对应于layout_width为wrap_content,此时,我们可以想象到,子View对结果的决定性很大。

        i)childDimension为精确值时。很容易明确specSize为自身的精确值,specMode为MeasureSpec.EXACTLY。

        ii)childDimension为LayoutParams.MATCH_PARENT时。specSize由父布局决定,为size;specMode为MeasureSpec.AT_MOST。

        iii)childDimension为LayoutParams.WRAP_CONTENT时。specSize由父布局决定,为size;specMode为MeasureSpec.AT_MOST。

  • 如果specMode值为MeasureSpec.UNSPECIFIED。前面说过,平时很少用,一般用在系统中,不过这里还是简单说明一下。这一段有个变量View.sUseZeroUnspecifiedMeasureSpec,它是用于表示当前的目标api是否低于23(对应系统版本为Android M)的,低于23则为true,否则为false。现在系统版本基本上都是Android M及以上的,所以这里该值我们当成false来处理。

        i)childDimension为精确值时。很容易明确specSize为自身的精确值,specMode为MeasureSpec.EXACTLY。

        ii)childDimension为LayoutParams.MATCH_PARENT时。specSize由父布局决定,为size;specMode和父布局一样,为MeasureSpec.UNSPECIFIED。

        iii)childDimension为LayoutParams.WRAP_CONTENT时。specSize由父布局决定,为size;specMode和父布局一样,为MeasureSpec.UNSPECIFIED。

       这个方法对理解测量时MeasureSpec的传递过程非常重要,并且需要记忆和理解的内容也不少,所以这里花的篇幅比较多。

 

       通过这一节,我们介绍了ViewGroup在测量过程中要用到的方法。通过这些方法,我们更加深入理解了测量过程中ViewGroup是如何测量子View的了。

 

  6、DecorView测量的大致流程

       前面我们提到过DecorView的继承链:DecorView extends FrameLayout extends ViewGroup extends View。所以在这个继承过程中一定会有子类重写onMeasure方法,当DecorView第一次调用到measure()方法后,流程就开始切换到重写的onMeasure()中了。我们按照这个继承顺序看看measure流程的相关源码:

 1 //=============DecorView.java=============
 2 @Override
 3 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
 4        ......
 5     super.onMeasure(widthMeasureSpec, heightMeasureSpec);
 6        ......
 7 }
 8 
 9 //=============FrameLayout.java=============
10 @Override
11 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
12   int count = getChildCount();
13   for (int i = 0; i < count; i++) {
14        final View child = getChildAt(i);
15        ......
16        measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
17        ......             
18    }
19    ......
20    setMeasuredDimension(......)
21    ...... }

       第16行中measureChildWithMargins()方法是ViewGroup提供的方法,前面我们介绍过了。从上述FrameLayout中重写的onMeasure方法中可以看到,是先把子view测量完成后,最后才去调用setMeasuredDimension(...)来测量自己的。事实上,整个测量过程就是从子view开始测量,然后一层层往上再测量父布局,直到DecorView为止的。

       可能到这里有些读者会有个疑问,DecorView中onMeasure方法的参数值是从哪里传过来的呢?呵呵,前面花了很大的篇幅,就在不断地讲它俩,这里再强调啰嗦一次:

1 //=====================ViewRootImpl.java=================
2 private void performTraversals() {
3    ......
4    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
5    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);      
6    ......
7 }

 如果还是不明白,回过头去再看看这部分的说明吧,这里就不再赘述了。

 

  7、DecorView视图树的简易measure流程图

        到目前为止,DecorView的整个测量流程就接上了,从ViewRootImpl类的performTraversals()开始,经过递归遍历,最后到叶子view测量结束,DecorView视图树的测量就完成了。这里再用一个流程图简单描述一下整个流程:

  

 

       在这一节的最后,推荐一篇博文,这里面有个非常详细的案例分析,如何一步一步从DecorView开始遍历,到整个View树测量完成,以及如何测量出每个view的宽高值:【Android View的绘制流程:https://www.jianshu.com/p/5a71014e7b1b?from=singlemessage】Measure过程的第4点。认真分析完该实例,一定会对测量过程有个更深刻的认识。

 

六、layout过程分析

       当measure过程完成后,接下来就会进行layout阶段,即布局阶段。在前面measure的作用是测量每个view的尺寸,而layout的作用是根据前面测量的尺寸以及设置的其它属性值,共同来确定View的位置。

  1、performLayout方法引出DecorView的布局流程

       测量完成后,会在ViewRootImpl类的performTraverserals()方法中,开始调用performLayout方法:

performLayout(lp, mWidth, mHeight);

       传入该方法的参数我们在上一节中已经分析过了,lp中width和height均为LayoutParams.MATCH_PARENT,mWidth和mHeight分别为屏幕的宽高。

1 //=====================ViewRootImpl.java===================
2 private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
3             int desiredWindowHeight) {
4    ......
5    final View host = mView;
6    ......
7    host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
8    ......
9 }

       mView的值上一节也讲过,就是DecorView,布局流程也是从DecorView开始遍历和递归。

 

  2、layout方法正式启动布局流程

       由于DecorView是一个容器,是ViewGroup子类,所以跟踪代码的时候,实际上是先进入到ViewGroup类中的layout方法中。

 1 //==================ViewGroup.java================
 2     @Override
 3     public final void layout(int l, int t, int r, int b) {
 4         if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
 5             if (mTransition != null) {
 6                 mTransition.layoutChange(this);
 7             }
 8             super.layout(l, t, r, b);
 9         } else {
10             // record the fact that we noop'd it; request layout when transition finishes
11             mLayoutCalledWhileSuppressed = true;
12         }
13     }

       这是一个final类型的方法,所以自定义 的ViewGroup子类无法重写该方法,可见系统不希望自定义的ViewGroup子类破坏layout流程。继续追踪super.layout方法,又跳转到了View中的layout方法。

 1 //=================View.java================
 2  /**
 3      * Assign a size and position to a view and all of its
 4      * descendants
 5      *
 6      * <p>This is the second phase of the layout mechanism.
 7      * (The first is measuring). In this phase, each parent calls
 8      * layout on all of its children to position them.
 9      * This is typically done using the child measurements
10      * that were stored in the measure pass().</p>
11      *
12      * <p>Derived classes should not override this method.
13      * Derived classes with children should override
14      * onLayout. In that method, they should
15      * call layout on each of their children.</p>
16      *
17      * @param l Left position, relative to parent
18      * @param t Top position, relative to parent
19      * @param r Right position, relative to parent
20      * @param b Bottom position, relative to parent
21      */
22     @SuppressWarnings({"unchecked"})
23     public void layout(int l, int t, int r, int b) {
24         ......
25         boolean changed = isLayoutModeOptical(mParent) ?
26                 setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);  
27         if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
28             onLayout(changed, l, t, r, b);
29             ......
30          }
31          ......
32 }

 先翻译一下注释中对该方法的描述:

      1)给view和它的所有后代分配尺寸和位置。

      2)这是布局机制的第二个阶段(第一个阶段是测量)。在这一阶段中,每一个父布局都会对它的子view进行布局来放置它们。一般来说,该过程会使用在测量阶段存储的child测量值。

      3)派生类不应该重写该方法。有子view的派生类(笔者注:也就是容器类,父布局)应该重写onLayout方法。在重写的onLayout方法中,它们应该为每一子view调用layout方法进行布局。

      4)参数依次为:Left、Top、Right、Bottom四个点相对父布局的位置。

 

  3、setFrame方法真正执行布局任务

       在上面的方法体中,我们先重点看看setFrame方法。至于setOpticalFrame方法,其中也是调用的setFrame方法。

 1 //=================View.java================
 2 /**
 3      * Assign a size and position to this view.
 4      *
 5      * This is called from layout.
 6      *
 7      * @param left Left position, relative to parent
 8      * @param top Top position, relative to parent
 9      * @param right Right position, relative to parent
10      * @param bottom Bottom position, relative to parent
11      * @return true if the new size and position are different than the
12      *         previous ones
13      * {@hide}
14      */
15     protected boolean setFrame(int left, int top, int right, int bottom) {
16         boolean changed = false;
17         ......
18         if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
19             changed = true;
20             ......
21             int oldWidth = mRight - mLeft;
22             int oldHeight = mBottom - mTop;
23             int newWidth = right - left;
24             int newHeight = bottom - top;
25             boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
26 
27             // Invalidate our old position
28             invalidate(sizeChanged);
29 
30             mLeft = left;
31             mTop = top;
32             mRight = right;
33             mBottom = bottom;
34             ......
35         }
36         return changed;
37  }

  注释中重要的信息有:

      1)该方法用于给该view分配尺寸和位置。(笔者注:也就是实际的布局工作是在这里完成的)

      2)返回值:如果新的尺寸和位置和之前的不同,返回true。(笔者注:也就是该view的位置或大小发生了变化)

       在方法体中,从第27行开始,对view的四个属性值进行了赋值,即mLeft、mTop、mRight、mBottom四条边界坐标被确定,表明这里完成了对该View的布局。

 

  4、onLayout方法让父布局调用对子view的布局

      再返回到layout方法中,会看到如果view发生了改变,接下来会调用onLayout方法,这和measure调用onMeasure方法类似。

 1 //============View.java============
 2 /**
 3      * Called from layout when this view should
 4      * assign a size and position to each of its children.
 5      *
 6      * Derived classes with children should override
 7      * this method and call layout on each of
 8      * their children.
 9      * @param changed This is a new size or position for this view
10      * @param left Left position, relative to parent
11      * @param top Top position, relative to parent
12      * @param right Right position, relative to parent
13      * @param bottom Bottom position, relative to parent
14      */
15     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
16     }

 先翻译一下关键注释:

      1)当该view要分配尺寸和位置给它的每一个子view时,该方法会从layout方法中被调用。

      2)有子view的派生类(笔者注:也就是容器,父布局)应该重写该方法并且为每一个子view调用layout。

       我们发现这是一个空方法,因为layout过程是父布局容器布局子view的过程,onLayout方法叶子view没有意义,只有ViewGroup才有用。所以,如果当前View是一个容器,那么流程会切到被重写的onLayout方法中。我们先看ViewGroup类中的重写:

1 //=============ViewGroup.java===========
2   @Override
3    protected abstract void onLayout(boolean changed,
4            int l, int t, int r, int b);

       进入到ViewGroup类中发现,该方法被定义为了abstract方法,所以以后凡是直接继承自ViewGroup类的容器,就必须要重写onLayout方法。 事实上,layout流程是绘制流程中必需的过程,而前面讲过的measure流程,其实可以不要,这一点等会再说。

       咱们先直接进入到DecorView中查看重写的onLayout方法。

1 //==============DecorView.java================
2  @Override
3  protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
4      super.onLayout(changed, left, top, right, bottom);
5      ......
6 }

       DecerView继承自FrameLayout,咱们继续到FrameLayout类中重写的onLayout方法看看。

 1 //================FrameLayout.java==============
 2     @Override
 3     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
 4         layoutChildren(left, top, right, bottom, false /* no force left gravity */);
 5     }
 6 
 7     void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
 8         final int count = getChildCount();
 9         ......
10         for (int i = 0; i < count; i++) {
11              final View child = getChildAt(i);
12              if (child.getVisibility() != GONE) {
13                  final LayoutParams lp = (LayoutParams) child.getLayoutParams();
14 
15                  final int width = child.getMeasuredWidth();
16                  final int height = child.getMeasuredHeight();
17                  ......
18                  child.layout(childLeft, childTop, childLeft + width, childTop + height);
19             }
20     }

       这里仅贴出关键流程的代码,咱们可以看到,这里面也是对每一个child调用layout方法的。如果该child仍然是父布局,会继续递归下去;如果是叶子view,则会走到view的onLayout空方法,该叶子view布局流程走完。另外,我们看到第15行和第16行中,width和height分别来源于measure阶段存储的测量值,如果这里通过其它渠道赋给width和height值,那么measure阶段就不需要了,这也就是我前面提到的,onLayout是必需要实现的(不仅会报错,更重要的是不对子view布局的话,这些view就不会显示了),而measure过程可以不要。当然,肯定是不建议这么做的,采用其它方式很实现我们要的结果。

 

  5、DecorView视图树的简易布局流程图

       如果是前面搞清楚了DecorView视图树的测量流程,那这一节的布局流程也就非常好理解了,咱们这里再简单梳理一下:

 

 

七、draw过程分析

       当layout完成后,就进入到draw阶段了,在这个阶段,会根据layout中确定的各个view的位置将它们画出来。该过程的分析思路和前两个过程类似,如果前面读懂了,那这个流程也就很容易理解了。

  1、从performDraw方法到draw方法

       draw过程,自然也是从performTraversals()中的performDraw()方法开始的,咱们从该方法追踪,咱们这里仅贴出关键流程代码,至于其它的逻辑,不是本文的重点,这里就先略过,有兴趣的可以自行研究。

 1 //==================ViewRootImpl.java=================
 2 private void performDraw() {
 3       ......
 4       boolean canUseAsync = draw(fullRedrawNeeded);
 5       ......
 6 }
 7 
 8 private boolean draw(boolean fullRedrawNeeded) {
 9       ......
10       if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
11                         scalingRequired, dirty, surfaceInsets)) {
12                     return false;
13                 }
14       ......
15 }
16 
17 private boolean drawSoftware(......){
18       ......
19       mView.draw(canvas);
20       ......
21 }

       前面我们讲过了,这mView就是DecorView,这样就开始了DecorView视图树的draw流程了。

  2、DecorView树递归完成“画”流程

       DecorView类中重写了draw()方法,追踪源码后进入到该部分。

1 //================DecorView.java==============
2 @Override
3 public void draw(Canvas canvas) {
4      super.draw(canvas);
5 
6      if (mMenuBackground != null) {
7          mMenuBackground.draw(canvas);
8      }
9 }

       从这段代码来看, 调用完super.draw后,还画了菜单背景,当然super.draw是咱们关注的重点,这里还做了啥咱们不用太关心。由于FrameLayout和ViewGroup都没有重写该方法,所以就直接进入都了View类中的draw方法了。

 1 //====================View.java===================== 
 2  /**
 3      * Manually render this view (and all of its children) to the given Canvas.
 4      * The view must have already done a full layout before this function is
 5      * called.  When implementing a view, implement
 6      * {@link #onDraw(android.graphics.Canvas)} instead of overriding this method.
 7      * If you do need to override this method, call the superclass version.
 8      *
 9      * @param canvas The Canvas to which the View is rendered.
10      */
11     @CallSuper
12     public void draw(Canvas canvas) {
13        ......
14         /*
15          * Draw traversal performs several drawing steps which must be executed
16          * in the appropriate order:
17          *
18          *      1. Draw the background
19          *      2. If necessary, save the canvas' layers to prepare for fading
20          *      3. Draw view's content
21          *      4. Draw children
22          *      5. If necessary, draw the fading edges and restore layers
23          *      6. Draw decorations (scrollbars for instance)
24          */
25 
26         // Step 1, draw the background, if needed
27         int saveCount;
28 
29         if (!dirtyOpaque) {
30             drawBackground(canvas);
31         }
32 
33         // skip step 2 & 5 if possible (common case)
34         ......
35         // Step 3, draw the content
36         if (!dirtyOpaque) onDraw(canvas);
37 
38         // Step 4, draw the children
39         dispatchDraw(canvas);
40         ......
41         // Step 6, draw decorations (foreground, scrollbars)
42         onDrawForeground(canvas);45         ......
43     }

      这段代码描述了draw阶段完成的7个主要步骤,这里咱们先翻译一下其注释:

      1)手动渲染该view(以及它的所有子view)到给定的画布上。

      2)在该方法调用之前,该view必须已经完成了全面的布局。当正在实现一个view是,实现onDraw(android.graphics.Cavas)而不是本方法。如果您确实需要重写该方法,调用超类版本。

      3)参数canvas:将view渲染到的画布。

      从代码上看,这里做了很多工作,咱们简单说明一下,有助于理解这个“画”工作。

      1)第一步:画背景。对应我我们在xml布局文件中设置的“android:background”属性,这是整个“画”过程的第一步,这一步是不重点,知道这里干了什么就行。

      2)第二步:画内容(第2步和第5步只有有需要的时候才用到,这里就跳过)。比如TextView的文字等,这是重点,onDraw方法,后面详细介绍。

      3)第三步:画子view。dispatchDraw方法用于帮助ViewGroup来递归画它的子view。这也是重点,后面也要详细讲到。

      4)第四步:画装饰。这里指画滚动条和前景。其实平时的每一个view都有滚动条,只是没有显示而已。同样这也不是重点,知道做了这些事就行。

       咱们进入onDraw方法看看

1 //=================View.java===============
2 /**
3      * Implement this to do your drawing.
4      *
5      * @param canvas the canvas on which the background will be drawn
6      */
7     protected void onDraw(Canvas canvas) {
8     }

 注释中说:实现该方法来做“画”工作。也就是说,具体的view需要重写该方法,来画自己想展示的东西,如文字,线条等。DecorView中重写了该方法,所以流程会走到DecorView中重写的onDraw方法。

1 //===============DocerView.java==============
2 @Override
3     public void onDraw(Canvas c) {
4         super.onDraw(c);
5       mBackgroundFallback.draw(this, mContentRoot, c, mWindow.mContentParent,
6         mStatusColorViewState.view, mNavigationColorViewState.view);
7  }

      这里调用了onDraw的父类方法,同时第4行还画了自己特定的东西。由于FrameLayout和ViewGroup也没有重写该方法,且View中onDraw为空方法,所以super.onDraw方法其实是啥都没干的。DocerView画完自己的东西,紧接着流程就又走到dispatchDraw方法了。

 1 //================View.java===============
 2 /**
 3      * Called by draw to draw the child views. This may be overridden
 4      * by derived classes to gain control just before its children are drawn
 5      * (but after its own view has been drawn).
 6      * @param canvas the canvas on which to draw the view
 7      */
 8     protected void dispatchDraw(Canvas canvas) {
 9 
10     }

       先看看注释:被draw方法调用来画子View。该方法可能会被派生类重写来获取控制,这个过程正好在该view的子view被画之前(但在它自己被画完成后)。

       也就是说当本view被画完之后,就开始要画它的子view了。这个方法也是一个空方法,实际上对于叶子view来说,该方法没有什么意义,因为它没有子view需要画了,而对于ViewGroup来说,就需要重写该方法来画它的子view。

       在源码中发现,像平时常用的LinearLayout、FrameLayout、RelativeLayout等常用的布局控件,都没有再重写该方法,DecorView中也一样,而是只在ViewGroup中实现了dispatchDraw方法的重写。所以当DecorView执行完onDraw方法后,流程就会切到ViewGroup中的dispatchDraw方法了。

 1 //=============ViewGroup.java============
 2  @Override
 3  protected void dispatchDraw(Canvas canvas) {
 4         final int childrenCount = mChildrenCount;
 5         final View[] children = mChildren;
 6         ......
 7         for (int i = 0; i < childrenCount; i++) {
 8             more |= drawChild(canvas, child, drawingTime);
 9             ......
10         }
11         ...... 
12  }

       从上述源码片段可以发现,这里其实就是对每一个child执行drawChild操作。

 1 /**
 2      * Draw one child of this View Group. This method is responsible for getting
 3      * the canvas in the right state. This includes clipping, translating so
 4      * that the child's scrolled origin is at 0, 0, and applying any animation
 5      * transformations.
 6      *
 7      * @param canvas The canvas on which to draw the child
 8      * @param child Who to draw
 9      * @param drawingTime The time at which draw is occurring
10      * @return True if an invalidate() was issued
11      */
12     protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
13         return child.draw(canvas, this, drawingTime);
14     }

先翻译注释的内容:

      1)画当前ViewGroup中的某一个子view。该方法负责在正确的状态下获取画布。这包括了裁剪,移动,以便子view的滚动原点为0、0,以及提供任何动画转换。

      2)参数drawingTime:“画”动作发生的时间点。

       继续追踪源码,进入到如下流程。

 1 //============View.java===========
 2 /**
 3      * This method is called by ViewGroup.drawChild() to have each child view draw itself.
 4      *
 5      * This is where the View specializes rendering behavior based on layer type,
 6      * and hardware acceleration.
 7      */
 8     boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
 9       ......
10       draw(canvas);
11       ......
12 }

       注释中说:该方法被ViewGroup.drawChild()方法调用,来让每一个子view画它自己。

       该方法中,又回到了draw(canvas)方法中了,然后再开始画其子view,这样不断递归下去,直到画完整棵DecorView树。

 

  3、DecorView视图树的简易draw流程图

       针对上述的代码追踪流程,这里梳理了DecorView整个view树的draw过程的关键流程,其中节点比较多,需要耐心分析。

      

       到目前为止,View的绘制流程就介绍完了。根节点是DecorView,整个View体系就是一棵以DecorView为根的View树,依次通过遍历来完成measure、layout和draw过程。而如果要自定义view,一般都是通过重写onMeasure(),onLayout(),onDraw()来完成要自定义的部分,整个绘制流程也基本上是围绕着这几个核心的地方来展开的。

 

八、博文参考阅读

       【Android View视图层次

       【Android进阶 - 视图层级实时分析

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

       【Android图形系统(三)-View绘制流程

       【Android View的绘制流程

       【Android View的绘制流程

 

结语

       本文的篇幅比较长,能看完并且理解也是一件辛苦的事情,笔者学习及写这篇博客,也是花了将近半个月的业余时间来完成的。但是要想超过别人,就是要做一件有一件辛苦但能够成长的事情,时间长了,人与人之间的距离就拉开了。所以,真心希望本文能帮助您理解View的绘制流程,那笔者半个月来的辛苦也就没有白费了。当然,本文肯定存在很多不足之处,希望读者能不吝赐教,共同进步。

posted @ 2019-05-31 14:25  宋者为王  阅读(40214)  评论(2编辑  收藏  举报