Android View 绘制流程(Draw) 完全解析

前言

前几篇文章,笔者分别讲述了DecorView,measure,layout流程等,接下来将详细分析三大工作流程的最后一个流程——绘制流程。测量流程决定了View的大小,布局流程决定了View的位置,那么绘制流程将决定View的样子,一个View该显示什么由绘制流程完成。以下源码均取自Android API 21。

从performDraw说起

前面几篇文章提到,三大工作流程始于ViewRootImpl#performTraversals,在这个方法内部会分别调用performMeasure,performLayout,performDraw三个方法来分别完成测量,布局,绘制流程。那么我们现在先从performDraw方法看起,ViewRootImpl#performDraw:

private void performDraw() {
    //...
    final boolean fullRedrawNeeded = mFullRedrawNeeded;
    try {
        draw(fullRedrawNeeded);
    } finally {
        mIsDrawing = false;
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }

    //省略...
}

里面又调用了ViewRootImpl#draw方法,并传递了fullRedrawNeeded参数,而该参数由mFullRedrawNeeded成员变量获取,它的作用是判断是否需要重新绘制全部视图,如果是第一次绘制视图,那么显然应该绘制所以的视图,如果由于某些原因,导致了视图重绘,那么就没有必要绘制所有视图。我们来看看ViewRootImpl#draw

private void draw(boolean fullRedrawNeeded) {
    ...
    //获取mDirty,该值表示需要重绘的区域
    final Rect dirty = mDirty;
    if (mSurfaceHolder != null) {
        // The app owns the surface, we won‘t draw.
        dirty.setEmpty();
        if (animating) {
            if (mScroller != null) {
                mScroller.abortAnimation();
            }
            disposeResizeBuffer();
        }
        return;
    }

    //如果fullRedrawNeeded为真,则把dirty区域置为整个屏幕,表示整个视图都需要绘制
    //第一次绘制流程,需要绘制所有视图
    if (fullRedrawNeeded) {
        mAttachInfo.mIgnoreDirtyState = true;
        dirty.set(0, 0, (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
    }

    //省略...

    if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
                return;
        }
}

这里省略了一部分代码,我们只看关键代码,首先是先获取了mDirty值,该值保存了需要重绘的区域的信息,关于视图重绘,后面会有文章专门叙述,这里先熟悉一下。接着根据fullRedrawNeeded来判断是否需要重置dirty区域,最后调用了ViewRootImpl#drawSoftware方法,并把相关参数传递进去,包括dirty区域,我们接着看该方法的源码:

private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
            boolean scalingRequired, Rect dirty) {

    // Draw with software renderer.
    final Canvas canvas;
    try {
        final int left = dirty.left;
        final int top = dirty.top;
        final int right = dirty.right;
        final int bottom = dirty.bottom;

        //锁定canvas区域,由dirty区域决定
        canvas = mSurface.lockCanvas(dirty);

        // The dirty rectangle can be modified by Surface.lockCanvas()
        //noinspection ConstantConditions
        if (left != dirty.left || top != dirty.top || right != dirty.right
                || bottom != dirty.bottom) {
            attachInfo.mIgnoreDirtyState = true;
        }

        canvas.setDensity(mDensity);
    } 

    try {

        if (!canvas.isOpaque() || yoff != 0 || xoff != 0) {
            canvas.drawColor(0, PorterDuff.Mode.CLEAR);
        }

        dirty.setEmpty();
        mIsAnimating = false;
        attachInfo.mDrawingTime = SystemClock.uptimeMillis();
        mView.mPrivateFlags |= View.PFLAG_DRAWN;

        try {
            canvas.translate(-xoff, -yoff);
            if (mTranslator != null) {
                mTranslator.translateCanvas(canvas);
            }
            canvas.setScreenDensity(scalingRequired ? mNoncompatDensity : 0);
            attachInfo.mSetIgnoreDirtyState = false;

            //正式开始绘制
            mView.draw(canvas);

        }
    }
    return true;
}

可以看书,首先是实例化了Canvas对象,然后锁定该canvas的区域,由dirty区域决定,接着对canvas进行一系列的属性赋值,最后调用了mView.draw(canvas)方法,前面分析过,mView就是DecorView,也就是说从DecorView开始绘制,前面所做的一切工作都是准备工作,而现在则是正式开始绘制流程。

