8、onDraw源码分析:Canvas与Paint的配合、View的onDraw默认实现、ViewGroup的dispatchDraw与onDraw的区别

好,咱们今天来聊聊 onDraw。说实话,这个方法是整个 View 绘制流程里最「接地气」的一个。你想想看,前面那些 measure、layout 都是在算位置、算大小,真正把东西画到屏幕上的,就是 onDraw 这一下。

我个人习惯把 onDraw 比作「画家的画笔落纸的那一刻」。前面准备再多,这一步画不好,用户看到的就是一团糟。所以,咱们得把 Canvas 和 Paint 这对「黄金搭档」搞清楚,再把 View 和 ViewGroup 的绘制差异弄明白。

8.1 Canvas 与 Paint:画笔和颜料

Canvas 和 Paint,说白了就是「画布」和「画笔」。Canvas 负责「怎么画」——画圆、画方、画路径;Paint 负责「画成什么样」——颜色、粗细、阴影、渐变。

我刚开始做自定义 View 时,经常把这两个搞混。后来想了个笨办法:Canvas 是动作,Paint 是属性

核心理解:
  • Canvas 提供绘制方法(drawCircle、drawRect、drawText 等)
  • Paint 提供绘制参数(颜色、样式、字体、抗锯齿等)
  • 两者缺一不可,配合使用才能画出东西

来看一段实际代码,这是我项目中经常用的写法:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    
    // 创建画笔,设置属性
    Paint paint = new Paint();
    paint.setColor(Color.RED);
    paint.setStyle(Paint.Style.FILL);
    paint.setAntiAlias(true);
    paint.setStrokeWidth(5f);
    
    // 用 Canvas 绘制圆形
    canvas.drawCircle(getWidth() / 2f, getHeight() / 2f, 100f, paint);
    
    // 修改画笔,画矩形
    paint.setColor(Color.BLUE);
    paint.setStyle(Paint.Style.STROKE);
    canvas.drawRect(50, 50, 200, 200, paint);
}

你看,同一个 paint 对象,改了颜色和样式,画出来的东西就完全不一样。Canvas 只管调用 drawCircle、drawRect,具体画成什么样,全看 Paint 怎么设置。

我的经验: 不要在 onDraw 里 new Paint()。onDraw 会被频繁调用,每次创建对象会导致内存抖动。我一般把 Paint 对象放在构造方法或 onSizeChanged 里初始化。

8.2 View 的 onDraw 默认实现

咱们来看看 View 的 onDraw 默认长什么样。源码其实很简单:

// View.java
protected void onDraw(Canvas canvas) {
    // 默认什么都不做
}

嗯,空的。为什么?因为 View 本身没有需要绘制的内容。它只是一个容器,具体画什么由子类决定。

但这里有个细节:如果 View 的背景需要绘制,系统会在调用 onDraw 之前帮你画好。你看 draw 方法的源码流程:

  1. 先绘制背景(drawBackground)
  2. 再调用 onDraw(绘制内容)
  3. 然后绘制子 View(dispatchDraw)
  4. 最后绘制滚动条、前景等

所以,你如果在 onDraw 里画东西,背景已经被画好了。你只需要关注「内容层」就行。

注意: 如果你重写了 onDraw 但没有调用 super.onDraw(canvas),背景依然会被绘制。因为背景绘制是在 draw 方法里完成的,不在 onDraw 里。我曾经因为这个踩过坑,以为不调 super 背景就没了,结果背景还在,只是我画的内容盖在上面。

8.3 ViewGroup 的 dispatchDraw 与 onDraw

好,接下来是重点。ViewGroup 和 View 的绘制逻辑有什么不同?

说白了,ViewGroup 多了一个 dispatchDraw 方法。这个方法专门用来绘制子 View。

咱们来看 ViewGroup 的绘制流程:

// ViewGroup.java
@Override
protected void onDraw(Canvas canvas) {
    // ViewGroup 默认不绘制任何内容
    // 除非你重写它
}

@Override
protected void dispatchDraw(Canvas canvas) {
    // 遍历所有子 View,调用它们的 draw 方法
    for (int i = 0; i < getChildCount(); i++) {
        View child = getChildAt(i);
        if (child.getVisibility() != GONE) {
            drawChild(canvas, child, drawingTime);
        }
    }
}

看到了吗?ViewGroup 的 onDraw 默认是空的,它不画自己。它只负责通过 dispatchDraw 去画子 View。

为什么会这样?你想想看,一个 LinearLayout 或者 RelativeLayout,它本身需要画什么吗?不需要。它只是把子 View 排列好,让子 View 自己去画。

但如果你想让 ViewGroup 有背景色、边框或者一些装饰效果,那就需要重写 onDraw 了。比如:

public class MyViewGroup extends LinearLayout {
    
    private Paint borderPaint;
    
    public MyViewGroup(Context context) {
        super(context);
        borderPaint = new Paint();
        borderPaint.setColor(Color.BLACK);
        borderPaint.setStyle(Paint.Style.STROKE);
        borderPaint.setStrokeWidth(2f);
        // 关键:告诉系统我需要绘制
        setWillNotDraw(false);
    }
    
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 画一个边框
        canvas.drawRect(0, 0, getWidth(), getHeight(), borderPaint);
    }
}
关键点:setWillNotDraw(false)

ViewGroup 默认会开启一个优化:如果它不需要绘制,系统就跳过 onDraw 调用。你如果重写了 onDraw,必须调用 setWillNotDraw(false) 来关闭这个优化。否则你的 onDraw 永远不会被执行。

我曾经排查过一个 bug,自定义 ViewGroup 画了半天边框没出来,最后发现就是忘了调这个方法。

8.4 两者的区别总结

对比项 View ViewGroup
onDraw 作用 绘制自身内容 绘制自身内容(默认空)
dispatchDraw 作用 无(View 没有子 View) 遍历并绘制所有子 View
默认行为 onDraw 空实现 onDraw 空实现,dispatchDraw 绘制子 View
绘制顺序 背景 → onDraw → 前景 背景 → onDraw → dispatchDraw → 前景
优化标记 setWillNotDraw 控制是否跳过 onDraw

简单来说:View 只管画自己,ViewGroup 默认不画自己,只管画孩子。如果你想让 ViewGroup 画点东西,记得关掉优化开关。

避坑指南: 我曾经在自定义 ViewGroup 里重写 onDraw 画分割线,结果怎么都不显示。排查了半天,发现是父布局的 clipChildren 属性设成了 false,导致绘制区域被裁剪了。嗯,这种问题往往不是 onDraw 本身的问题,而是绘制环境的问题。遇到类似情况,先检查 clip 相关属性。

8.5 知识体系图

下面这张图帮你理清 onDraw 相关的核心逻辑:

onDraw 核心知识体系 onDraw 方法 Canvas + Paint 配合 Canvas:绘制动作(drawCircle等) Paint:绘制属性(颜色、样式等) 两者缺一不可 View 默认实现 onDraw 默认空实现 背景由 draw 方法绘制 子类重写实现具体内容 ViewGroup 区别 onDraw:默认空,画自身 dispatchDraw:遍历绘制子 View setWillNotDraw 控制优化 重写 onDraw 需关优化 核心区别:View 画自己,ViewGroup 画孩子 dispatchDraw 是 ViewGroup 独有的绘制子 View 的方法

这张图把 onDraw 的三个核心维度串起来了。左边是工具(Canvas + Paint),中间是基础(View 默认实现),右边是扩展(ViewGroup 的特殊处理)。你写代码时,先想清楚自己属于哪种场景,再决定怎么下手。


公众号:蓝海资料掘金营,微信deep3321