自定义View Draw过程- 最易懂的自定义View原理系列(4)

前言

  • 自定义View是Android开发者必须了解的基础
  • 网上有大量关于自定义View原理的文章,但存在一些问题:内容不全、思路不清晰、无源码分析、简单问题复杂化等等
  • 今天,我将全面总结自定义View原理中的draw过程,我能保证这是市面上的最全面、最清晰、最易懂的
  • 文章较长,建议收藏等充足时间再进行阅读

目录

目录


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;
    }
    ...    
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53

下面,我们继续分析在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);
    }
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

步骤3: onDraw()

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

protected void onDraw(Canvas canvas) {

        // 复写从而实现绘制逻辑
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

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

步骤4: dispatchDraw()

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

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5

步骤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);
    }
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

至此,单一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;
    }
    ...    
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55

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

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

至此,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);
}
 
 
  • 1
  • 2
  • 3
  • 该方法用于设置 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过程

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值