Android中View的绘制流程

知识准备:

1.每个activity都会有一个Window(唯一实现类PhoneWindow)对象,Window对象包含ViewRoot(ViewRootImpl.java),ViewRoot包含DecorView。

DecorView继承自FrameLayout(即ViewGroup),ViewRootImpl不是具体View,它相当于一个View的工具类或者叫管理类,比如view.requestLayout()
重绘布局,实际上调用到了ViewRootImpl里面相关方法,通知整个布局重绘。
2.整个画面的绘制流程是从ViewRootImpl的performTraversals()方法开始的。所以先看ViewRootImpl类。
3.根布局一般指ViewRootImpl,顶级布局一般指DecorView。(该说法待确认)
4.DecorView=content + 状态栏 + 导航栏;参考:http://www.jianshu.com/p/a65861e946cb

   个人理解:这里仅仅是代表DecorView的高度=content高度+状态栏高度+导航栏高度,状态栏、导航栏并不是由DecorView负责绘制的,因为他们并不在同一个绘制窗口的同一层中。

进入正题,下面通过伪代码的方式分析view绘制流程

一、ViewRootImpl

整个画面的绘制流程是从ViewRootImpl的performTraversals()方法开始的

public class ViewRootImpl {
    private void performTraversals() {
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
        performLayout(lp, desiredWindowWidth, desiredWindowHeight);
        performDraw();
        //这里会调用DecorView的measure、layout、draw方法,DecorView继承自FrameLayout即ViewGroup,这样整个画面的View绘制流程就串联起来了。
        //根据这三行代码可知,Measure、Layout、Draw这三大过程是针对整个view树结构进行的,并不是一个view测量完之后就进行这个view的layout及draw过程。
        //而是整个view树结构的measure完成后,再进行整个View树结构的layout,最后进行整个view树的draw。所以,不要经常无意义的重绘画面,资源耗费很大。
        // 虽然进行重绘的过程会根据第一次绘制的参数有所优化,但是也是有很复杂的计算过程。

        //上面三个方法中的宽高的参数,是系统根据屏幕尺寸,当前界面theme计算出来的。
    }
}

二、ViewGroup

public class ViewGroup {
    // 一、measure过程:
    // 默认没有提供:
    // 1.public final void measure(int widthMeasureSpec, int heightMeasureSpec)方法,因为是final的,所以是在view中实现的,请到View中寻找相关默认代码。
    // 2.onMeasure(int widthMeasureSpec, int heightMeasureSpec),这个需要各个不同ViewGroup控件自己实现,请到View中寻找相关默认代码
    //比如LinearLayout的onMeasure()方法,就会调用到measureChildWithMargins(),然后getChildMeasureSpec()方法,返回的宽高值,调用child.measure(width,weight)
    //这样,getChildMeasureSpec()方法里的规则就影响到了子View的宽高,以及wrapContent问题。

