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 怎么设置。
8.2 View 的 onDraw 默认实现
咱们来看看 View 的 onDraw 默认长什么样。源码其实很简单:
// View.java
protected void onDraw(Canvas canvas) {
// 默认什么都不做
}
嗯,空的。为什么?因为 View 本身没有需要绘制的内容。它只是一个容器,具体画什么由子类决定。
但这里有个细节:如果 View 的背景需要绘制,系统会在调用 onDraw 之前帮你画好。你看 draw 方法的源码流程:
- 先绘制背景(drawBackground)
- 再调用 onDraw(绘制内容)
- 然后绘制子 View(dispatchDraw)
- 最后绘制滚动条、前景等
所以,你如果在 onDraw 里画东西,背景已经被画好了。你只需要关注「内容层」就行。
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);
}
}
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 画点东西,记得关掉优化开关。
8.5 知识体系图
下面这张图帮你理清 onDraw 相关的核心逻辑:
这张图把 onDraw 的三个核心维度串起来了。左边是工具(Canvas + Paint),中间是基础(View 默认实现),右边是扩展(ViewGroup 的特殊处理)。你写代码时,先想清楚自己属于哪种场景,再决定怎么下手。
公众号:蓝海资料掘金营,微信deep3321