Android视图绘制流程完全解析(一)

这篇博客是对郭神的Android视图绘制流程解析系列文章的学习总结以及个人见解。
郭霖大神的原创博客:
Android视图绘制流程完全解析,带你一步步深入了解View(二)
继续膜拜大神~


在我们踏上Android开发之旅时,从最开始接触的”Hello world”到能熟悉运用各种控件,我们都在与View打交道。在Android里,View就是所有布局,所有的控件的基类,所以不管是Android中的任何一个布局、任何一个控件,其实都是直接或间接继承自View的。如果你想玩各种各样的高级控件,或者自定义View等等进阶技能,就有必要了解一下View的绘制流程。

一、概括
任何一个视图View的绘制过程,都要经历最重要的三个阶段:首先是测量阶段onMeasure(),接着是布局阶段onLayout(),最后是绘制阶段onDraw()。View系统的绘制流程会从ViewRoot的performTraversals()方法中开始。简略代码如下:

private void performTraversals() {
        ......
        //测量阶段
        host.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        ......
        //布局阶段
        host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
        ......
        //绘制阶段
        host.draw(canvas);
        ......
    }

而且,在视图View的绘制过程中,视图View要和它所属布局ViewGroup共同完成这一个绘制过程。所以,我们先来看看视图View绘制过程的第一阶段测量阶段onMeasure()。

二、onMeasure()
在介绍测量过程之前,首先先为大家介绍一下MeasureSpec类。在上面代码中,host.measure(childWidthMeasureSpec, childHeightMeasureSpec),measure()方法接收两个参数,childWidthMeasureSpecchildHeightMeasureSpec。它俩是详细测量值(measureSpec),是父视图经过计算得出的,分别用于给子视图的宽度和高度的规格和参考大小。(为什么是参考大小,等下会有答案。)。
对于详细测量值(measureSpec),需要两样东西来确定它,那就是大小(specSize)和模式(specMode)。而measureSpec,specSize,specMode他们三个的关系,都封装在View类中的一个内部类里,名叫MeasureSpec。详细测量值measureSpec由specSize和specMode共同组成的,其中specSize记录的是大小,specMode记录的是规格。specMode一共有三种类型,如下所示(引用自郭霖博客):

1. EXACTLY
表示父视图希望子视图的大小应该是由specSize的值来决定的,系统默认会按照这个规则来设置子视图的大小,开发人员当然也可以按照自己的意愿设置成任意的大小。
2. AT_MOST
表示子视图最多只能是specSize中指定的大小,开发人员应该尽可能小得去设置这个视图,并且保证不会超过specSize。系统默认会按照这个规则来设置子视图的大小,开发人员当然也可以按照自己的意愿设置成任意的大小。
3. UNSPECIFIED
表示开发人员可以将视图按照自己的意愿设置成任意的大小,没有任何限制。这种情况比较少见,不太会用到。

好了,介绍完MeasureSpec,接下来让我们看看视图View测量阶段。
如上面的代码所示,在测量阶段,首先,performTraversals()方法中调用View的measure()方法。注意,measure方法是final的,不允许重写的。在measure()方法内部,调用了onMeasure()方法。onMeasure()方法才是真正去测量并设置View大小的地方,我们可以通过重载onMeasure来实现自己的测量逻辑。让我们看一下onMeasure()的源码:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

先不管setMeasuredDimension(),我们先来看看setMeasuredDimension()里面的参数。系统默认用getDefaultSize()方法来获取视图的大小,来看看getDefaultSize()方法的源码:

public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }

这个方法很简单,接受两个参数,其中一个就是measureSpec。分别调用MeasureSpec.getMode()方法和MeasureSpec.getSize()方法解析出specMode和specSize。然后根据specMode来判断具体返回的值。另一个参数是size,这个size是什么?我们再倒回去看一下getDefaultSize()方法的第一个参数。size是由getSuggestedMinimumWidth()方法所得(对应的还有getSuggestedMinimumHeight())。让我们看一下这个方法:

protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }

这个方法就一行代码,根据判断View的Background的有无和比较View的Background尺寸和View的最小宽度(高度)返回该View的最大宽度(高度)。所以再看回getDefaultSize()方法,当我们View是自己自定义大小时,也就是属于MeasureSpec.UNSPECIFIED这个模式时,返回的size就是我们自定义的大小,否则就是返回specSize,也就是父视图传递给子视图的参考大小。所以说,每一个视图View的实际宽高都是由它的父视图和它本身一起确定的,也是前面所讲到为什么specSize是参考大小的原因。到了现在,逻辑是否清晰很多了?
再看回onMeasure()的源码:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

setMeasuredDimension()方法是用来设定视图的测量宽高,测量宽高是调用getDefaultSize()方法获得的。setMeasuredDimension()方法也就是对View的成员变量mMeasuredWidth和mMeasuredHeight变量进行赋值。这一步骤之后,调用getMeasuredWidth()和getMeasuredHeight()才能获取得到视图测量出的宽高。在这之前调用这两个方法获取得到的值都会是0。

到了这里,一次最基础的View的measure测量过程就完成了。其实,View也能嵌套View的,也就是ViewGroup。一个布局ViewGroup中一般都会包含多个子视图,每个视图都需要经历一次measure过程。所以我们来看一下ViewGroup中测量方法。
ViewGroup中有个measureChildren()方法,用来测量子视图的大小。源码如下:

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {  
    final int size = mChildrenCount;  
    final View[] children = mChildren;  
    for (int i = 0; i < size; ++i) {  
        final View child = children[i];  
        if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {  
            measureChild(child, widthMeasureSpec, heightMeasureSpec);  
        }  
    }  
} 

很简单,遍历当前布局下所有子视图,然后逐个调用measureChild()方法来测量相应子视图的大小。看看measureChild()的源码:

protected void measureChild(View child, int parentWidthMeasureSpec,  
        int parentHeightMeasureSpec) {  
    final LayoutParams lp = child.getLayoutParams();  
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,  
            mPaddingLeft + mPaddingRight, lp.width);  
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,  
            mPaddingTop + mPaddingBottom, lp.height);  
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);  
} 

调用getChildMeasureSpec()方法计算子视图的MeasureSpec,然后再调用子视图的measure()方法,并把计算出的MeasureSpec传递进去。接下来就是前面所说的View的测量过程了。

好了,视图绘制流程的第一阶段测量阶段结束。


二、onLayout()

第一阶段测量阶段结束后,接下来是onLayout()布局阶段,很简单的逻辑。

private void performTraversals() {
        ......
        //测量阶段
        host.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        ......
        //布局阶段
        host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
        ......
        //绘制阶段
        host.draw(canvas);
        ......
    }

在ViewRoot的performTraversals()方法里调用View的layout()方法来执行布局过程,layout()方法接收四个参数,分别对应着左、上、右、下的坐标(这个坐标是相对于当前视图的父视图而言的)。
1. layout()方法会首先判断该视图View大小是否发生变化,没有就没必要对当前视图进行重绘。
2. 然后再调用View的onLayout()进行布局。但View的onLayout()方法是一个空方法,因为onLayout()过程是为了确定视图在布局中所在的位置,而这个操作应该是由布局来完成的,即父视图决定子视图的显示位置。所以布局的操作会在ViewGroup的onLayout()里面。
然而ViewGroup中的onLayout()方法是一个抽象方法。这就说明,所有ViewGroup的子类都必须重写这个方法。比如像LinearLayout、RelativeLayout等布局,都是重写了这个方法,然后在内部按照各自的规则对子视图进行布局的。让我们尝试自定义一个布局,借此来更深刻地理解onLayout()的过程。(代码引用自郭林大神博客)