View的绘制

由于ViewGroup没有重写draw方法,因此所有的View都是调用View#draw方法,因此,我们直接看它的源码:

public void draw(Canvas canvas) {
    final int privateFlags = mPrivateFlags;
    final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
            (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
    mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;

    /*
     * 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
    int saveCount;

    if (!dirtyOpaque) {
        drawBackground(canvas);
    }

    // skip step 2 & 5 if possible (common case)
    final int viewFlags = mViewFlags;
    boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
    boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
    if (!verticalEdges && !horizontalEdges) {
        // Step 3, draw the content
        if (!dirtyOpaque) onDraw(canvas);

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

        // Overlay is part of the content and draws beneath Foreground
        if (mOverlay != null && !mOverlay.isEmpty()) {
            mOverlay.getOverlayView().dispatchDraw(canvas);
        }

        // Step 6, draw decorations (foreground, scrollbars)
        onDrawForeground(canvas);

        // we‘re done...
        return;
    }
    ...
}

可以看到,draw过程比较复杂,但是逻辑十分清晰,而官方注释也清楚地说明了每一步的做法。我们首先来看一开始的标记位dirtyOpaque,该标记位的作用是判断当前View是否是透明的,如果View是透明的,那么根据下面的逻辑可以看出,将不会执行一些步骤,比如绘制背景、绘制内容等。这样很容易理解,因为一个View既然是透明的,那就没必要绘制它了。接着是绘制流程的六个步骤,这里先小结这六个步骤分别是什么,然后再展开来讲。

绘制流程的六个步骤:
1、对View的背景进行绘制
2、保存当前的图层信息(可跳过)
3、绘制View的内容
4、对View的子View进行绘制(如果有子View)
5、绘制View的褪色的边缘,类似于阴影效果(可跳过)
6、绘制View的装饰(例如:滚动条)
其中第2步和第5步是可以跳过的,我们这里不做分析,我们重点来分析其它步骤。

Step 1 :绘制背景

这里调用了View#drawBackground方法,我们看它的源码:

private void drawBackground(Canvas canvas) {

    //mBackground是该View的背景参数,比如背景颜色
    final Drawable background = mBackground;
    if (background == null) {
        return;
    }

    //根据View四个布局参数来确定背景的边界
    setBackgroundBounds();

    ...

    //获取当前View的mScrollX和mScrollY值
    final int scrollX = mScrollX;
    final int scrollY = mScrollY;
    if ((scrollX | scrollY) == 0) {
        background.draw(canvas);
    } else {
        //如果scrollX和scrollY有值,则对canvas的坐标进行偏移,再绘制背景
        canvas.translate(scrollX, scrollY);
        background.draw(canvas);
        canvas.translate(-scrollX, -scrollY);
    }
}

可以看出,这里考虑到了view的偏移参数,scrollX和scrollY,绘制背景在偏移后的view中绘制。

Step 3:绘制内容

这里调用了View#onDraw方法,View中该方法是一个空实现,因为不同的View有着不同的内容,这需要我们自己去实现,即在自定义View中重写该方法来实现。

Step 4: 绘制子View

如果当前的View是一个ViewGroup类型,那么就需要绘制它的子View,这里调用了dispatchDraw,而View中该方法是空实现,实际是ViewGroup重写了这个方法,那么我们来看看,ViewGroup#dispatchDraw:

protected void dispatchDraw(Canvas canvas) {
    boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);
    final int childrenCount = mChildrenCount;
    final View[] children = mChildren;
    int flags = mGroupFlags;

    for (int i = 0; i < childrenCount; i++) {
        while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
            final View transientChild = mTransientViews.get(transientIndex);
            if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
                    transientChild.getAnimation() != null) {
                more |= drawChild(canvas, transientChild, drawingTime);
            }
            transientIndex++;
            if (transientIndex >= transientCount) {
                transientIndex = -1;
            }
        }
        int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i;
        final View child = (preorderedList == null)
                ? children[childIndex] : preorderedList.get(childIndex);
        if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
            more |= drawChild(canvas, child, drawingTime);
        }
    }
    //省略...

}

源码很长,这里简单说明一下,里面主要遍历了所以子View,每个子View都调用了drawChild这个方法,我们找到这个方法,ViewGroup#drawChild

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
     return child.draw(canvas, this, drawingTime);
}

可以看出,这里调用了View的draw方法,但这个方法并不是上面所说的,因为参数不同,我们来看看这个方法,View#draw

boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {

    //省略...

    if (!drawingWithDrawingCache) {
        if (drawingWithRenderNode) {
            mPrivateFlags &= ~PFLAG_DIRTY_MASK;
            ((DisplayListCanvas) canvas).drawRenderNode(renderNode);
        } else {
            // Fast path for layouts with no backgrounds
            if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
                mPrivateFlags &= ~PFLAG_DIRTY_MASK;
                dispatchDraw(canvas);
            } else {
                draw(canvas);
            }
        }
    } else if (cache != null) {
        mPrivateFlags &= ~PFLAG_DIRTY_MASK;
        if (layerType == LAYER_TYPE_NONE) {
            // no layer paint, use temporary paint to draw bitmap
            Paint cachePaint = parent.mCachePaint;
            if (cachePaint == null) {
                cachePaint = new Paint();
                cachePaint.setDither(false);
                parent.mCachePaint = cachePaint;
            }
            cachePaint.setAlpha((int) (alpha * 255));
            canvas.drawBitmap(cache, 0.0f, 0.0f, cachePaint);
        } else {
            // use layer paint to draw the bitmap, merging the two alphas, but also restore
            int layerPaintAlpha = mLayerPaint.getAlpha();
            mLayerPaint.setAlpha((int) (alpha * layerPaintAlpha));
            canvas.drawBitmap(cache, 0.0f, 0.0f, mLayerPaint);
            mLayerPaint.setAlpha(layerPaintAlpha);
        }
    }

}

我们主要来看核心部分,首先判断是否已经有缓存,即之前是否已经绘制过一次了,如果没有,则会调用draw(canvas)方法,开始正常的绘制,即上面所说的六个步骤,否则利用缓存来显示。
这一步也可以归纳为ViewGroup绘制过程,它对子View进行了绘制,而子View又会调用自身的draw方法来绘制自身,这样不断遍历子View及子View的不断对自身的绘制,从而使得View树完成绘制。

Step 6 绘制装饰

所谓的绘制装饰,就是指View除了背景、内容、子View的其余部分,例如滚动条等,我们看View#onDrawForeground:

public void onDrawForeground(Canvas canvas) {
    onDrawScrollIndicators(canvas);
    onDrawScrollBars(canvas);

    final Drawable foreground = mForegroundInfo != null ? mForegroundInfo.mDrawable : null;
    if (foreground != null) {
        if (mForegroundInfo.mBoundsChanged) {
            mForegroundInfo.mBoundsChanged = false;
            final Rect selfBounds = mForegroundInfo.mSelfBounds;
            final Rect overlayBounds = mForegroundInfo.mOverlayBounds;

            if (mForegroundInfo.mInsidePadding) {
                selfBounds.set(0, 0, getWidth(), getHeight());
            } else {
                selfBounds.set(getPaddingLeft(), getPaddingTop(),
                        getWidth() - getPaddingRight(), getHeight() - getPaddingBottom());
            }

            final int ld = getLayoutDirection();
            Gravity.apply(mForegroundInfo.mGravity, foreground.getIntrinsicWidth(),
                    foreground.getIntrinsicHeight(), selfBounds, overlayBounds, ld);
            foreground.setBounds(overlayBounds);
        }

        foreground.draw(canvas);
    }
}

可以看出,逻辑很清晰,和一般的绘制流程非常相似,都是先设定绘制区域,然后利用canvas进行绘制,这里就不展开详细地说了,有兴趣的可以继续了解下去。

那么,到目前为止,View的绘制流程也讲述完毕了,希望这篇文章对你们起到帮助作用,谢谢你们的阅读。

更多阅读
Android View 测量流程(Measure)完全解析
Android View 布局流程(Layout)完全解析

时间: 2024-10-05 05:27:56

Android View 绘制流程(Draw) 完全解析的相关文章

Android View 测量流程(Measure)完全解析

前言 上一篇文章,笔者主要讲述了DecorView以及ViewRootImpl相关的作用,这里回顾一下上一章所说的内容:DecorView是视图的顶级View,我们添加的布局文件是它的一个子布局,而ViewRootImpl则负责渲染视图,它调用了一个performTraveals方法使得ViewTree开始三大工作流程,然后使得View展现在我们面前.本篇文章主要内容是:详细讲述View的测量(Measure)流程,主要以源码的形式呈现,源码均取自Android API 21. 从ViewRoo

Android View绘制流程

框架分析 在之前的下拉刷新中,小结过触屏消息先到WindowManagerService(Wms)然后顺次传递给ViewRoot(派生自Handler),经decor view到Activity再传递给指定的View,这次整理View的绘制流程,通过源码可知,这个过程应该没有涉及到IPC(或者我没有发现),需要绘制时在UI线程中通过ViewRoot发送一个异步请求消息,然后ViewRoot自己接收并不处理这个消息. 在正式进入View绘制之前,首先需要明确一下Android UI的架构组成,偷图

Android视图View绘制流程与源码分析(全)

来源:[工匠若水 http://blog.csdn.net/yanbober] 1 背景 还记得前面<Android应用setContentView与LayoutInflater加载解析机制源码分析>这篇文章吗?我们有分析到Activity中界面加载显示的基本流程原理,记不记得最终分析结果就是下面的关系: 看见没有,如上图中id为content的内容就是整个View树的结构,所以对每个具体View对象的操作,其实就是个递归的实现. 前面<Android触摸屏事件派发机制详解与源码分析一(

Android应用层View绘制流程与源码分析

Android应用层View绘制流程与源码分析 1 背景 还记得前面<Android应用setContentView与LayoutInflater加载解析机制源码分析>这篇文章吗?我们有分析到Activity中界面加载显示的基本流程原理,记不记得最终分析结果就是下面的关系: 看见没有,如上图中id为content的内容就是整个View树的结构,所以对每个具体View对象的操作,其实就是个递归的实现. 前面<Android触摸屏事件派发机制详解与源码分析一(View篇)>文章的3-1

Android View绘制及实践

概述 整个View树的绘图流程是在ViewRoot.java类的performTraversals()函数展开的,该函数做的执行过程可简单概况为: - 判断是否需要重新计算视图大小(measure) - 判断是否重新需要安置视图的位置(layout) - 判断是否需要重绘(draw) 其整个流程图如下: 图片来自:Android 开源项目源码解析 公共技术点中的 View 绘制流程 在Android中View的整个生命周期,调用invalidate和requestLayout会触发一系列的方法,

公共技术点之 View 绘制流程

公共技术点之 View 绘制流程   项目:,分析者:lightSky,校对者:Trinea 本文为 Android 开源项目源码解析 公共技术点中的 View 绘制流程 部分 分析者:lightSky View 绘制机制 1. View 树的绘图流程 当 Activity 接收到焦点的时候,它会被请求绘制布局,该请求由 Android framework 处理.绘制是从根节点开始,对布局树进行 measure 和 draw.整个 View 树的绘图流程在ViewRoot.java类的perfo

Android View measure流程详解

Android View measure流程详解 Android中View绘制的流程包括:measure(测量)->layout(布局)->draw(绘制). 因为Android中每个View都占据了一块矩形的空间,当我们要在屏幕上显示这个矩形的View的时候 首先我们需要知道这个矩形的大小(宽和高)这就对应了View的measure流程. 有了View的宽和高,我们还需要知道View左上角的起点在哪里,右下角的终点在哪里,这就对应了View的layout流程. 当矩形的区域在屏幕上确定之后,

Android View绘制机制

------------------------------------------------------------------------------ GitHub:lightSky    微博:    light_sky, 即时分享最新技术,欢迎关注 ------------------------------------------------------------------------------ 前言 该篇文章来自一个开源项目android-open-project-analy

简单研究Android View绘制一

2015-07-27 16:52:58 一.如何通过继承ViewGroup来实现自定义View?首先得搞清楚Android时如何绘制View的,参考Android官方文档:How Android Draws Views 以下翻译摘自:http://blog.csdn.net/linghu_java/article/details/23882681,这也是一片好文章,推荐大家看看- When an Activity receives focus, it will be requested to d