onDraw() — 是 Android SDK 中 View 类的关键方法,负责绘制视图的内容。系统会在每次 View 外观更新时调用 onDraw — 在初始渲染时、在 invalidate() 之后或在尺寸变化时。根据 Android Developers Documentation (2026),使用 Canvas API 重写 onDraw 是在 Android 应用程序中创建自定义图形的主要方式。
要点
onDraw(Canvas canvas) — 是 android.view.View 类的 protected 方法,当 View 需要被渲染时由 Android 系统调用。开发者重写此方法以实现自定义绘制:形状、文本、图像、渐变、动画。Canvas 参数是执行所有绘图操作的图形表面。
方法签名很简单:override fun onDraw(canvas: Canvas)。在此方法内部不能创建新对象(以避免 GC 负载)、执行长时间操作或与网络交互。View 在其生命周期中也包含 onDraw 方法,位于 onLayout(计算位置)和 dispatchDraw(绘制子元素)之间。
当继承自 View 时,重写 onDraw 是显示任何内容的必要条件 — 否则 View 将为空。当继承自现有子类(TextView、ImageView、Button)时,重写 onDraw 是可选的,用于在标准内容之上添加额外图形。
Android 按照严格定义的顺序执行 View 的绘制,该顺序从主线程(UI 线程)调用。完整的循环包括三个阶段:measure(测量尺寸)、layout(计算位置)和 draw(绘制)。onDraw 是 draw 阶段的一部分,在 View 的尺寸和位置确定后被调用。
draw(Canvas canvas) — 是 View 的公共方法,调用 onDraw 和 dispatchDraw。draw 方法不用于重写 — 取而代之的是重写 onDraw。在为当前 View 执行 onDraw 之后,会调用 dispatchDraw,它会递归地为所有子 View 调用 draw(如果当前 View 是 ViewGroup 容器)。
onDraw 在以下条件下被调用:1) View 首次变为可见(在 setVisibility(VISIBLE) 之后);2) 应用程序为此 View 或其父级调用 invalidate();3) View 的尺寸发生变化(新的 layout);4) 与 View 关联的动画发生变化(通过 ValueAnimator 或 ObjectAnimator);5) View 的状态发生变化(pressed、focused、enabled)。
从 Android 3.0 (API 11) 开始,大多数 Canvas 操作通过 GPU 进行硬件加速。开发者可以通过 canvas.isHardwareAccelerated() 检查加速是否启用。在硬件加速下,某些 Canvas 操作不受支持(例如,clipPath 与非矩形区域)并且可能被忽略或导致错误。
Canvas 提供了丰富的绘制方法集。所有方法都在当前坐标系中工作,该坐标系可以通过变换(translate、rotate、scale、skew)进行修改。每个方法接受一个 Paint 对象,该对象定义颜色、样式、线条粗细、效果和抗锯齿。
| Canvas 方法 | 用途 | Paint 关键参数 |
|---|---|---|
| drawCircle | 绘制圆形 | centerX, centerY, radius, paint |
| drawRect | 绘制矩形 | left, top, right, bottom, paint |
| drawLine | 绘制线条 | startX, startY, endX, endY, paint |
| drawPath | 绘制任意轮廓 | path, paint |
| drawBitmap | 绘制图像 | bitmap, srcRect, dstRect, paint |
| drawText | 绘制文本 | text, x, y, paint |
| drawArc | 绘制弧线或扇形 | ovalRect, startAngle, sweepAngle, useCenter, paint |
| drawOval | 绘制椭圆 | ovalRect, paint |
Paint 定义绘制样式:颜色 (setColor)、线条粗细 (setStrokeWidth)、填充样式 (setStyle — FILL、STROKE、FILL_AND_STROKE)、抗锯齿 (setAntiAlias)、效果 (setShadowLayer、setMaskFilter) 和渐变 (setShader)。建议在 View 的构造函数中创建和配置 Paint 一次,而不是在 onDraw 内部,以避免在绘制循环中创建对象。
Canvas 使用坐标系统,其中 X 轴向右,Y 轴向下。坐标原点 (0, 0) 是 View 的左上角。在调用 draw 方法之前,开发者可以通过 canvas.translate(dx, dy)、canvas.rotate(degrees, px, py)、canvas.scale(sx, sy, px, py) 更改坐标系统。所有后续的绘图操作都将在变换后的系统中执行。
onDraw 在主 UI 线程中执行,其持续时间直接影响帧率 (fps)。为了维持 60 fps,onDraw 方法必须在不超过 16 毫秒内完成。任何减速都会导致丢帧 (jank),在视觉上表现为动画卡顿或滚动延迟。
基本规则 onDraw 优化包括:1) 不要在 onDraw 内部创建对象 — 所有 Paint、Path、Rect 都应该是类的字段;2) 不要分配内存 (new) — 这会触发垃圾回收 (GC) 和丢帧;3) 通过 Bitmap.createBitmap 将复杂操作缓存到 Bitmap 中并绘制准备好的 Bitmap;4) 不要执行输入输出操作或加载资源。
clipRect 将绘制区域限制为指定的矩形。用于避免 Canvas 浪费时间渲染可见区域外的像素。在滚动大列表或缩放时特别有效:将 clipRect 设置为可见区域(脏矩形),Canvas 将只绘制可见的内容。
canvas.save() 将 Canvas 的当前状态(变换和裁剪)保存到堆栈中。canvas.restore() 恢复最后保存的状态。这对于临时变换是必需的:保存状态,应用 translate/rotate,绘制,然后恢复。save 和 restore 很轻量,在合理使用下(少于 10–15 层嵌套)不会影响性能。
invalidate() — 是 View 的方法,通知系统 View 的内容已过时,需要重新绘制。调用 invalidate 会启动一个新的 draw 循环,在此循环中将调用 onDraw。invalidate 只能从 UI 线程(主线程)调用。从后台线程调用时使用 postInvalidate。
postInvalidate() — invalidate 的线程安全版本,通过 Handler 将重绘请求发送到主线程队列。在后台任务中,当需要在数据加载或计算完成后更新 View 时使用。与 runOnUiThread 结合使用以与 UI 同步。
对于部分重绘,使用 invalidate(Rect dirty) 或 invalidate(int l, int t, int r, int b)。这向系统指示只有指定区域发生了变化,而不是整个 View。Android 通过仅重绘脏矩形来优化渲染,这显著提高了部分更新动画的性能。
让我们来看一下在 Android 中使用 Kotlin 重写 onDraw 创建自定义 View 的实际示例。
示例创建了一个带有动画填充的圆形进度指示器。onDraw 绘制两个圆:灰色背景和彩色填充,其弧线由当前进度决定。在 setProgress 中调用 invalidate 确保值更改时重新绘制。
class CircularProgressView
@JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : View(context, attrs) {
private val bgPaint = Paint().apply {
style = Paint.Style.STROKE
strokeWidth = 12f
color = Color.parseColor("#E0E0E0")
isAntiAlias = true
}
private val progressPaint = Paint().apply {
style = Paint.Style.STROKE
strokeWidth = 12f
color = Color.BLUE
isAntiAlias = true
strokeCap = Paint.Cap.ROUND
}
var progress: Float = 0f
set(value) {
field = value.coerceIn(0f, 1f)
invalidate()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val size = minOf(width, height) - 24f
val oval = RectF(12f, 12f,
12f + size, 12f + size)
canvas.drawArc(oval, 270f, 360f, false, bgPaint)
canvas.drawArc(oval, 270f,
progress * 360f,
false, progressPaint)
}
}
示例绘制了一个带有渐变的矩形和其上的文本。渐变通过 LinearGradient(Shader)创建,文本通过 drawText 创建。两个 Paint 都在构造函数中创建一次,在 onDraw 中仅根据 View 的当前宽度更改矩形的大小。
class GradientTextView(context: Context)
: View(context) {
private val shaderPaint = Paint().apply {
isAntiAlias = true
}
private val textPaint = Paint().apply {
color = Color.WHITE
textSize = 48f
isAntiAlias = true
textAlign = Paint.Align.CENTER
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val gradient = LinearGradient(
0f, 0f, width.toFloat(), 0f,
Color.parseColor("#FF6B35"),
Color.parseColor("#FF0040"),
Shader.TileMode.CLAMP
)
shaderPaint.shader = gradient
canvas.drawRect(0f, 0f,
width.toFloat(),
height.toFloat(), shaderPaint)
val xCenter = width / 2f
val yCenter = height / 2f
val yOffset = (textPaint.descent() +
textPaint.ascent()) / 2f
canvas.drawText("来自 Canvas 的问候",
xCenter, yCenter - yOffset,
textPaint)
}
}
示例创建了一个动画时钟,每 1000 毫秒更新一次绘制。onDraw 中的 postDelayed 在一秒后调用 invalidate,创建了一个无限的重绘循环。时钟指针通过 drawLine 以不同的长度和粗细绘制。
class AnalogClockView(context: Context)
: View(context) {
private val handPaint = Paint().apply {
style = Paint.Style.STROKE
isAntiAlias = true
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val cx = width / 2f
val cy = height / 2f
val radius = minOf(cx, cy) - 20f
val now = Calendar.getInstance()
val hours = now.get(Calendar.HOUR) % 12
val minutes = now.get(Calendar.MINUTE)
val seconds = now.get(Calendar.SECOND)
val secondAngle = Math.toRadians(
seconds * 6f - 90f)
handPaint.strokeWidth = 2f
handPaint.color = Color.RED
canvas.drawLine(cx, cy,
cx + cos(secondAngle) * radius * 0.8f,
cy + sin(secondAngle) * radius * 0.8f,
handPaint)
postDelayed({ invalidate() }, 1000)
}
}
常见问题
不可以,不应直接调用 onDraw。要请求重绘,请使用 invalidate()(从 UI 线程)或 postInvalidate()(从后台线程)。系统自行决定何时调用 onDraw,并通过将多个 invalidate 合并为一次传递来优化渲染。
原因:View 被隐藏(setVisibility(GONE) 或 INVISIBLE);View 未添加到 Window 层次结构中;View 的尺寸为 0(width 或 height = 0);调用了 setWillNotDraw(true)(ViewGroup 默认为 true)。调试时检查这些要点。
在 60 fps 下,onDraw 每秒最多可被调用 60 次。在 120 fps 下(在具有 120 Hz 显示屏的设备上)— 最多 120 次。每次调用应在 ~8–16 毫秒内完成。在通过 ObjectAnimator 或 ValueAnimator 进行的活跃动画中,频率由动画帧数决定。
onDraw 负责绘制 View 自身的内容。dispatchDraw 负责绘制子 View(在 ViewGroup 中)。dispatchDraw 在 onDraw 之后调用,并递归遍历所有子元素。对于普通 View(不是 ViewGroup),dispatchDraw 为空。
可以,可以通过 Canvas(bitmap) 为 Bitmap 创建 Canvas 并在任何线程上绘制。这用于在后台预计算复杂图形。但是,在 onDraw 中传递的 Canvas 属于系统,只能在 UI 线程中的 onDraw 方法内使用。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。