public class SimpleLayout extends ViewGroup {    
    public SimpleLayout(Context context, AttributeSet attrs) {  
        super(context, attrs);  
    } 
    @Override  
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);  
        if (getChildCount() > 0) {  
            View childView = getChildAt(0);  
            measureChild(childView, widthMeasureSpec, heightMeasureSpec);  
        }  
    }  
    @Override  
    protected void onLayout(boolean changed, int l, int t, int r, int b) {  
        if (getChildCount() > 0) {  
            View childView = getChildAt(0);  
            childView.layout(0, 0, childView.getMeasuredWidth(), childView.getMeasuredHeight());  
        }  
    }  
}  

代码非常的简单,很简单的逻辑。onMeasure()方法会在onLayout()方法之前调用,所以先在onMeasure()方法中测量出子视图的大小。
接着在onLayout()方法中同样判断SimpleLayout是否有包含一个子视图,然后调用这个子视图的layout()方法来确定它在SimpleLayout布局中的位置。
所以在布局阶段,我们可以自己自定义ViewGroup并重写onLayout()方法,自己定义布局规则;也可以直接调用Android定义好的规则,比如像LinearLayout、RelativeLayout等布局。

在平常开发中,有时我们需要获取View视图的宽高,其中会碰到以下的方法:
1. getWidth()和getHeight()
2. getMeasureWidth()和getMeasureHeight()
或许在这之前,你还会疑惑它们有什么区别?但你了解了这onMeasure()和onLayout()过程后就能清楚分辨出来。
1. getMeasureWidth()和getMeasureHeight()方法是在measure()过程结束后才可以获取得到。前面也说过了,这两个方法中的值是通过setMeasuredDimension()方法来进行设置的。
2. getWidth()和getHeight()方法是在layout()过程结束后才可以获取得到。getWidth()方法中的值则是通过视图右边的坐标减去左边的坐标计算出来的,也就是在View的layout()方法中right坐标减去left坐标。同理getHeight()也是。

好了,onLayout()布局阶段结束。


三、onDraw()

private void performTraversals() {
        ......
        //测量阶段
        host.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        ......
        //布局阶段
        host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
        ......
        //绘制阶段
        host.draw(canvas);
        ......
    }

接在layout()布局阶段之后,便是draw()绘制阶段。
draw()方法内部的绘制过程总共可以分为六步:
1. 绘制该View的背景
2. 为绘制渐变框做一些准备操作
3. 调用onDraw()方法绘制视图本身
4. 调用dispatchDraw()方法绘制每个子视图,dispatchDraw()已经在Android框架中实现了,在ViewGroup方法中。(应用程序程序一般不需要重写该方法,但可以捕获该方法的发生,做一些特别的事情。)
5. 绘制渐变框
6. 绘制滚动条
其中郭神说,第二步和第五步在一般情况下很少用到。我们先忽略了第二步和第五步。(看了源码,略复杂。)

第一步:
首先绘制视图View的背景。这里会根据layout()布局过程确定的视图位置来确定绘制的位置。然后再调用Drawable的draw()方法来完成背景的绘制工作。
第三步:
这一步是对视图的内容进行绘制。这部分功能由子视图去完成,因为每个视图的内容部分肯定都是各不相同的,所以也是我们自定义view的时候,需要重写onDraw()方法进行绘制。而且,View默认不会绘制任何内容,真正的绘制都需要自己在子视图中实现。比如像系统提供的控件TextView、ImageView等,它们都有重写onDraw()这个方法,并且在里面执行了相当不少的绘制逻辑。
第四步:
这一步是对当前视图的所有子视图进行绘制。如果当前的视图没有子视图,那么也就不需要进行绘制了。这部分功能是在ViewGroup的dispatchDraw()方法中有具体的绘制代码实现。
第六步:
这一步是对视图的滚动条进行绘制。


在整个视图的绘制过程中,重点在onMeasure()测量过程的逻辑,理解之后,后面两个阶段都很好理解了。而且,在我们自己自定义View时,后两个阶段都会有我们自己的逻辑规则的。所以理解onMeasure()测量过程的逻辑是重点!!!
到此为止,视图绘制流程的第三阶段、整个视图的绘制过程就全部结束了!!!全部结束了!!!!

你可能感兴趣的:(Android学习之路)