View详解之一View的绘制原理

View 是开发中最常用的类之一,我们来看看View的真面目吧。

首先,先了解几个概念

1.Window

window是个抽象类,用于承载View和Viewgroup的类,我们在activity中添加View的过程,其实承载这些View的都是Window,只要有View的地方就有Window,比如说Toast,Dialog等。

2.Window窗口的布局层次结构

在activity中View的从层次结构如下图。

View详解之一View的绘制原理_第1张图片.png)

在最外层黑的是activity,红的是window,绿的是顶级View(DecorView),接着是标题栏,ContentView

DecorView是个FrameLayout一般内部有个LinearLayout,LinearLayout是标题栏,ContentView也是FrameLayout,它具有id=content,可以通过R.android.id.content找到ContentView,找到之后我们可以得到我们为activity设置的布局View,contentView.getchildAt(0)。

从上图可知,View的绘制是从DecorView开始绘制的。而它的绘制是有ViewRoot类的performTraversals()方法开始的。如下图

View详解之一View的绘制原理_第2张图片

从上图看出,绘制过程是从ViewGroup开始,每个View都要经历三个过程,依次执行ViewGroup的Measure遍历子View执行Measure,然后ViewGroup的layout遍历子View执行Layout,draw同上步骤。

3.绘制过程的一个很重要的类MeasureSpec

MeasureSpec,理解为测量规格。

MeasureSpec 值是一个32位int数,高2位代表了SpecMode指测量模式,低30位代表了SpecSize指在改成测量模式下的大小。

子View的该值由父容器在Measure过程中 计算得到,然后调用子View的Measure将MeasureSpec传入,进行子View的Measure过程。

那是怎么通过Measure,获得模式和大小呢?其实,还有一个Mask值为0x3<< 30用来求解Mode和Size,通过MeasureSpec的值与Mask按位与,即可 得到相应的值。

SpecMode有三类:

UNSPECIFIED:父容器不对子View有任何限制,要多大给多大,一般用于系统内部,常常我们给View指定一个默认大小。

EXACTLY:父容器已经得到子View的精确大小,对应于子View的LayoutParams是matchParent和具体数值的情况。

AT_MOST:父容器指定一个可用大小即SpecSize,子View不能大于这个值,View对应的Layoutparams是wrap_content

MeasureSpec和LayoutParams 的关系:

DecorView的MeasureSpec由窗口的尺寸和自身的LayoutParams来共同确定。对于布局中的普通View,MeasureSpec由父容器的MeasureSpec和自身的Layoutparams(包括margin和padding等参数)共同决定。MeasureSpec一旦确定后就可以对子View进行Measure确定View的测量宽高。看一段源码:

/**
     * Ask one of the children of this view to measure itself, taking into
     * account both the MeasureSpec requirements for this view and its padding.
     * The heavy lifting is done in getChildMeasureSpec.
     *
     * @param child The child to measure
     * @param parentWidthMeasureSpec The width requirements for this view
     * @param parentHeightMeasureSpec The height requirements for this view
     */
    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);
    }

measureChild计算出child的MeasureSpec然后,调用其Measure()进行测量。看看getChildMeasureSepc()的源码。

 /**
     * Does the hard part of measureChildren: figuring out the MeasureSpec to
     * pass to a particular child. This method figures out the right MeasureSpec
     * for one dimension (height or width) of one child view.
     *
     * The goal is to combine information from our MeasureSpec with the
     * LayoutParams of the child to get the best possible results. For example,
     * if the this view knows its size (because its MeasureSpec has a mode of
     * EXACTLY), and the child has indicated in its LayoutParams that it wants
     * to be the same size as the parent, the parent should ask the child to
     * layout given an exact size.
     *
     * @param spec The requirements for this view
     * @param padding The padding of this view for the current dimension and
     *        margins, if applicable
     * @param childDimension How big the child wants to be in the current
     *        dimension
     * @return a MeasureSpec integer for the child
     */
    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 = 0;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = 0;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

这段代码的注释写的还是比较清楚的,根据父容器的MeasureSpec中的mode和siez值结合子View自身的Layoutparams得到MeasureSpec,调用makeMeasureSpec生成子View的MeasureSpec的值。

view的MeasureSpec创建规则,childlayoutparams和parentSpecmode决定如下表所示

View详解之一View的绘制原理_第3张图片

看出wrap_content时需要进行处理否则和match-parent大小一样,
可以指定一个默认的大小,或者根据View的内容确定大小。

View和Viewgroup的三大绘制流程

Measure用于测量子view的宽高,layout用于确定view的顶点的位置和最终的宽高,draw用于在频幕上绘制view

Measure过程

这里要区分View和Viewgroup的Measure
viewgroup除了测量自身外还要遍历其child,对child进行测量。

View的Measure

View的Measure是final方法,会调用onMeasure(),我们经常需要重写onMeasure()实现自定义View。看看onMeasure()源码。

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

