外观
draw、onDraw、dispatchDraw 各画什么?
标准回答
draw(Canvas) 是组织一个 View 绘制过程的方法。它不只是画某一种内容,而是安排背景、自身内容、子 View 和前景等阶段。只想画控件自己的内容时,通常不需要重写整套 draw。
onDraw(Canvas) 负责当前 View 自身的内容。例如 TextView 的文字、自定义仪表盘的刻度和指针,都属于自身内容。背景通常由框架在另一个阶段处理;容器里的按钮和文本,也不由容器的 onDraw 一股脑画出来。
dispatchDraw(Canvas) 负责组织子 View 的绘制。以一个装着两个按钮的 FrameLayout 为例,容器的 dispatchDraw 通过 drawChild 等路径安排这两个孩子绘制;每个按钮仍负责自己的背景、文字等内容。如果孩子也是 ViewGroup,就继续组织下一层孩子。普通 View 没有孩子,因此默认 dispatchDraw 没有子 View 要画。硬件加速时可能复用绘制记录,不代表每次都重新调用所有孩子的 onDraw。
三个方法的关系可以简化为:父容器的 draw 先处理背景,再调用自己的 onDraw,然后进入 dispatchDraw 绘制孩子,之后继续处理前景等内容。这里强调职责与相对阶段;实际实现还包含裁剪、缓存、图层和跳过无效工作的分支。
如果想在孩子上方画角标,可以重写容器的 dispatchDraw:先调用 super.dispatchDraw(canvas) 保留孩子绘制,再画角标。把自定义绘制放在 super 之前,则发生在孩子绘制之前,可能被孩子盖住。这解释的是普通 View 绘制阶段,最终覆盖效果还要考虑容器前景、Z、裁剪和独立 Surface。
关键机制
- draw:总流程。普通自定义绘制不要漏掉背景、子 View 和前景等框架行为。
- onDraw:画当前控件自己的内容。自定义 ViewGroup 需要画自身内容时,要留意 setWillNotDraw 等设置。
- dispatchDraw:组织画孩子。重写后通常保留 super.dispatchDraw,否则可能跳过默认的子 View 绘制。
通俗例子
FrameLayout 的 background 画卡片底色,容器 onDraw 可以画底部网格,dispatchDraw 安排两个按钮绘制。在 super.dispatchDraw 之后画一个红点,就是先有按钮内容、再加角标;红点只是绘制内容,本身不是一个可点击的子 View。
追问与回答
onDraw 不执行,为什么孩子仍然能显示?
因为容器自身内容与孩子绘制是不同阶段。某些 ViewGroup 默认不需要画自身内容,框架可以跳过其 onDraw,但仍通过 dispatchDraw 绘制孩子。如果需要容器自己的网格或装饰,可以结合 setWillNotDraw(false) 与正确的刷新请求开启自身绘制;不能把“父容器没走 onDraw”理解成孩子一定不能画。
重写 dispatchDraw 时,super 应该放在哪里?
通常先调用 super.dispatchDraw(canvas) 让孩子正常绘制,再画希望叠加的内容。若希望自定义内容位于孩子之前,就放在 super 前。不要为了叠加一层就省略 super,否则默认孩子绘制可能被跳过。修改 Canvas 的裁剪或变换时,用 save / restore 限定影响范围,避免把后面的绘制一起改变。
为什么自定义 View 通常重写 onDraw,而不是 draw?
因为只画一个圆、文字或仪表盘时,需要补的是自身内容,onDraw 正好对应这个职责。draw 还组织背景、孩子和前景等工作,重写它更容易破坏整体流程。自定义容器要在孩子前后加绘制,则考虑 dispatchDraw;按想改变的绘制阶段选方法。
易混淆点
dispatchDraw 是绘制分发,dispatchTouchEvent 是触摸事件分发。它们虽然都带 dispatch,处理的事情不同。draw / onDraw 返回也都不能单独证明像素已经上屏。
参考资料
内容来源
- Android 技术札记 · 原条目
draw-order