View的绘制流程

View的绘制流程:Measure()——>Layout()——>Draw()

各步骤的主要工作:

Measure():测量视图大小。从顶层父View到子View递归调用measure方法,measure方法又回调OnMeasure。

Layout():确定View位置,进行页面布局。从顶层父View向子View的递归调用view.layout方法的过程,即父View根据上一步measure子View所得到的布局大小和布局参数,将子View放在合适的位置上。

Draw():绘制视图。六个步骤:

①、绘制视图的背景;drawBackground(canvas);

②、保存画布的图层(Layer);

③、绘制View的内容;onDraw(canvas);

④、绘制View子视图,如果没有就不用;dispatchDraw(canvas);

⑤、还原图层(Layer);

⑥、绘制前景,滚动条等。onDrawForeground(canvas); 

 

带着问题来思考整个measure过程。

1、系统为什么要有measure过程?

        开发人员在绘制UI的时候,基本都是通过XML布局文件的方式来配置UI,而每个View必须要设置的两个群属性就是layout_width和layout_height,这两个属性代表着当前View的尺寸。

官方文档截图:

View的绘制流程

        所以这两个属性的值是必须要指定的,这两个属性的取值只能为三种类型:

                 1、固定的大小,比如100dp。

                 2、刚好包裹其中的内容,wrap_content。

                 3、想要和父布局一样大,match_parent / fill_parent。

        由于Android希望提供一个更优雅的GUI框架,所以提供了自适应的尺寸,也就是 wrap_content 和 match_parent 。

        试想一下,那如果这些属性只允许设置固定的大小,那么每个View的尺寸在绘制的时候就已经确定了,所以可能都不需要measure过程。但是由于需要满足自适应尺寸的机制,所以需要一个measure过程。

 

2、measure过程都干了点什么事?

        由于上面提到的自适应尺寸的机制,所以在用自适应尺寸来定义View大小的时候,View的真实尺寸还不能确定。但是View尺寸最终需要映射到屏幕上的像素大小,所以measure过程就是干这件事,把各种尺寸值,经过计算,得到具体的像素值。measure过程会遍历整棵View树,然后依次测量每个View真实的尺寸。具体是每个ViewGroup会向它内部的每个子View发送measure命令,然后由具体子View的onMeasure()来测量自己的尺寸。最后测量的结果保存在View的mMeasuredWidth和mMeasuredHeight中,保存的数据单位是像素。

 

3、对于自适应的尺寸机制,如何合理的测量一颗View树?

        系统在遍历完布局文件后,针对布局文件,在内存中生成对应的View树结构,这个时候,整棵View树种的所有View对象,都还没有具体的尺寸,因为measure过程最终是要确定每个View打的准确尺寸,也就是准确的像素值。但是刚开始的时候,View中layout_width和layout_height两个属性的值,都只是自适应的尺寸,也就是match_parent和wrap_content,这两个值在系统中为负数,所以系统不会把它们当成具体的尺寸值。所以当一个View需要把它内部的match_parent或者wrap_content转换成具体的像素值的时候,他需要知道两个信息。

        1、针对于match_parent,父布局当前具体像素值是多少,因为match_parent就是子View想要和父布局一样大。

        2、针对wrap_content,子View需要根据当前自己内部的content,算出一个合理的能包裹所有内容的最小值。但是如果这个最小值比当前父布局还大,那不行,父布局会告诉你,我只有这么大,你也不应该超过这个尺寸。

        由于树这种数据结构的特殊性,我们在研究measure的过程时,可以只研究一个ViewGroup和2个View的简单场景。大概示意图如下:

View的绘制流程

        也就是说,在measure过程中,ViewGroup会根据自己当前的状况,结合子View的尺寸数据,进行一个综合评定,然后把相关信息告诉子View,然后子View在onMeasure自己的时候,一边需要考虑到自己的content大小,一边还要考虑的父布局的限制信息,然后综合评定,测量出一个最优的结果。

 

4、那么ViewGroup是如何向子View传递限制信息的?

        谈到传递限制信息,那就是MeasureSpec类了,该类贯穿于整个measure过程,用来传递父布局对子View尺寸测量的约束信息。简单来说,该类就保存两类数据。

                1、子View当前所在父布局的具体尺寸。

                2、父布局对子View的限制类型。

        那么限制类型又分为三种类型:

                1、UNSPECIFIED,不限定。意思就是,子View想要多大,我就可以给你多大,你放心大胆的measure吧,不用管其他的。也不用管我传递给你的尺寸值。(其实Android高版本中推荐,只要是这个模式,尺寸设置为0)

                2、EXACTLY,精确的。意思就是,根据我当前的状况,结合你指定的尺寸参数来考虑,你就应该是这个尺寸,具体大小在MeasureSpec的尺寸属性中,自己去查看吧,你也不要管你的content有多大了,就用这个尺寸吧。

                3、AT_MOST,最多的。意思就是,根据我当前的情况,结合你指定的尺寸参数来考虑,在不超过我给你限定的尺寸的前提下,你测量一个恰好能包裹你内容的尺寸就可以了。

 

