Android View原理解析之绘制流程(draw)

提示:本文的源码均取自Android 7.0(API 24)

前言

自定义View是Android进阶路线上必须攻克的难题,而在这之前就应该先对View的工作原理有一个系统的理解。本系列将分为4篇博客进行讲解,本文主要对View的绘制流程进行讲解。相关内容如下:

  • Android View原理解析之基础知识(MeasureSpec、DecorView、ViewRootImpl)
  • Android View原理解析之测量流程(measure)
  • Android View原理解析之布局流程(layout)
  • Android View原理解析之绘制流程(draw)

从View的角度看draw流程

在本系列的第一篇文章中讲到整个视图树(ViewTree)的根容器是DecorView,ViewRootImpl通过调用DecorView的draw方法开启布局流程。draw是定义在View中的方法,我们先从View的角度来看看布局过程中发生了什么。

首先来看一下draw方法中的逻辑,关键代码如下:

/**
 * Manually render this view (and all of its children) to the given Canvas.
 * The view must have already done a full layout before this function is
 * called.  
 * 
 * View的子类不应该重写这个方法,而应该重写onDraw方法绘制自己的内容
 *
 * @param canvas The Canvas to which the View is rendered.
 */
@CallSuper
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;

    /*
     * 完整地绘制流程将按顺序执行以下6步
     * 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 绘制子View
     * 5. If necessary, draw the fading edges and restore layers 恢复保存的图层
     * 6. Draw decorations (scrollbars for instance) 绘制装饰(比如滑动条)
     */

    // ① Step 1, 绘制背景(如果有必要的话)
    int saveCount;

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

    // 通常情况下会跳过第2步和第5步
    // 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, 绘制内容
        if (!dirtyOpaque) onDraw(canvas);

        // ③ Step 4, 绘制子View
        dispatchDraw(canvas);

        drawAutofilledHighlight(canvas);

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

        // ④ Step 6, 绘制View的装饰 (foreground, scrollbars)
        onDrawForeground(canvas);

        // ⑤ Step 7, 绘制默认的焦点高亮
        drawDefaultFocusHighlight(canvas);

        if (debugDraw()) {
            debugDrawFocus(canvas);
        }

        // we're done...
        return;
    }
    
    /* 以下会完整执行所有绘制步骤(一般不会执行到这里)
     * Here we do the full fledged routine...
     * (this is an uncommon case where speed matters less,
     * this is why we repeat some of the tests that have been
     * done above)
     */
    ........
}

这个方法的逻辑非常清晰,这里咱们再来总结一下在draw中要执行的步骤:

  1. 绘制背景
  2. 保存Canvas图层信息(如果有必要的话)
  3. 绘制View的内容
  4. 绘制子View
  5. 绘制保存的Canvas图层信息(如果有必要的话)
  6. 绘制View的装饰(比如滑动条)

其中第2步和第5步在通常情况下是不会执行的,所以我们也就不再深究它们了。首先在代码①的位置,调用了drawBackground方法绘制View的背景,那让我们首先来看看在这个方法中做了些什么:

private void drawBackground(Canvas canvas) {
    final Drawable background = mBackground;
    if (background == null) {
        return;
    }

    setBackgroundBounds();

    // Attempt to use a display list if requested.
    if (canvas.isHardwareAccelerated() && mAttachInfo != null
            && mAttachInfo.mThreadedRenderer != null) {
        mBackgroundRenderNode = getDrawableRenderNode(background, mBackgroundRenderNode);

        final RenderNode renderNode = mBackgroundRenderNode;
        if (renderNode != null && renderNode.isValid()) {
            setBackgroundRenderNodeProperties(renderNode);
            ((DisplayListCanvas) canvas).drawRenderNode(renderNode);
            return;
        }
    }

    final int scrollX = mScrollX;
    final int scrollY = mScrollY;
    if ((scrollX | scrollY) == 0) {
    	// 绘制View背景
        background.draw(canvas);
    } else {
    	// 如果View发生了移动,先移动画布,再绘制View背景
        canvas.translate(scrollX, scrollY);
        background.draw(canvas);
        canvas.translate(-scrollX, -scrollY);
    }
}

