美文网首页Android进阶
Android View 绘制流程(Draw)源码解析

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

作者: linda_zhou | 来源:发表于2017-08-23 21:33 被阅读0次

    前言

    在前面两篇文章中我们具体分析了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)源码解析

    相关文章

      网友评论

        本文标题:Android View 绘制流程(Draw)源码解析

        本文链接:https://www.haomeiwen.com/subject/wogorxtx.html