28 July 2012

经过对View树的measure和layout过程后,接下来将结合前两步得到的结果对View树进行绘制,之前以为measure过程是measure、layout和draw三部曲中最复杂的一步,在仔细分析draw过程后才发现自己之前的论断有失准确性。不过从整体来看,draw过程的逻辑是比较清晰的,和measure和layout过程十分相似,而本文将从整体来介绍draw的整个流程,至于其中的细节可能会在另外一篇文章中介绍。

和measure和layout一样,draw过程也是在ViewRoot的performTraversals()的内部发起的,其调用顺序在measure()layout()之后,同样的,performTraversals()发起的draw过程最终会调用到mViewdraw()函数,这里的mView对于Actiity来说就是PhoneWindow.DecorView

首先来看下与draw过程相关的函数,之所以先列出相关函数,目的是让大家对这些函数有个印象,以免在介绍具体细节的时候茫然:

  • ViewRootImpl.draw(),仅在ViewRootImpl.performTraversals()的内部调用
  • DecorView.draw(), 上一步中的ViewRootImpl.draw()会调用到该函数,DecorView.draw()继承自Framelayout,DecorView和FrameLayout,以及FrameLayout的父类ViewGroup都未重载draw(),而ViewGroup的父类是View类,因此DecorView.draw()调用的是View.draw()的默认实现
  • View.onDraw(),绘制View本身,自定义View往往会重载该函数来绘制View本身的内容
  • View.dispatchDraw(), View中的dispatchDraw默认是空实现,ViewGroup重载了该函数,内部会循环调用View.drawChild()来发起对子视图的绘制,应用程序不应该重载ViewGroup,因为该函数的默认实现代表了View的绘制流程
  • ViewGroup.drawChild(),该函数只在ViewGroup中实现,原因就是只有ViewGroup才需要绘制child,drawChild内部又会调用View.draw()函数来完成子视图的绘制(有可能直接调用dispatchDraw

下面笔者将从源码来展现draw的整体过程,当然源码中只展现关键部分,细节部分将在下一篇文章中具体介绍,首先来看performTraversals(),因为draw过程最先是从这里发起的:

private void performTraversals() {
    final View host = mView;
    ...
    host.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    ...
    host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
    ...
    draw(fullRedrawNeeded);
}

注意到measure和layout过程直接调用的是mView的measure和layout函数,而draw调用的是ViewRootImpl的内部draw(boolean fullRedrawNeeded)函数,再由draw(boolean fullRedrawNeeded)函数来调用mView.draw()函数,draw(boolean fullRedrawNeeded)包含draw过程的一些前期处理,通过下面的代码可以看出调用关系:

private void draw(boolean fullRedrawNeeded) {
        Surface surface = mSurface;
        if (surface == null || !surface.isValid()) {
            return;
        }
        ...
        try {
            canvas.translate(0, -yoff);
            if (mTranslator != null) {
                mTranslator.translateCanvas(canvas);
            }
            canvas.setScreenDensity(scalingRequired ? DisplayMetrics.DENSITY_DEVICE : 0);
            mAttachInfo.mSetIgnoreDirtyState = false;
            mView.draw(canvas);
       } finally {
            if (!mAttachInfo.mSetIgnoreDirtyState) {
            // Only clear the flag if it was not set during the mView.draw() call
            mAttachInfo.mIgnoreDirtyState = false;
            }
       }
       ...
}

下面将转到mView.draw(),之前提到mView.draw()调用的就是View.java的默认实现,View类中的draw函数体现了View绘制的核心流程,因此我们下面重点来看下View.java中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
        ...
        background.draw(canvas);
        ...
        // skip step 2 & 5 if possible (common case)
        ...
        // Step 2, save the canvas' layers
        ...
        if (solidColor == 0) {
            final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG;

            if (drawTop) {
                canvas.saveLayer(left, top, right, top + length, null, flags);
            }
        ...
        // Step 3, draw the content
        if (!dirtyOpaque) onDraw(canvas);

        // Step 4, draw the children
        dispatchDraw(canvas);

        // Step 5, draw the fade effect and restore layers

        if (drawTop) {
            matrix.setScale(1, fadeHeight * topFadeStrength);
            matrix.postTranslate(left, top);
            fade.setLocalMatrix(matrix);
            canvas.drawRect(left, top, right, top + length, p);
        }
        ...
        // Step 6, draw decorations (scrollbars)
        onDrawScrollBars(canvas);
    }

官方源码中对View的绘制流程给了非常详细的注释,笔者将整个绘制过程的关键步骤抽取出来方便了解整个过程,通过阅读上面的代码可以知道整个绘制过程包括View的背景绘制,View本身内容的绘制,子视图的绘制(如果包含子视图),渐变框的绘制以及滚动条的绘制。 我们重点要关注的是View本身内容的绘制和子视图的绘制,即onDraw()dispatchDraw()函数。 对于View.java和ViewGroup.java,onDraw()默认都是空实现,因为具体View本身长什么样子是由View的设计者来决定的,默认不显示任何东西。 View.java中dispatchDraw()默认为空实现,因为其不包含子视图,而ViewGroup重载了dispatchDraw()来对其子视图进行绘制,通常应用程序不应该对dispatchDraw()进行重载,其默认实现体现了View系统绘制的流程。那么,接下来我们继续分析下ViewGroup中dispatchDraw()的具体流程:

@Override
    protected void dispatchDraw(Canvas canvas) {
       ...

        if ((flags & FLAG_USE_CHILD_DRAWING_ORDER) == 0) {
            for (int i = 0; i < count; i++) {
                final View child = children[i];
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
                    more |= drawChild(canvas, child, drawingTime);
                }
            }
        } else {
            for (int i = 0; i < count; i++) {
                final View child = children[getChildDrawingOrder(count, i)];
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
                    more |= drawChild(canvas, child, drawingTime);
                }
            }
        }
        ...
    }

dispatchDraw()的核心代码就是通过for循环调用drawChild()对ViewGroup的每个子视图进行绘制,上述代码中如果FLAG_USE_CHILD_DRAWING_ORDERtrue,则子视图的绘制顺序通过getChildDrawingOrder来决定,默认的绘制顺序即是子视图加入ViewGroup的顺序,而我们可以重载getChildDrawingOrder函数来更改默认的绘制顺序,这会影响到子视图之间的重叠关系。 drawChild()的核心过程就是为子视图分配合适的cavas剪切区,剪切区的大小正是由layout过程决定的,而剪切区的位置取决于滚动值以及子视图当前的动画。设置完剪切区后就会调用子视图的draw()函数进行具体的绘制,如果子视图的包含SKIP_DRAW标识,那么仅调用dispatchDraw(),即跳过子视图本身的绘制,但要绘制视图可能包含的字视图。 完成了dispatchDraw()过程后,View系统会调用onDrawScrollBars()来绘制滚动条,滚动条绘制的细节这里就不展开介绍了。

总结:以上文字着重于对draw整体流程的介绍,通过对关键函数以及它们之间的调用关系来展示draw的内部过程,从整体上把握整个过程将更有利于后面对细节的理解和分析。



blog comments powered by Disqus