源代码分析

        在View的源代码中,提取到了下面一些关于measure过程的信息。

        我们知道,整棵View树的根节点是DecorView,它是一个FrameLayout,所以它是一个ViewGroup,所以整棵View树的测量是从一个ViewGroup对象的measure方法开始的。

 

View:

1、measure

/** 开始测量一个View有多大,parent会在参数中提供约束信息,实际的测量工作是在onMeasure()中进行的,该方法会调用onMeasure()方法,所以只有onMeasure能被也必须要被override */
public final void measure(int widthMeasureSpec, int heightMeasureSpec);

父布局会在自己的onMeasure方法中,调用child.measure ,这就把measure过程转移到了子View中。

 

2、onMeasure


/** 具体测量过程,测量view和它的内容,来决定测量的宽高(mMeasuredWidth  mMeasuredHeight )。该方法中必须要调用setMeasuredDimension(int, int)来保存该view测量的宽高。 */
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec);

子View会在该方法中,根据父布局给出的限制信息,和自己的content大小,来合理的测量自己的尺寸。

 

3、setMeasuredDimension

 
/** 保存测量结果 */
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight);

当View测量结束后,把测量结果保存起来,具体保存在mMeasuredWidth和mMeasuredHeight中。

 


ViewGroup:

1、measureChildren

/** 让所有子view测量自己的尺寸,需要考虑当前ViewGroup的MeasureSpec和Padding。跳过状态为gone的子view */
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec);-->getChildMeasureSpec()-->child.measure();

测量所有的子View尺寸,把measure过程交到子View内部。

 

2、measureChild

/** 测量单个View,需要考虑当前ViewGroup的MeasureSpec和Padding。 */
protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec);-->getChildMeasureSpec()-->child.measure();

对每一个具体的子View进行测量。

 

3、measureChildWithMargins

 

/** 测量单个View,需要考虑当前ViewGroup的MeasureSpec和Padding、margins。 */
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed);-->getChildMeasureSpec()-->child.measure();

对每一个具体的子View进行测量。但是需要考虑到margin等信息。

 

4、getChildMeasureSpec


/** measureChildren过程中最困难的一部分,为child计算MeasureSpec。该方法为每个child的每个维度(宽、高)计算正确的MeasureSpec。目标就是把当前viewgroup的MeasureSpec和child的LayoutParams结合起来,生成最合理的结果。
比如,当前ViewGroup知道自己的准确大小,因为MeasureSpec的mode为EXACTLY,而child希望能够match_parent,这时就会为child生成一个mode为EXACTLY,大小为ViewGroup大小的MeasureSpec。
 */
public static int getChildMeasureSpec(int spec, int padding, int childDimension);

根据当前自身的状况,以及特定子View的尺寸参数,为特定子View计算一个合理的限制信息。

源代码:

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

 

伪代码:

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        获取限制信息中的尺寸和模式。
        switch (限制信息中的模式) {
            case 当前容器的父容器,给当前容器设置了一个精确的尺寸:
                if (子View申请固定的尺寸) {
                    你就用你自己申请的尺寸值就行了;
                } else if (子View希望和父容器一样大) {
                    你就用父容器的尺寸值就行了;
                } else if (子View希望包裹内容) {
                    你最大尺寸值为父容器的尺寸值,但是你还是要尽可能小的测量自己的尺寸,包裹你的内容就足够了;
                } 
                    break;
            case 当前容器的父容器,给当前容器设置了一个最大尺寸:
                if (子View申请固定的尺寸) {
                    你就用你自己申请的尺寸值就行了;
                } else if (子View希望和父容器一样大) {
                    你最大尺寸值为父容器的尺寸值,但是你还是要尽可能小的测量自己的尺寸,包裹你的内容就足够了;
                } else if (子View希望包裹内容) {
                    你最大尺寸值为父容器的尺寸值,但是你还是要尽可能小的测量自己的尺寸,包裹你的内容就足够了;
                } 
                    break;
            case 当前容器的父容器,对当前容器的尺寸不限制:
                if (子View申请固定的尺寸) {
                    你就用你自己申请的尺寸值就行了;
                } else if (子View希望和父容器一样大) {
                    父容器对子View尺寸不做限制。
                } else if (子View希望包裹内容) {
                    父容器对子View尺寸不做限制。
                }
                    break;
        } return 对子View尺寸的限制信息;
    }


        当自定义View的时候,也需要处理measure过程,主要有两种情况。

        1、继承自View的子类。

                需要覆写onMeasure来正确测量自己。最后都需要调用setMeasuredDimension来保存测量结果

                一般来说,自定义View的measure过程伪代码为:

int mode = MeasureSpec.getMode(measureSpec);
int size = MeasureSpec.getSize(measureSpec);

int viewSize = 0;

swith (mode) {
    case MeasureSpec.EXACTLY:
        viewSize = size; //当前View尺寸设置为父布局尺寸
        break;
    case MeasureSpec.AT_MOST:
        viewSize = Math.min(size, getContentSize()); //当前View尺寸为内容尺寸和父布局尺寸当中的最小值
        break;
    case MeasureSpec.UNSPECIFIED:
        viewSize = getContentSize(); //内容有多大,就设置尺寸为多大
        break;
    default:
        break;
}

setMeasuredDimension(viewSize);

        2、继承自ViewGroup的子类。

                不但需要覆写onMeasure来正确测量自己,可能还要覆写一系列measureChild方法,来正确的测量子view,比如ScrollView。或者干脆放弃父类实现的measureChild规则,自己重新实现一套测量子view的规则,比如RelativeLayout。最后都需要调用setMeasuredDimension来保存测量结果。

                一般来说,自定义ViewGroup的measure过程的伪代码为:

//ViewGroup开始测量自己的尺寸
viewGroup.onMeasure();
//ViewGroup为每个child计算测量限制信息(MeasureSpec)
viewGroup.getChildMeasureSpec();
//把上一步生成的限制信息,传递给每个子View,然后子View开始measure自己的尺寸
child.measure();
//子View测量完成后,ViewGroup就可以获取每个子View测量后的尺寸
child.getChildMeasuredSize();
//ViewGroup根据自己自身状况,比如Padding等,计算自己的尺寸
viewGroup.calculateSelfSize();
//ViewGroup保存自己的尺寸
viewGroupsetMeasuredDimension();

 

带着问题来思考整个layout过程。

1、系统为什么要有layout过程?

        View框架在经过第一步的measure过程后,成功计算了每一个View的尺寸。但是要成功的把View绘制到屏幕上,只有View的尺寸还不行,还需要准确的知道该View应该被绘制到什么位置。除此之外,对一个ViewGroup而言,还需要根据自己特定的layout规则,来正确的计算出子View的绘制位置,已达到正确的layout目的。这也就是layout过程的职责。

        该位置是View相对于父布局坐标系的相对位置,而不是以屏幕坐标系为准的绝对位置。这样更容易保持树型结构的递归性和内部自治性。而View的位置,可以无限大,超出当前ViewGroup的可视范围,这也是通过改变View位置而实现滑动效果的原理。

2、layout过程都干了点什么事?

        由于View是以树结构进行存储,所以典型的数据操作就是递归操作,所以,View框架中,采用了内部自治的layout过程。

        每个叶子节点根据父节点传递过来的位置信息,设置自己的位置数据,每个非叶子节点,除了负责根据父节点传递过来的位置信息,设置自己的位置数据外(如果有父节点的话),还需要根据自己内部的layout规则(比如垂直排布等),计算出每一个子节点的位置信息,然后向子节点传递layout过程。

        对于ViewGroup,除了根据自己的parent传递的位置信息,来设置自己的位置之外,还需要根据自己的layout规则,为每一个子View计算出准确的位置(相对于子View的父布局的位置)。

        对于View,根据自己的parent传递的位置信息,来设置自己的位置。

View的绘制流程

        View对象的位置信息,在内部是以4个成员变量的保存的,分别是mLeft、mRight、mTop、mBottom。他们的含义如图所示。

View的绘制流程

 

源代码分析

        在View的源代码中,提取到了下面一些关于layout过程的信息。

        我们知道,整棵View树的根节点是DecorView,它是一个FrameLayout,所以它是一个ViewGroup,所以整棵View树的测量是从一个ViewGroup对象的layout方法开始的。

 

View:

1、layout

/** 

分配一个位置信息到一个View上面,每个parent会调用children的layout方法来设置children的位置。最好不要覆写该方法,有children的viewGroup,应该覆写onLayout方法

*/

public void layout(int l, int t, int r, int b) ;

源代码:
这里不给出,如果有兴趣,自行查阅SDK。


伪代码:

public void layout(int l, int t, int r, int b) {
    if (根据一些flag,发现需要进一步measure) {
        onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
    }
 //暂存旧的位置信息
 int oldL = mLeft;
    int oldT = mTop;
    int oldB = mBottom;
    int oldR = mRight;
    //设置新的位置信息
    mLeft = l;
    mTop = t;
    mBottom = b;
    mRight = r;
 
    if (layout改变了 || 需要layout) {
        onLayout(changed, l, t, r, b);
          
  //回调layoutChange事件
        for (遍历监听对象) {
            listener.onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
        }
    }

    标记为已经执行过layout;
} 

 
2、onLayout

/** 根据布局规则,计算每一个子View的位置,View类默认是空实现。 所以这里没有源代码*/
protected void onLayout(boolean changed, int left, int top, int right, int bottom);

 

ViewGroup:

 

ViewGroup中,只需要覆写onLayout方法,来计算出每一个子View的位置,并且把layout流程传递给子View。

源代码:

ViewGroup没有实现,具体可以参考LinearLayout和RelativeLayout的onLayout方法。虽然各个具体实现都很复杂,但是基本流程是一样的,可以参考下面的伪代码。

伪代码:

protected void onLayout(boolean changed, int l, int t, int r, int b) {
    for (遍历子View) {
        /**
        根据如下数据计算。
            1、自己当前布局规则。比如垂直排放或者水平排放。
            2、子View的测量尺寸。
            3、子View在所有子View中的位置。比如位置索引,第一个或者第二个等。
        */
        计算每一个子View的位置信息; 
        
        child.layout(上面计算出来的位置信息);
    }        
}

结论

        一般来说,自定义View,如果该View不包含子View,类似于TextView这种的,是不需要覆写onLayout方法的。而含有子View的,比如LinearLayout这种,就需要根据自己的布局规则,来计算每一个子View的位置。

 

带着问题来思考整个draw过程。

1、系统为什么要有draw过程?

        View框架在经过了measure过程和layout过程之后,就已经确定了每一个View的尺寸和位置。那么接下来,也是一个重要的过程,就是draw过程,draw过程是用来绘制View的过程,它的作用就是使用graphic框架提供的各种绘制功能,绘制出当前View想要的样子。

 

2、draw过程都干了点什么事?

        View框架中,draw过程主要是绘制View的外观。ViewGroup除了负责绘制自己之外,还需要负责绘制所有的子View。而不含子View的View对象,就负责绘制自己就可以了。

        draw过程的主要流程如下:

        1、绘制 backgroud(drawBackground)      
        2、如果需要的话,保存canvas的layer,来准备fading(不是必要的步骤)
        3、绘制view的content(onDraw方法)
        4、绘制children(dispatchDraw方法)
        5、如果需要的话,绘制fading edges,然后还原layer(不是必要的步骤)
        6、绘制装饰器、比如scrollBar(onDrawForeground)

源代码分析

        在View的源代码中,提取到了下面一些关于layout过程的信息。

        我们知道,整棵View树的根节点是DecorView,它是一个FrameLayout,所以它是一个ViewGroup,所以整棵View树的测量是从一个ViewGroup对象的draw方法开始的。

 

View:

1、draw

/** 

绘制一个View以及他的子View。最好不要覆写该方法,应该覆写onDraw方法来绘制自己。

*/

public void draw(Canvas canvas);

源代码:

这里不给出,有兴趣的读者,可以自行查阅SDK。

伪代码

public void draw(Canvas canvas) {
    1、绘制 backgroud(drawBackground)  ;
    2、如果需要的话,保存canvas的layer,来准备fading ;
    3、绘制view的content(onDraw方法);
    4、绘制children(dispatchDraw方法);
    5、如果需要的话,绘制fading edges,然后还原layer ;
    6、绘制装饰器、比如scrollBar(onDrawForeground);
}

2、onDraw

/** 

绘制一个View的外观。View的默认实现是空实现,所以这里没有源码给出。

*/

protected void onDraw(Canvas canvas);

 

ViewGroup:

1、dispatchDraw

/** 绘制子View,View类是空实现,ViewGroup类中有实现 */

protected void dispatchDraw(Canvas canvas);

源代码:

这里不再给出,有兴趣的读者自行查阅SDK。

伪代码:

protected void dispatchDraw(Canvas canvas) {
    if (需要绘制布局动画) {
    for (遍历子View) {
        绑定布局动画;
    }
    启动动画控制,通知动画开始;
    }

    for (遍历子View) {
    child.draw();
    }
}

https://blog.csdn.net/sinat_27154507/article/details/79748010

http://www.cnblogs.com/xyhuangjinfu/p/5435201.html