This method must be called by {@link #onMeasure(int, int)} to store the * measured width and measured height. Failing to do so will trigger an * exception at measurement time.

* protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) { boolean optical = isLayoutModeOptical(this); if (optical != isLayoutModeOptical(mParent)) { Insets insets = getOpticalInsets(); int opticalWidth = insets.left + insets.right; int opticalHeight = insets.top + insets.bottom; measuredWidth += optical ? opticalWidth : -opticalWidth; measuredHeight += optical ? opticalHeight : -opticalHeight; } setMeasuredDimensionRaw(measuredWidth, measuredHeight); }

onMeasure调用setMeasuredDimension()设置View的宽高测量值,从注释可以看出该方法必须在onmeasure中调用以保存测量的宽高,getdefaultSize()处理了mode是UNSPECIFED情况,给定一个默认大小,其他情况使用MeasureSpec的值。

ps:这里需要注意在自定义View的时候需要在onMeasure中处理wrap_content的情况,否则wrap_content即mode是At_most的大小和match_parent大小一样。

ViewGroup的Measure过程

ViewGroup是个抽象类,除了Measure自身还需要measureChildren(),但没有实现onMeasure方法,像LinearLayout和RelativeLayout的onMeasure方法实现是有差别的,所以由子类具体实现onMeasure。

看一下LinearLayout的onMeasure()实现:

// See how tall everyone is. Also remember max width.
        for (int i = 0; i < count; ++i) {
            final View child = getVirtualChildAt(i);

            if (child == null) {
                mTotalLength += measureNullChild(i);
                continue;
            }

            if (child.getVisibility() == View.GONE) {
               i += getChildrenSkipCount(child, i);
               continue;
            }

            if (hasDividerBeforeChildAt(i)) {
                mTotalLength += mDividerHeight;
            }

代码还是有点多的,就不贴了,主要是计算了垂直方向遍历子View的高度得到总的高度然后调用setMeasuredDimension()设置自己的大小。

Measure过程完了之后,就可以在onMeasure中获得view的宽高了,但这可能是不准确的,有时候Measure需要多次执行,最好在onlayout中获取最终的宽高。

如果想在activity初始化时候如oncreate,onresume中获得view的宽高是不行的,因为view的Measure和activity的生命周期不是同步执行的。得到的宽高可能是0.

可以在以下几处地方获取,view的宽高。

1.onwindowFoucusChanged

该方法表示view已经初始化完毕了,注意该方法会被多次调用,,activity的窗口失去焦点和获得焦点都会调用此方法。比如onpause和onresume

2.view.post(Runnable)

在activity的onstart中通过post将一个Runnable投递到消队列的尾部,在run中获取测量宽高,

3.ViewTreeObserver

该类的多个方法可以实现获取宽高功能,比如OnGlobalLayoutListener,,View树的状态或者View的可见性发生改变时都会调用onGloaballayout()回调,可以在此获得宽高。

4.手动调用Measure()方法

根据view的Layoutparams判断是否能获取成功,

match_parent不行此时需要知道父view的Measurespec,

具体数值,

MeasureSpec.makeMeasureSpec(100,MesureSpec);
View.measure()

wrap_content

此时指定mode为at_most,

layout过程

layout在Viewgroup中的作用是Viewgroup确定自身的位置和子元素的位置,Viewgroup位置确定后会调用onlayout确定子View的位置,调用子View的layout继续确定子子view的位置,不断遍历。

layout调用setFrame确定view的四个顶点位置,

onlayout有子类具体实现,LinearLayout中会调用setChildFrame()确定子view的顶点,

最后说明一下measuredwidth和width的区别,默认情况下,是相同的,width是在layout之后通过右顶点减左顶点得到的,特殊情况在onlayout()中传入的顶点不是测量得到的宽高是,就会出现不同。

draw过程

draw过程比较简单,分为六部,主要介绍一下四部

1.绘制背景,background.draw

2.绘制自己ondraw

3.绘制chilren,dispatchdraw

4.绘制装饰,onDrawScrollBars;

Viewgroup有一个方法setWillNotDraw(),当继承自viewgroup时,如果不需要绘制自己可以设置改值为false,系统会进行相应优化,viewStub就是使用这个方法,实现不绘制。

view的状态改变和重绘

view的状态改变会导致重绘,也可以动态设置view的显隐导致重绘.

调用performTranversal,走绘制流程,但是另外需要注意的是,invalidate()方法虽然最终会调用到performTraversals()方法中,但这时measure和layout流程是不会重新执行的,因为视图没有强制重新测量的标志位,而且大小也没有发生过变化,所以这时只有draw流程可以得到执行。而如果你希望视图的绘制流程可以完完整整地重新走一遍,就不能使用invalidate()方法,而应该调用requestLayout()了。

这个方法中的流程比invalidate()方法要简单一些,但中心思想是差不多的,这里也就不再详细进行分析了。

你可能感兴趣的:(Android)