Android View 绘制流程(Draw)源码解析

前言

在前面两篇文章中我们具体分析了View的measure和layout流程,当确定了View的大小和位置后,我们是如何把View显示到屏幕上的呢?本篇就来分析一下View的draw流程,文中源码基于 Android API 21。

View绘制流程

由setContentView探究Activity界面加载流程及Activity、Window和DecorView的关系中,我们提到View三大工作流程是从ViewRootImpl#performTraversals开始的,其中performMeasure、performLayout、performDraw方法分别对应了View的测量、布局、绘制。因此我们从performDraw开始分析View绘制流程。

    private void performDraw() {
        ...
        final boolean fullRedrawNeeded = mFullRedrawNeeded;
        mFullRedrawNeeded = false;

        mIsDrawing = true;
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "draw");
        try {
            draw(fullRedrawNeeded);
        } finally {
            mIsDrawing = false;
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
        //省略...
    }

performDraw方法内部会调用draw(boolean fullRedrawNeeded)方法,参数fullRedrawNeeded意为是否需要全部重新绘制视图。

    private void draw(boolean fullRedrawNeeded) {
       //省略...
        //获取需要重新绘制的区域
        final Rect dirty = mDirty;
        if (mSurfaceHolder != null) {
            // The app owns the surface, we won't draw.
            dirty.setEmpty();//如果mSurfaceHolder != null则把绘制区域置为(0,0,0,0)。
            if (animating) {
                if (mScroller != null) {
                    mScroller.abortAnimation();
                }
                disposeResizeBuffer();
            }
            return;
        }
        //如果需要全部重绘,则把绘制区域置为整个屏幕的大小。
        if (fullRedrawNeeded) {
            mAttachInfo.mIgnoreDirtyState = true;
            dirty.set(0, 0, (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
        }

        ...

        int xOffset = 0;
        int yOffset = curScrollY;
        final WindowManager.LayoutParams params = mWindowAttributes;
        final Rect surfaceInsets = params != null ? params.surfaceInsets : null;
        if (surfaceInsets != null) {
            xOffset -= surfaceInsets.left;
            yOffset -= surfaceInsets.top;

            // Offset dirty rect for surface insets.
            dirty.offset(surfaceInsets.left, surfaceInsets.right);
        }

                if (!dirty.isEmpty() || mIsAnimating) {
            if (mAttachInfo.mHardwareRenderer != null && mAttachInfo.mHardwareRenderer.isEnabled()) {
                ...
                dirty.setEmpty();
                ...
            } else {
                //省略...
                if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
                    return;
                }
            }
        }

      }

这个方法主要是确定dirty的区域,即确定需要绘制的区域。继续看drawSoftware方法。

   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实例
            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;
            }

            // TODO: Do this in native
            canvas.setDensity(mDensity);
        } 

        try {
            // If this bitmap's format includes an alpha channel, we
            // need to clear it before drawing so that the child will
            // properly re-composite its drawing on a transparent
            // background. This automatically respects the clip/dirty region
            // or
            // If we are applying an offset, we need to clear the area
            // where the offset doesn't appear to avoid having garbage
            // left in the blank areas.
            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 进行处理,最后执行mView.draw(canvas)开始绘制。这里的mView就是DecorView,绘制是从DecorView开始的。DecorView其实是个FrameLayout,不过ViewGroup并没有重写draw(Canvas canvas)方法,所以来看下View#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
        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);

            // Step 6, draw decorations (scrollbars)
            onDrawScrollBars(canvas);

            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }

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

        ...

    }

此方法列出了绘制View的具体步骤,如下:

  1. 绘制背景
  2. 必要时,保存画布的图层为褪色做准备
  3. 绘制View自身内容
  4. 绘制子View的内容
  5. 必要时,绘制褪色边缘并恢复图层
  6. 绘制装饰(列如滚动条)

