zoukankan      html  css  js  c++  java
  • 自定义View Draw过程(4)

    目录


    目录

    1. 知识基础

    具体请看我写的另外一篇文章:自定义View基础 - 最易懂的自定义View原理系列


    2. draw过程作用

    绘制View视图


    3. draw过程详解

    同measure、layout过程一样,draw过程根据View的类型分为两种情况:

    1. 如果View = 单一View,则仅绘制本身View;
    2. 如果View = VieGroup(包含子View),除了绘制自身View外,还需要绘制子View。

    接下来,我将详细分析这两种情况下的draw过程。

    3.1 单一View的draw过程

    3.1.1 应用场景

    在没有现成的View,需要自己实现的时候,就使用自定义View,一般继承自View、SurfaceView等

    特点是:不包含子View。

    3.1.2 原理(步骤)

    • 步骤1:View绘制自身(含背景、内容);
    • 步骤2:绘制装饰(滚动指示器、滚动条、和前景)

    3.1.3 单一View的的具体draw过程

    如下图所示:


    单一View的layout过程

    下面我将一个个方法进行详细分析。

    步骤1: draw()

    作用:根据给定的 Canvas 自动渲染 View(包括其所有子 View)。

    在调用该方法之前必须要完成 layout 过程

    public void draw(Canvas canvas) {
    
    // 特别注意:
    // 所有的视图最终都是调用 View 的 draw ()绘制视图( ViewGroup 没有复写此方法)
    // 在自定义View时,不应该复写该方法,而是复写 onDraw(Canvas) 方法进行绘制。
    // 如果自定义的视图确实要复写该方法,那么需要先调用 super.draw(canvas)完成系统的绘制,然后再进行自定义的绘制。
        ...
    
        /*
         * 绘制过程如下:
         *   1. 绘制view背景
         *   2. 绘制view内容
         *   3. 绘制子View
         *   4. 绘制装饰(渐变框,滑动条等等)
         */
    
    
        int saveCount;
        if (!dirtyOpaque) {
              // 步骤1: 绘制本身View背景
            drawBackground(canvas);
        }
    
            // 如果有必要,就保存图层(还有一个复原图层)
            // 优化技巧:
            // 当不需要绘制 Layer 时,“保存图层“和“复原图层“这两步会跳过
            // 因此在绘制的时候,节省 layer 可以提高绘制效率
            final int viewFlags = mViewFlags;
            if (!verticalEdges && !horizontalEdges) {
    
            if (!dirtyOpaque) 
                 // 步骤2:绘制本身View内容
                onDraw(canvas);
            //  View 中:默认为空实现
            // ViewGroup中:自定义View时需要进行复写!!!!
    
    ..
            // 步骤3:绘制子View
            dispatchDraw(canvas);
           // 由于单一View没有子View,所以View 中:默认为空实现
    
    
            ...
    
            // 步骤4:绘制滑动条和前景色等等
            onDrawScrollBars(canvas);
    
            // we're done...
            return;
        }
        ...    
    }

    下面,我们继续分析在draw()中调用的drawBackground()、 onDraw()dispatchDraw()onDrawScrollBars(canvas)

    步骤2: drawBackground()

    • 作用:绘制自身View的背景
    • 源码分析如下:
    private void drawBackground(Canvas canvas) {
        // 获取背景 drawable
        final Drawable background = mBackground;
        if (background == null) {
            return;
        }
        // 根据在 layout 过程中获取的 View 的位置参数,来设置背景的边界
        setBackgroundBounds();
    
        .....
    
        // 获取 mScrollX 和 mScrollY值 
        final int scrollX = mScrollX;
        final int scrollY = mScrollY;
        if ((scrollX | scrollY) == 0) {
            background.draw(canvas);
        } else {
            // 如果 mScrollX 和 mScrollY 有值,则对 canvas 的坐标进行偏移
            canvas.translate(scrollX, scrollY);
    
    
            // 调用 Drawable 的 draw 方法绘制背景
            background.draw(canvas);
            canvas.translate(-scrollX, -scrollY);
        }
    }

    步骤3: onDraw()

    • 作用:绘制自身View的内容
    • 源码分析如下:
    // 由于 View 的内容各不相同,所以该方法是一个空实现
    // 在自定义绘制过程中,需要由子类去实现复写该方法从而绘制自身的内容。
    
    protected void onDraw(Canvas canvas) {
    
            // 复写从而实现绘制逻辑
    }

    请记住:自定义View中必须 且 只需要复写onDraw()

    步骤4: dispatchDraw()

    • 作用:绘制子View
    • 源码分析:
    // 单一View 中:默认为空实现
    // 因为没有子View,不需要绘制子View,所以为空实现
    protected void dispatchDraw(Canvas canvas) {
    
    }

    步骤5: 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);
        }
    }

    至此,单一View的draw过程已经分析完毕。

    3.1.4 总结

    单一View的draw过程解析如下:

    只需要绘制自身View


    单一View的draw过程

    3.2 ViewGroup的draw过程

    3.2.1 应用场景

    利用现有组件根据特定布局方式来组成新的组件,大多继承自ViewGroup或各种Layout

    特点:含有子View

    3.2.2 原理(步骤)


    原理

    步骤1:ViewGroup绘制自身(含背景、内容);
    步骤2:ViewGroup遍历子View并绘制包含的所有子View;

    类似于单一View的draw过程

    步骤3:ViewGroup绘制装饰(滚动指示器、滚动条、和前景)

    这样自上而下、一层层地传递下去,直到完成整个View树的draw过程

    3.2.3 ViewGroup的具体draw过程

    如下图所示:


    ViewGroup的draw过程

    下面我将对每个步骤和方法进行详细分析。

    步骤1:draw()

    • 作用:根据给定的 Canvas 自动渲染 View

      在调用该方法之前必须要完成 layout 过程

    • 源码分析:(与单一View draw过程的draw()类似)

    public void draw(Canvas canvas) {
    
    // 特别注意:
    // 所有的视图最终都是调用 View 的 draw ()绘制视图( ViewGroup 没有复写此方法)
    // 在自定义View时,不应该复写该方法,而是复写 onDraw(Canvas) 方法进行绘制。
    // 如果自定义的视图确实要复写该方法,那么需要先调用 super.draw(canvas)完成系统的绘制,然后再进行自定义的绘制。
        ...
    
        /*
         * 绘制过程如下:
         *   1. 绘制view背景
         *   2. 绘制view内容
         *   3. 绘制子View
         *   4. 绘制装饰(渐变框,滑动条等等)
         */
    
    
        int saveCount;
        if (!dirtyOpaque) {
              // 步骤1: 绘制本身View背景
            drawBackground(canvas);
        }
    
            // 如果有必要,就保存图层(还有一个复原图层)
            // 优化技巧:
            // 当不需要绘制 Layer 时,“保存图层“和“复原图层“这两步会跳过
            // 因此在绘制的时候,节省 layer 可以提高绘制效率
            final int viewFlags = mViewFlags;
            if (!verticalEdges && !horizontalEdges) {
    
            if (!dirtyOpaque) 
                 // 步骤2:绘制本身View内容
                onDraw(canvas);
            //  View 中:默认为空实现
            // ViewGroup中:自定义View时需要进行复写!!!!
    
    ..
            // 步骤3:绘制子View
    
            dispatchDraw(canvas);
    
            // View 中:默认为空实现
            // ViewGroup中:系统已经复写好对其子视图进行绘制我们不需要复写
    
            ...
    
            // 步骤4:绘制滑动条和前景色等等
            onDrawScrollBars(canvas);
    
            // we're done...
            return;
        }
        ...    
    }

    步骤2:drawBackground()、步骤3:onDraw()、步骤5:onDrawForeground()

    • 与单一View的draw过程类似,详细请回看上面描述。
    • 下面直接进入与单一View draw过程最大不同的步骤4:dispatchDraw()

    步骤4: dispatchDraw()

    • 作用:遍历子View并绘制
    • 源码分析:
    // 仅贴出重要代码
    
    // 特别注意:
    // ViewGroup中:由于 系统 已经为我们实现了该方法,所以我们一般都不需要重写该方法
    // View中默认为空实现(因为没有子View可以去绘制)
    protected void dispatchDraw(Canvas canvas) {
        ......
    
    // 遍历子View
        final int childrenCount = mChildrenCount;
        ......
    
        for (int i = 0; i < childrenCount; i++) {
                ......
                if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
                        transientChild.getAnimation() != null) {
                  // 绘制视图
                  // 继续看下面源码分析
                    more |= drawChild(canvas, transientChild, drawingTime);
                }
                ......
        }
    }
    
    
    // drawChild()源码分析
    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        // 最终还是调用了子 View 的 draw ()进行子View的绘制
        return child.draw(canvas, this, drawingTime);
    }

    至此,ViewGroup的draw过程已经分析完毕。

    3.2.4 总结

    对于ViewGroup的draw过程流程如下:


    ViewGroup的draw过程

    4. 其他细节问题:View.setWillNotDraw()

    View 中有一个特殊的方法:setWillNotDraw()

    public void setWillNotDraw(boolean willNotDraw) {
        setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
    }
    • 该方法用于设置 WILL_NOT_DRAW 标记位
    • 该标记位的作用是:当一个View不需要绘制内容时,系统进行相应优化

      默认情况下:View 不启用该标记位(设置为true);ViewGroup 默认启用(设置为false)

    应用场景:

    • setWillNotDraw参数设置为true:当自定义View继承自 ViewGroup 、且本身并不具备任何绘制时,设置为 true 后,系统会进行相应的优化。

    • setWillNotDraw参数设置为false:当自定义View继承自 ViewGroup 、且需要绘制内容时,那么设置为 false,来关闭 WILL_NOT_DRAW 这个标记位。


    5. 总结

    • 对于ViewGroup的draw过程

    步骤1:ViewGroup绘制自身(含背景、内容);
    步骤2:ViewGroup遍历子View并绘制包含的所有子View;

    类似于单一View的draw过程

    步骤3:ViewGroup绘制装饰(滚动指示器、滚动条、和前景)

    • 对于View的draw过程
      只需要绘制自身(含背景、内容)+装饰即可

    • 一个图总结自定义View - draw过程,如下图:


    自定义View - draw过程
  • 相关阅读:
    【搞笑】各种程序评测结果
    【UVA】P1510 Neon Sign
    【转载】实用:根号怎么打出来? <引自百度>
    【转载】C++中的模板template <typename T>
    * ! THUSC2017杜老师
    * ! THUSCH2017巧克力
    ! BJOI2019光线
    ! BJOI2019奥术神杖
    ! TJOI/HEOI2016字符串
    ! TJOI/HEOI2016求和
  • 原文地址:https://www.cnblogs.com/xinmengwuheng/p/7070092.html
Copyright © 2011-2022 走看看