在这个方法中的background其实就是个Drawable对象,绘制背景的时候只要调用Drawable#draw方法就行了。当然,如果View的位置发生了移动(scrollX或scrollY不为0),需要先平移画布,再绘制background。

然后在View#draw代码②的位置,调用了onDraw方法绘制自己的内容,这个方法的代码如下:

/**
 * Implement this to do your drawing.
 * @param canvas the canvas on which the background will be drawn
 */
protected void onDraw(Canvas canvas) {
}

这是个空方法,意味着View的子类需要自己负责绘制内容。如果通过继承View实现自定义View,就应该重写onDraw方法,并在这个方法中绘制自身的内容。View#draw方法的注释也提到,View的子类不应该直接重写draw方法,而应该重写onDraw方法。

随后在View#draw代码③的位置,调用了dispatchDraw方法绘制自己的子View,这个方法同样是空实现。因为一个纯粹的View是没有子View的,自然也没必要执行相应的绘制逻辑。代码如下:

/**
 * Called by draw to draw the child views. This may be overridden
 * by derived classes to gain control just before its children are drawn
 * (but after its own view has been drawn).
 */
protected void dispatchDraw(Canvas canvas) {
}

紧接着在View#draw代码④的位置,调用了onDrawForeground方法绘制View的装饰,比如前景、滑动条等,代码如下:

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);
    }
}

在这个方法中会依次调用onDrawScrollIndicatorsonDrawScrollbars绘制滑动条的指示器和滑动条,最后绘制View的前景色。代码中的foreground是一个Drawable对象,因此只需要调用Drawable#draw方法就完成了对View前景色的绘制。

最后在View#draw代码⑤的位置,调用drawDefaultFocusHighlight方法绘制View的默认焦点高亮状态,代码如下:


private void drawDefaultFocusHighlight(Canvas canvas) {
    if (mDefaultFocusHighlight != null) {
        if (mDefaultFocusHighlightSizeChanged) {
            mDefaultFocusHighlightSizeChanged = false;
            final int l = mScrollX;
            final int r = l + mRight - mLeft;
            final int t = mScrollY;
            final int b = t + mBottom - mTop;
            mDefaultFocusHighlight.setBounds(l, t, r, b);
        }
        mDefaultFocusHighlight.draw(canvas);
    }
}

mDefaultFocusHighlight同样是一个Drawable对象,这里调用Drawable#draw方法完成了对默认焦点高亮状态的绘制。

从ViewGroup的角度看draw流程

说完了View的绘制流程,接下来再从ViewGroup角度看看绘制过程中发生了什么。

ViewGroup并没有重写draw方法,说明ViewGroup也是遵循上文提到的绘制步骤的。此外,ViewGroup也没有重写onDraw方法,说明ViewGroup默认也不会绘制自身的内容。如果我们通过继承ViewGroup实现自定义View,且有绘制自身的需求,就应该重写onDraw方法。

ViewGroup重写了dispatchDraw方法,这个方法将负责绘制子View,关键代码如下:

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

    .......

    boolean more = false;
    final long drawingTime = getDrawingTime();

    if (usingRenderNodeProperties) canvas.insertReorderBarrier();
    final int transientCount = mTransientIndices == null ? 0 : mTransientIndices.size();
    int transientIndex = transientCount != 0 ? 0 : -1;
    // Only use the preordered list if not HW accelerated, since the HW pipeline will do the
    // draw reordering internally
    final ArrayList<View> preorderedList = usingRenderNodeProperties
            ? null : buildOrderedChildList();
    final boolean customOrder = preorderedList == null
            && isChildrenDrawingOrderEnabled();
    
    // 循环处理子View
    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) {
            	// 绘制子View
                more |= drawChild(canvas, transientChild, drawingTime);
            }
            transientIndex++;
            if (transientIndex >= transientCount) {
                transientIndex = -1;
            }
        }

        final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
        final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
        if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
            // 绘制子View
        	more |= drawChild(canvas, child, drawingTime);
        }
    }
    .......
}