    // ViewGroup默认提供了如下几个方法,作为工具方法在自定义ViewGroup时候使用:
    // 遍历每个孩子,调用measureChild方法对其进行测量
    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];

            //测量子元素时,对可见性为GONE的View要做特殊处理,一般来说就是跳过对它们的测量,来优化布局效率。
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }

    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);
    }
    //在自己自定义控件时,上面的这两个方法几乎不会用到。因为measureChildren太过简单粗暴,
    //只考虑了父控件的padding,但是没考虑到child view的margin,这就会导致child view在使用match_parent属性的时候,margin属性会有问题
    //而且一般都要考虑孩子们之间的逻辑关系(排列顺序、间隔等),再计算他们的测量规格。
    // 不过这个方法也给我们一点启示,就是:
    //测量子元素时,对可见性为GONE的View要做特殊处理,一般来说就是跳过对它们的测量,来优化布局。

    //一般都用下面的 几个方法来测量子view,比如LinearLayout就用到了下面的方法,来测量子view相关参数

    protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + widthUsed, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin+ heightUsed, lp.height);
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
    //measureChildWithMargins方法,比measureChild方法多考虑了个margin。所以,这个方法使用的更多一些。


    /**
     * getChildMeasureSpec()方法的作用是将父控件的测量规格和child view的布局参数LayoutParams相结合,得到一个最可能符合条件的child view的测量规格。

     *而且请注意,这各方法里的运算规则,最终影响了自定义view时候,view要在其onMeasure中单独处理view的wrap_conent属性,不处理的话就和match_parent

     *呈现出一样的效果了

     *
     * @param spec           父控件的测量规格
     * @param padding        父控件里已经占用的大小
     * @param childDimension child view布局LayoutParams里的尺寸
     * @return child view 的测量规格
     */
    //注意,这个方法是static的
    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) {
            // 当父控件的测量模式 是 精确模式,也就是有精确的尺寸了
            case MeasureSpec.EXACTLY:
                //如果child的布局参数有固定值,比如"layout_width" = "100dp"
                //那么显然child的测量规格也可以确定下来了,测量大小就是100dp,测量模式也是EXACTLY
                if (childDimension >= 0) {
                    resultSize = childDimension;
                    resultMode = MeasureSpec.EXACTLY;
                }
                //如果child的布局参数是"match_parent",也就是想要占满父控件
                //而此时父控件是精确模式,也就是能确定自己的尺寸了,那child也能确定自己大小了,就是父控件的大小,测量模式也是EXACTLY
                else if (childDimension == LayoutParams.MATCH_PARENT) {
                    resultSize = size;
                    resultMode = MeasureSpec.EXACTLY;
                }
                //如果child的布局参数是"wrap_content",也就是想要根据自己的逻辑决定自己大小,
                //比如TextView根据设置的字符串大小来决定自己的大小
                //那就自己决定呗,不过你的大小肯定不能大于父控件的大小嘛
                //所以测量模式就是AT_MOST,测量大小就是父控件的size,注意这里wrap_content返回的测量模式是AT_MOST,

                //并且允许使用的最大尺寸是父控件的大小parentSize,这样在View的onMeasure中,默认的getDefaultSize()方法中,没有对这个AT_MOST情况作出处理,

                //而是直接返回了父控件的大小,就相当于match_parent效果了,这里先注意一下,等分析到View的onMeasure过程时,会再次分析到。

                else if (childDimension == LayoutParams.WRAP_CONTENT) {
                    resultSize = size;
                    resultMode = MeasureSpec.AT_MOST;
                }
                break;

            // 当父控件的测量模式 是 最大模式,也就是说父控件自己还不知道自己的尺寸,但是大小不能超过size
            case MeasureSpec.AT_MOST:
                //同样的,既然child能确定自己大小,尽管父控件自己还不知道自己大小,也优先满足孩子的需求
                if (childDimension >= 0) {
                    resultSize = childDimension;
                    resultMode = MeasureSpec.EXACTLY;
                }
                //child想要和父控件一样大,但父控件自己也不确定自己大小,所以child也无法确定自己大小
                //但同样的,child的尺寸上限也是父控件的尺寸上限size
                else if (childDimension == LayoutParams.MATCH_PARENT) {
                    resultSize = size;
                    resultMode = MeasureSpec.AT_MOST;
                }
                //child想要根据自己逻辑决定大小,那就自己决定呗

                //还是要注意这里child的AT_MOST模式,及wrap_content
                else if (childDimension == LayoutParams.WRAP_CONTENT) {
                    resultSize = size;
                    resultMode = MeasureSpec.AT_MOST;
                }
                break;

            // Parent asked to see how big we want to be
            case MeasureSpec.UNSPECIFIED:

                 //这个case一般用不到,是系统重绘的时候用到的
                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);
    }


    //二、layout过程
    //通过super.layout(l, t, r, b);能调用到View类里的setFrame()和onLayout()方法
    @Override
    public final void layout(int l, int t, int r, int b) {
        if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
            if (mTransition != null) {
                mTransition.layoutChange(this);
            }
            super.layout(l, t, r, b);
        } else {
            // record the fact that we noop'd it; request layout when transition finishes
            mLayoutCalledWhileSuppressed = true;
        }
    }

    //ViewGroup方法里onLayout声明成了抽象方法,所以继承ViewGroup的类都得自己去实现自己定位子元素的逻辑。
    //比如LinearLayout中onLayout最终会调用child.layout(left, top, left + width, top + height);方法,去布局子元素
    @Override
    protected abstract void onLayout(boolean changed, int l, int t, int r, int b);

    //三、draw过程
    //没有提供实现:draw(Canvas canvas)、drawBackground()、onDraw(canvas)、onDrawScrollBars,都复用View的默认实现
    //提供实现了dispatchDraw(canvas),此方法里遍历调用child.draw(canvas, this, drawingTime);绘制孩子
    @Override
    protected void dispatchDraw(Canvas canvas) {
        for (int i = 0; i < childrenCount; i++) {
            child.draw(canvas, this, drawingTime);
        }
    }
}

三、View