不过,对于第2&5步,源码中有一行注释skip step 2 & 5 if possible (common case),意为:一般情况下,如果可以的话,跳过第2&5步。因此我们只分析其余4个步骤。

绘制背景

这一步是在drawBackground中执行的,看下View#drawBackground方法

private void drawBackground(Canvas canvas) {
        //mBackground为此View设置的背景
        final Drawable background = mBackground;
        //如果该View没有设置背景,则返回
        if (background == null) {
            return;
        } 

        if (mBackgroundSizeChanged) {
            //指定背景将被绘制的区域
            background.setBounds(0, 0,  mRight - mLeft, mBottom - mTop);
            mBackgroundSizeChanged = false;
            mPrivateFlags3 |= PFLAG3_OUTLINE_INVALID;
        }

        ...

        final int scrollX = mScrollX;
        final int scrollY = mScrollY;
        if ((scrollX | scrollY) == 0) {
            background.draw(canvas);
        } else {
            canvas.translate(scrollX, scrollY);
            background.draw(canvas);
            canvas.translate(-scrollX, -scrollY);
        }
    }

这个方法的作用就是将背景绘制到制定的画布上,并且考虑到了mScrollX、mScrollY不等于0的情况。

绘制View自身内容

这一步是在onDraw中完成的,看下View#onDraw方法。

    protected void onDraw(Canvas canvas) {
    }

可以看到View#onDraw是个空实现,需要子类去做具体实现,因为View自身的内容各不相同。

绘制子View的内容

这一步是通过dispatchDraw来完成的,View#dispatchDraw方法仍然是个空实现,不过ViewGroup对此方法进行了重写(对于开发人员来说,一般不需要重写此方法),因此我们来看看ViewGroup#dispatchDraw方法。

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

        ...

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

        if (usingRenderNodeProperties) canvas.insertReorderBarrier();
        // 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();

        for (int i = 0; i < childrenCount; i++) {
            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);
            }
        }
        if (preorderedList != null) preorderedList.clear();

        // Draw any disappearing views that have animations
        if (mDisappearingChildren != null) {
            final ArrayList<View> disappearingChildren = mDisappearingChildren;
            final int disappearingCount = disappearingChildren.size() - 1;
            // Go backwards -- we may delete as animations finish
            for (int i = disappearingCount; i >= 0; i--) {
                final View child = disappearingChildren.get(i);
                more |= drawChild(canvas, child, drawingTime);
            }
        }

        ...

    }

这个方法很长,其主要内容就是遍历子View,然后执行drawChild方法。

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

drawChild方法内部,会调用View中的draw(canvas, this, drawingTime)方法,注意这个方法和最开始的那个draw(canvas)方法不同,参数不一样的哦。

对于draw(canvas, this, drawingTime)这个方法我们先来看一段此方法的注释。

    /**
     * This method is called by ViewGroup.drawChild() to have each child view draw itself.
     * This draw() method is an implementation detail and is not intended to be overridden or
     * to be called from anywhere else other than ViewGroup.drawChild().
     */

翻译如下:
ViewGroup.drawChild()调用此方法让每个子视图绘制自身。这个draw()方法对细节做了具体实现,并且不能被覆盖或从除ViewGroup.drawChild()之外的其他地方调用。

这个方法它是用来绘制了每个子View的内容的,并且我们不能去修改其实现细节。具体细节这里就不做分析了。

绘制装饰

这一步是通过onDrawScrollBars来完成的,