在这个方法中会循环处理子View,并调用drawChild方法完成对子View的绘制。一般而言,如果通过继承ViewGroup实现自定义View,是不用重写dispatchDraw方法的,直接维持ViewGroup的默认实现逻辑就好了。接下来,就让我们来看一下在drawChild中又做了些什么:

/**
 * Draw one child of this View Group. This method is responsible for getting
 * the canvas in the right state. This includes clipping, translating so
 * that the child's scrolled origin is at 0, 0, and applying any animation
 * transformations.
 *
 * @param canvas The canvas on which to draw the child
 * @param child Who to draw
 * @param drawingTime The time at which draw is occurring
 * @return True if an invalidate() was issued
 */
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
    return child.draw(canvas, this, drawingTime);
}

这个方法的实现逻辑很简单,直接调用View#draw方法完成了绘制逻辑。但是要注意,这里调用的并不是上文提到的draw方法,这里是View#draw的重载版本,关键代码如下:

/**
 * This method is called by ViewGroup.drawChild() to have each child view draw itself.
 *
 * This is where the View specializes rendering behavior based on layer type,
 * and hardware acceleration.
 */
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
    final boolean hardwareAcceleratedCanvas = canvas.isHardwareAccelerated();
    /* If an attached view draws to a HW canvas, it may use its RenderNode + DisplayList.
     *
     * If a view is dettached, its DisplayList shouldn't exist. If the canvas isn't
     * HW accelerated, it can't handle drawing RenderNodes.
     */
    boolean drawingWithRenderNode = mAttachInfo != null
            && mAttachInfo.mHardwareAccelerated
            && hardwareAcceleratedCanvas;

    boolean more = false;
    
    ........

    final boolean drawingWithDrawingCache = cache != null && !drawingWithRenderNode;
    
    ........

    if (!drawingWithDrawingCache) {
        if (drawingWithRenderNode) {
            mPrivateFlags &= ~PFLAG_DIRTY_MASK;
            ((DisplayListCanvas) canvas).drawRenderNode(renderNode);
        } else {
            // ① 判断是否需要跳过对自身的绘制流程
            if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
                mPrivateFlags &= ~PFLAG_DIRTY_MASK;
                // 跳过View的绘制流程,直接调用dispatchView绘制子View
                dispatchDraw(canvas);
            } else {
            	// 绘制View
                draw(canvas);
            }
        }
    } else if (cache != null) {
        mPrivateFlags &= ~PFLAG_DIRTY_MASK;
        if (layerType == LAYER_TYPE_NONE || mLayerPaint == null) {
            // 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();
            if (alpha < 1) {
                mLayerPaint.setAlpha((int) (alpha * layerPaintAlpha));
            }
            canvas.drawBitmap(cache, 0.0f, 0.0f, mLayerPaint);
            if (alpha < 1) {
                mLayerPaint.setAlpha(layerPaintAlpha);
            }
        }
    }

    ........

    return more;
}

这个方法中的代码很多,这里就只保留了关键部分的代码。在代码①的位置会先判断是否需要跳过对自己的绘制流程:如果当前的View是ViewGroup,并且不需要绘制背景时,就会直接调用dispatchDraw方法绘制子View;否则会调用自身的draw方法,后续步骤就和View中的绘制流程一致了。

到了这里,ViewGroup的绘制流程也分析完毕了,整体看来还是比较简单的。如果要继承ViewGroup实现自定义View,绘制流程其实完全可以保持默认实现。除非我们有一些特殊的绘制逻辑,比如像LinearLayout一样在子View之间绘制分割线,那就可以通过重写onDraw方法实现。

整体的流程图

上面分别从View和ViewGroup的角度讲解了绘制流程,这里再以流程图的形式归纳一下整个draw过程,便于加深记忆:

Android View原理解析之绘制流程(draw)_第1张图片

小结

绘制流程中的许多工作已经被系统完成了,相比前两个步骤还是比较容易的。但是如果想要获得更好的学习效果,最好还是打开AndroidStudio,循着本文的脉络试着一步步探索源码中的逻辑。

参考资料

https://blog.csdn.net/lfdfhl/article/details/51435968
https://blog.csdn.net/a553181867/article/details/51570854

你可能感兴趣的:(Android进阶)