public class View {
    //一、measure过程:
    //注意这个方法是final的,不能被重写
    // 父布局ViewGroup调用过ViewGroup.getChildMeasureSpec(),得到的宽高值被传到View.measure()方法里,ViewGroup.getChildMeasureSpec()根据规则,
    // 产生MeasureSpec.AT_MOST、MeasureSpec.EXACTLY等模式,传给了子View,从而传给了onMeasure()方法。
    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    //这个方法中的setMeasuredDimension(width,height)最终决定并告诉系统测量值的大小
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
    //前方高能预警:
    // View的getDefaultSize方法,在当测量模式为UNSPECIFIED时,返回的就是上面getSuggestedMinimumWidth/Height()方法里的大小,对我们正常逻辑没什么影响。
    // 重点是在AT_MOST和EXACTLY两种情况下。getDefaultSize十分简单粗暴,直接返回了specSize,
    // 也就是View父布局提供的测量规格里的测量尺寸,然后这就被setMeasuredDimension(width,height)。
    // 1.如果当前view是精确模式,MeasureSpec.EXACTLY,也就是xml中指定了尺寸,则直接返回了specSize,
    //   这没问题,父布局在检测到该view是精确尺寸的时候,就直接告诉view,你的尺寸就按照被指定的精确尺寸大小设置,specSize就是这个精确尺寸值。
    // 2.如果当前view是MatchParent模式,那么父布局给出的尺寸就是当前view可用的最大尺寸,并且MeasureSpec.AT_MOST,当前view直接使用这个尺寸就行。
    // 3.如果当前view是WrapContent模式,那么父布局给出的尺寸就是当前view可用的最大尺寸,并且MeasureSpec.AT_MOST,当前如果直接用这个尺寸,
    //   就会造成wrap_content时与match_parent的效果一样。所以要在onMesure方法中额外处理WrapContent情况,计算出具体期望大小,
    //   然后再调用setMeasuredDimension(width,height)
    public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = android.view.View.MeasureSpec.getMode(measureSpec);
        int specSize = android.view.View.MeasureSpec.getSize(measureSpec);
        switch (specMode) {
            case android.view.View.MeasureSpec.UNSPECIFIED:
                result = size;
                break;
            case android.view.View.MeasureSpec.AT_MOST://这里应该分match_parent和wrap_content两种情况决定返回值,

                                                                                            //但是系统简单粗暴的没处理,直接返回父布局给出建议的最大尺寸
            case android.view.View.MeasureSpec.EXACTLY:
                result = specSize;
                break;
        }
        return result;
    }

    //view的背景尺寸和测量指定的最小尺寸,之一
    protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }

    //二、layout过程
    //layout方法首先会调用setFrame方法来给View的四个顶点属性赋值,即mLeft,mRight,mTop,mBottom四个值,
    // 此时这个View的位置就确定了。同时我们也就能通过调用getWidth()和getHeight()方法来获取View的实际宽高了。
    // 然后,onLayout方法才会被调用,
    // 在View类里的onLayout方法是个空方法,而在ViewGroup方法里声明成了抽象方法,所以继承ViewGroup的类都得自己去实现自己定位子元素的逻辑。
    // 最后,在layout方法的最后有一个OnLayoutChangeListener的集合,这是View位置发生改变时的回调接口。所以可以通过addOnLayoutChangeListener
    // 方法可以监听一个View的位置变化,并做出想要的响应。
    public void layout(int l, int t, int r, int b) {
        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;
        boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnLayoutChangeListeners != null) {
                ArrayList listenersCopy = (ArrayList) li.mOnLayoutChangeListeners.clone();
                int numListeners = listenersCopy.size();
                for (int i = 0; i < numListeners; ++i) {
                    listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
                }
            }
        }
    }

    //默认是空方法,因为view不包含子view,不需要处理
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    }


    //三、draw过程
    public void draw(Canvas canvas) {
        /*
         * Draw traversal performs several drawing steps which must be executed
         * in the appropriate order:
         *
         *      1. Draw the background
         *      2. If necessary, save the canvas' layers to prepare for fading
         *      3. Draw view's content
         *      4. Draw children
         *      5. If necessary, draw the fading edges and restore layers
         *      6. Draw decorations (scrollbars for instance)
         */


        // Step 1, draw the background, if needed
        if (!dirtyOpaque) {
            drawBackground(canvas);//绘制背景
        }
        // Step 2, save the canvas' layers
        //省略一些代码,Step 2不是必须的
        // Step 3, draw the content
        if (!dirtyOpaque) onDraw(canvas);
        // Step 4, draw the children
        dispatchDraw(canvas);//绘制子view
        // Step 5, draw the fade effect and restore layers
        //省略一些代码,Step 5不是必须的
        // Step 6, draw decorations (scrollbars)
        onDrawScrollBars(canvas);
    }

    //View的drawBackground()有默认实现
    private void drawBackground(Canvas canvas) {
        //省略一堆代码,绘制背景
    }
    //View的onDraw方法是空方法,绘制自己。一般继承View的,都需要实现onDraw()方法
    protected void onDraw(Canvas canvas) {
    }
    //View的dispatchDraw方法是空方法,绘制孩子。一般继承ViewGroup的,都需要实现dispatchDraw()方法
    protected void dispatchDraw(Canvas canvas) {
    }
    //绘制滚动条等装饰
    protected final void onDrawScrollBars(Canvas canvas) {
        //省略一堆代码,绘制滚动条等装饰
    }

     //如果一个view不需要绘制任何内容,那么就请设置这个标志位为true,这样系统会进行相应的优化。View默认未启用,ViewGroup默认启用了。

      //当明确知道ViewGroup要来绘制内容时,要关闭WILL_NOT_DRAW这个标志位。

     public void setWillNotDraw(boolean willNotDraw) {

         setFlags(willNotDraw ? WILL_NOT_DRAW : 0,DRAW_MASK);

     }
}

以上就是view绘制流程的分析,关于view的重绘有两个方法:invalidate()和requestLayout()。invalidate()没有走measure、layout过程,只走了draw过程;而requestLayout()方法这是整个绘制流程重新走了一遍。关于view重绘流程,具体请参考http://blog.csdn.net/guolin_blog/article/details/17045157。

你可能感兴趣的:(技术分享,Android,代码分析,控件)