dispatchDraw() — 是ViewGroup类中的一个方法,负责在Android层次结构中递归绘制所有子View。根据Android开发者文档(2026),dispatchDraw在主View的draw()方法中的onDraw之后自动调用,遍历所有子元素并调用它们自己的draw()。开发者在自定义ViewGroup中重写dispatchDraw以添加效果、在子元素之上叠加图形或更改绘制顺序。
要点
dispatchDraw(Canvas canvas) — 是ViewGroup类的protected方法,Android系统调用它来绘制当前容器的所有子View。dispatchDraw是标准绘制管道的一部分:首先执行onDraw(绘制View自身的内容),然后执行dispatchDraw(递归绘制子元素),最后执行onDrawForeground(绘制前景和滚动条)。该方法不适用于从应用程序代码直接调用。
ViewGroup中dispatchDraw的标准实现遍历所有子View,检查它们的可见性,并为每个子View调用draw(Canvas)。遍历顺序对应于子元素的索引(从0到childCount - 1)。如果子View重叠,列表中靠后的元素将绘制在靠前元素之上。通过更改dispatchDraw中的顺序,可以改变Z-order绘制顺序。
对于View(非ViewGroup),dispatchDraw方法为空——普通View没有子元素,因此没有可绘制的内容。该方法存在于View基类中,但仅在ViewGroup实例上调用。开发者可以通过dispatchDraw的默认行为检查View是否有子元素,但通常检查instanceof ViewGroup更简单。
View类的draw(Canvas)方法组织了一个三阶段绘制管道。第一阶段——调用onDraw(canvas),View在其中绘制自己的内容。第二阶段——调用dispatchDraw(canvas),启动子View的绘制。第三阶段——onDrawForeground(canvas),负责前景层、滚动条和涟漪效果。该序列保证了图形图层的正确叠加顺序。
onDraw总是在dispatchDraw之前执行。这意味着父View的内容显示在子View之下。如果需要在子元素之上绘制某些内容,可以在onDrawForeground中或在重写的dispatchDraw中调用super.dispatchDraw(canvas)然后在其上进行绘制来完成。背景(background)甚至在onDraw之前绘制——在drawBackground(canvas)方法中,位于draw()方法内部。
在硬件加速(hardware acceleration)下,draw管道通过GPU工作。View在DisplayList中绘制——这是一个绘图命令列表,会被缓存和重复使用。dispatchDraw在硬件加速模式下将子View的DisplayList添加到场景的全局DisplayList中。更改dispatchDraw可能导致DisplayList失效和缓存重建,从而影响性能。
| 方法 | 调用顺序 | 用途 |
|---|---|---|
| drawBackground | 1 | 绘制背景(background drawable) |
| onDraw | 2 | 绘制View自身的内容 |
| dispatchDraw | 3 | 绘制所有子View(仅用于ViewGroup) |
| onDrawForeground | 4 | 绘制前景、滚动条和涟漪效果 |
onDraw负责绘制View自身的内容——形状、文本、图像等属于该特定元素的图形。dispatchDraw负责绘制子View——ViewGroup内部的所有元素。如果ViewGroup没有重写dispatchDraw,则使用ViewGroup中的实现,它简单地递归遍历childCount并为每个子元素调用draw。
对于不绘制自身内容的ViewGroup(例如FrameLayout、LinearLayout),可以通过设置setWillNotDraw(true)来优化onDraw。在这种情况下,onDraw永远不会被调用,从而节省资源。dispatchDraw将继续工作并为子元素调用draw。所有标准ViewGroup(LinearLayout、RelativeLayout、ConstraintLayout)都使用setWillNotDraw(true)。
如果在没有调用super.dispatchDraw(canvas)的情况下重写dispatchDraw,子View将不会被绘制。这对于临时隐藏所有子元素可能有用,但在大多数情况下会导致错误。推荐做法:在重写方法的开头调用super.dispatchDraw(canvas),然后添加您自己的图形(例如带有alpha通道的覆盖层)在子元素之上。
让我们创建一个OverlayViewGroup——一个自定义的ViewGroup,它在所有子View之上添加一个带有文本标签的半透明层。dispatchDraw首先调用super(绘制所有子元素),然后绘制覆盖层矩形和文本。Paint在构造函数中创建,以避免在draw循环中进行内存分配。
class OverlayViewGroup(context: Context)
: ViewGroup(context) {
private val overlayPaint = Paint().apply {
color = Color.parseColor("#66000000")
style = Paint.Style.FILL
}
private val textPaint = Paint().apply {
color = Color.WHITE
textSize = 36f
isAntiAlias = true
textAlign = Paint.Align.CENTER
}
init {
setWillNotDraw(false)
}
override fun dispatchDraw(canvas: Canvas) {
super.dispatchDraw(canvas)
canvas.drawRect(0f, 0f,
width.toFloat(),
height.toFloat(), overlayPaint)
canvas.drawText("预览模式",
width / 2f,
height / 2f, textPaint)
}
override fun onLayout(changed: Boolean,
l: Int, t: Int,
r: Int, b: Int) {
var top = t
for (i in 0 until childCount) {
val child = getChildAt(i)
val cw = child.measuredWidth
val ch = child.measuredHeight
child.layout(l, top, l + cw, top + ch)
top += ch
}
}
override fun onMeasure(widthMeasureSpec: Int,
heightMeasureSpec: Int) {
measureChildren(widthMeasureSpec, heightMeasureSpec)
val maxWidth = resolveSize(
getChildAt(0).measuredWidth,
widthMeasureSpec)
var totalHeight = 0
for (i in 0 until childCount) {
totalHeight += getChildAt(i).measuredHeight
}
setMeasuredDimension(maxWidth,
resolveSize(totalHeight, heightMeasureSpec))
}
}
下面的示例演示了子元素绘制顺序的更改。CircularRevealLayout重写dispatchDraw并以相反顺序绘制子元素,创建了反转的Z-order效果。为了实现动画,基于动画化的offset值添加了绘制索引的循环移位。
class ReverseOrderLayout(context: Context)
: ViewGroup(context) {
private var reverse = false
override fun dispatchDraw(canvas: Canvas) {
if (!reverse) {
super.dispatchDraw(canvas)
return
}
for (i in childCount - 1 downTo 0) {
val child = getChildAt(i)
if (child.visibility == GONE) continue
drawChild(canvas, child, getDrawingTime())
}
}
fun toggleReverse() {
reverse = !reverse
invalidate()
}
override fun onLayout(changed: Boolean,
l: Int, t: Int,
r: Int, b: Int) {
var left = l
for (i in 0 until childCount) {
val child = getChildAt(i)
val cw = child.measuredWidth
child.layout(left, t,
left + cw, t + child.measuredHeight)
left += cw
}
}
override fun onMeasure(widthMeasureSpec: Int,
heightMeasureSpec: Int) {
measureChildren(widthMeasureSpec, heightMeasureSpec)
var totalWidth = 0
for (i in 0 until childCount) {
totalWidth += getChildAt(i).measuredWidth
}
setMeasuredDimension(
resolveSize(totalWidth, widthMeasureSpec),
resolveSize(getChildAt(0).measuredHeight,
heightMeasureSpec))
}
}
dispatchDraw用于需要影响子View绘制过程而不影响ViewGroup自身内容的场景。主要场景:在所有子元素上应用公共效果(覆盖层、阴影、渐变);更改Z-order顺序以创建深度效果;通过Canvas变换实现子元素的出现或消失动画。
如果需要在子元素之下叠加图形(背景效果),应使用onDraw——dispatchDraw在其之后调用。如果图形应在子元素之上——使用dispatchDraw调用super,然后使用Canvas.draw。如果需要全局颜色或透明度更改,在dispatchDraw中使用canvas.saveLayerAlpha()包裹子元素的绘制会很方便。
不推荐将dispatchDraw用于:1)实时绘制复杂动画(使用invalidate和onDraw);2)创建ViewGroup的屏幕截图(使用buildDrawingCache()或View.draw(Canvas));3)修改子View(坐标、尺寸——这是onLayout的任务,不是dispatchDraw的)。dispatchDraw——仅用于应用视觉效果。
常见问题
不可以,dispatchDraw是protected方法,由Android系统在公共方法draw()内部调用。直接调用dispatchDraw没有意义,因为它不执行准备工作(保存Canvas、绘制背景和前景)。作为替代,使用View.draw(Canvas)在任意Canvas上进行程序化绘制。
如果在您自己的绘制操作之后调用super.dispatchDraw(canvas),子View将被绘制在自定义图形之上。这改变了图层的叠加顺序:先绘制自定义图层,然后绘制子元素。如果需要相反的情况(图形在子元素之上),先调用super.dispatchDraw,然后执行您自己的操作。
dispatchDraw为每个子View调用draw(),总时间与子元素数量成正比。在硬件加速下,在dispatchDraw中添加Canvas操作可能导致DisplayList失效和缓存重建。对于5-10个子元素影响最小,对于50+个子元素,建议缓存复杂的覆盖层。
View(非ViewGroup)没有子元素,因此dispatchDraw不执行有用的工作。然而,该方法存在于View基类中是由于多态性:draw()中的代码为任何View调用dispatchDraw,但View的实现不包含绘制子元素的逻辑。只有ViewGroup重写dispatchDraw。
可以,dispatchDraw接收一个Canvas,可以在调用super.dispatchDraw之前对其进行变换(平移、旋转、缩放)。这用于将整个子元素集合作为一个整体进行动画处理。在完成super.dispatchDraw后,建议将Canvas恢复为原始状态。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。