protected final void onDrawScrollBars(Canvas canvas) {
     
            ...

            final int viewFlags = mViewFlags;

            final boolean drawHorizontalScrollBar =
                (viewFlags & SCROLLBARS_HORIZONTAL) == SCROLLBARS_HORIZONTAL;
            final boolean drawVerticalScrollBar =
                (viewFlags & SCROLLBARS_VERTICAL) == SCROLLBARS_VERTICAL
                && !isVerticalScrollBarHidden();

            if (drawVerticalScrollBar || drawHorizontalScrollBar) {
                final int width = mRight - mLeft;
                final int height = mBottom - mTop;

                final ScrollBarDrawable scrollBar = cache.scrollBar;

                final int scrollX = mScrollX;
                final int scrollY = mScrollY;
                final int inside = (viewFlags & SCROLLBARS_OUTSIDE_MASK) == 0 ? ~0 : 0;

                int left;
                int top;
                int right;
                int bottom;

                if (drawHorizontalScrollBar) {
                    int size = scrollBar.getSize(false);
                    if (size <= 0) {
                        size = cache.scrollBarSize;
                    }

                    scrollBar.setParameters(computeHorizontalScrollRange(),
                                            computeHorizontalScrollOffset(),
                                            computeHorizontalScrollExtent(), false);
                    final int verticalScrollBarGap = drawVerticalScrollBar ?
                            getVerticalScrollbarWidth() : 0;
                    top = scrollY + height - size - (mUserPaddingBottom & inside);
                    left = scrollX + (mPaddingLeft & inside);
                    right = scrollX + width - (mUserPaddingRight & inside) - verticalScrollBarGap;
                    bottom = top + size;
                    onDrawHorizontalScrollBar(canvas, scrollBar, left, top, right, bottom);
                    if (invalidate) {
                        invalidate(left, top, right, bottom);
                    }
                }

                if (drawVerticalScrollBar) {
                    int size = scrollBar.getSize(true);
                    if (size <= 0) {
                        size = cache.scrollBarSize;
                    }

                    scrollBar.setParameters(computeVerticalScrollRange(),
                                            computeVerticalScrollOffset(),
                                            computeVerticalScrollExtent(), true);
                    int verticalScrollbarPosition = mVerticalScrollbarPosition;
                    if (verticalScrollbarPosition == SCROLLBAR_POSITION_DEFAULT) {
                        verticalScrollbarPosition = isLayoutRtl() ?
                                SCROLLBAR_POSITION_LEFT : SCROLLBAR_POSITION_RIGHT;
                    }
                    switch (verticalScrollbarPosition) {
                        default:
                        case SCROLLBAR_POSITION_RIGHT:
                            left = scrollX + width - size - (mUserPaddingRight & inside);
                            break;
                        case SCROLLBAR_POSITION_LEFT:
                            left = scrollX + (mUserPaddingLeft & inside);
                            break;
                    }
                    top = scrollY + (mPaddingTop & inside);
                    right = left + size;
                    bottom = scrollY + height - (mUserPaddingBottom & inside);
                    onDrawVerticalScrollBar(canvas, scrollBar, left, top, right, bottom);
                    if (invalidate) {
                        invalidate(left, top, right, bottom);
                    }
                }
            }
        }
    }

此方法用来绘制滚动条,代码很长,但是逻辑清晰,根据滚动条的方向确定滚动条的left、top、right、bottom值,进而确定了滚动条的位置,然后调用onDrawHorizontalScrollBar 和或onDrawVerticalScrollBar方法进行绘制。

    protected void onDrawHorizontalScrollBar(Canvas canvas, Drawable scrollBar,
            int l, int t, int r, int b) {
        //指定scrollbar将被绘制的区域
        scrollBar.setBounds(l, t, r, b);
        //开始绘制
        scrollBar.draw(canvas);
    }

    protected void onDrawVerticalScrollBar(Canvas canvas, Drawable scrollBar,
            int l, int t, int r, int b) {
        scrollBar.setBounds(l, t, r, b);
        scrollBar.draw(canvas);
    }

至此,View的绘制流程就分析完了,希望能对您有所帮助,若文中有错误或表述不当的地方还望指出,互相交流,共同成长!

相关文章
由setContentView探究Activity界面加载流程及Activity、Window和DecorView的关系
Android View 测量流程(Measure)源码解析
Android View 布局流程(Layout)源码解析

    原文作者:linda_zhou
    原文地址: https://www.jianshu.com/p/92fb0089ce64
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