美文网首页安卓UIAndroid自定义View
自定义 View - onDraw 过程详解

自定义 View - onDraw 过程详解

作者: Kip_Salens | 来源:发表于2019-04-21 23:49 被阅读21次

    之前两篇文章分析了 onMeasure 过程和 onLayout 过程,不熟悉的童鞋可以回头去复习下,本篇文章来分析绘制过程的最后一个 onDraw 过程。这个过程的绘制使用到的 Paint 和 Canvas 在之前也有讲解到,在本篇的练习代码中有使用到,不会具体讲解这些知识点,不熟悉的话可以看看我之前的文章

    自定义 View - Paint 详解

    自定义 View - Canvas 详解

    View 绘制过程

    绘制过程也不复杂,在 View draw 方法的源码中注释写的也很详细,先给出一张图。

    图中的绘制过程也是代码中给出的绘制过程,对于 单一 View 和 ViewGroup 来说,差别仅仅在于绘制子 View 上, 因为单一 View 没有子 View,所以不需要绘制子 View,即 dispatchDraw(canvas) 是一个空实现;那对于 ViewGroup 来说,如果有子 View 的话,就需要实现该方法,绘制子 View,过程循环遍历子 View,然后调用 子 View 的 draw 方法,这样就回到了子 View 的绘制过程,具体分析看后面。

    draw_process.png

    1.单一 View 绘制

    单一 View 的绘制过程就是上面图中的流程,下面主要看下代码中的注释。

        
        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)
             */
             上面指出绘制过程的 6 个步骤,其中 2 和 5 是对图层的操作,不是必要的
             1、绘制背景,该方法发生在一个叫 drawBackground() 的方法里,但这个方法是 private 的,不能重写,如果要设置背景,只能用自带的 API 去设置(xml 布局文件的 android:background 属性以及 Java 代码的 View.setBackgroundXxx() 方法
             
             3、绘制主体部分,例如,对于 TextView,绘制文字部分
             
             4、绘制子 View,单一 View 没有子 View,空实现,对于继承 ViewGroup 的 Layout,如果有子 View 需要实现该方法。
             
             6、绘制滑动边缘渐变和滑动条、以及前景,这些东西是在一个方法中绘制的,不能分开
    
            int saveCount;
    
            // Step 1:绘制背景
            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:绘制主体部分
                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, 绘制装饰部件,滚动条,前景等
                onDrawForeground(canvas);
    
                // Step 7, 具有焦点时,绘制焦点高亮
                drawDefaultFocusHighlight(canvas);
    
                if (debugDraw()) {
                    debugDrawFocus(canvas);
                }
    
                // 其实到这里已经结束了
                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 总调度函数绘制的过程,下面看下每个过程的具体函数。

    绘制背景

    背景绘制过程采用 Drawable 的 draw 方法,注意这个方法是 private,也就是说背景绘制我们不能重写,如果要设置背景,只能用自带的 API 去设置(xml 布局文件的 android:background 属性以及 Java 代码的 View.setBackgroundXxx() 方法

    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;
                }
            }
    
            // background 是一个 Drawable,背景绘制过程采用 Drawable 的 draw 方法
            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);
            }
        }
    

    onDraw

      // 绘制 View 本身的内容
      // 继承自 View 的单一 View 实现各不相同,需要自己实现,如 TextView 绘制文字
      protected void onDraw(Canvas canvas) {
    
      }
    

    dispatchDraw

    
    /**
      * dispatchDraw 用来绘制子 View,
      * 因为单一 View 没有子 View,所以不需要绘制子 View,即 dispatchDraw(canvas) 是一个空实现;那对于
      * ViewGroup 来说,如果有子 View 的话,就需要实现该方法,绘制子 View,过程循环遍历子 View
      */
      protected void dispatchDraw(Canvas canvas) {
    
            ... // 空实现
    
      }
    

    onDrawForeground

    绘制前景,这个过程也会绘制滚动条和指示器等小装饰,然后绘制前景,前景绘制过程和背景过程类似,也是通过 Drawable 的 draw 方法绘制的,装饰和前景一起绘制,该方法不能拆分。

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

    2. View Group 绘制过程

    由于 ViewGroup 继承 View,所以上面几个过程基本是一致的,除了 dispatchDraw 方法,一般情况下 ViewGroup 是有子 View 的,所以通过 dispatchDraw 方法来进行子 View 的绘制,下面主要分析一下 dispatchDraw 方法。

     @Override
        protected void dispatchDraw(Canvas canvas) {
             final int childrenCount = mChildrenCount;
            final View[] children = mChildren;
            ...
            
            for (int i = 0; i < childrenCount; i++) {
                ...
    
                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 的绘制方法
        protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
            return child.draw(canvas, this, drawingTime);
        }
    

    dispatchDraw 实际上就是对子 View 的遍历,然后依次调用子 View 的绘制方法,就按照上面的 View 的绘制方法执行,当然也有可能还是 ViewGroup,那么还会继续调用 dispatchDraw 方法,遍历子 View。

    绘制方法重写顺序分析

    上面过程分析了 View 的绘制过程,那么我们进行自定义 View 时,会有这样一个问题,有时我们会只用 super 调用 父类方法,我们自己重写的部分代码,是放在上面,还是下面呢,有何影响?下面就来对上面的方法进行分析,写在 super 上面和下面有何影响。

    (1) super.onDraw() 前

    public class MyView extends View {  
        ...
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            ... // 自定义绘制代码
        }
        ...
    }
    

    写在 onDraw 的下面,自定义的绘制部分会盖住控件原有的内容,如在 ImageView 的图片上显示图片的尺寸信息等。

    before_draw.jpg

    (2) super.onDraw() 后

    写在 onDraw 的下面,自定义的绘制部分被控件原有的内容盖住,如在文字下面绘制强调色。

    after_draw.jpg

    **(3) ViewGroup 子类的 onDraw() 中****

    一个继承自 ViewGroup 的子类,本身在 onDraw 是一个空实现,在 onDraw 中进行绘制,如果没有子 View,那么会显示出绘制的内容,如果有子 View,子 View 会遮住 onDraw 中绘制的内容。

    ondraw_layout.jpg

    摘自 Hencoder

    另外有一点,出于效率的考虑,ViewGroup 默认会绕过 draw() 方法,换而直接执行 dispatchDraw(),以此来简化绘制流程。所以如果自定义了某个 ViewGroup 的子类(比如 LinearLayout)并且需要在它的除 dispatchDraw() 以外的任何一个绘制方法内绘制内容,可能会需要调用 View.setWillNotDraw(false) 这行代码来切换到完整的绘制流程(是「可能」而不是「必须」的原因是,有些 ViewGroup 是已经调用过 setWillNotDraw(false) 了的,例如 ScrollView)

    (4) 在 dispatchDraw 下面

    写在 dispatchDraw 下面,会遮住子 View 部分

    dispatch_draw.jpg

    (5) 在onDrawForeground 后

    写在 在onDrawForeground 后面会会遮住滑动边缘渐变、滑动条和前景

    after_foreground.jpg

    (6) 在onDrawForeground 前

    写在 在onDrawForeground 前面会被前景部分遮住

    before_foreground.jpg

    最后给出一张 Hencoder 大神总结的一张图

    draw_position.png

    参考

    HenCoder Android 开发进阶:自定义 View 1-5 绘制顺序

    练习代码地址

    相关文章

      网友评论

        本文标题:自定义 View - onDraw 过程详解

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