onDraw() — 是什么、调用机制以及方法重写

作者: IT Sectr 发布日期: 2026-07-22 阅读时间: 10 分钟

onDraw() — 是 Android SDK 中 View 类的关键方法,负责绘制视图的内容。系统会在每次 View 外观更新时调用 onDraw — 在初始渲染时、在 invalidate() 之后或在尺寸变化时。根据 Android Developers Documentation (2026),使用 Canvas API 重写 onDraw 是在 Android 应用程序中创建自定义图形的主要方式。

要点

  • onDraw(Canvas) — 系统调用的 View 方法,用于在 Canvas 上绘制内容
  • invalidate() — 请求重新绘制 View,导致在下一帧循环中调用 onDraw
  • Canvas API 提供了 drawCircle、drawBitmap、drawText、drawPath 等绘制方法
  • Android 系统在 UI 线程中执行绘制,因此 onDraw 必须轻量且快速
  • Hardware acceleration 从 Android 3.0 (API 11) 开始自动通过 GPU 加速绘制

什么是 onDraw()?

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 是可选的,用于在标准内容之上添加额外图形。

View 的绘制循环

Android 按照严格定义的顺序执行 View 的绘制,该顺序从主线程(UI 线程)调用。完整的循环包括三个阶段:measure(测量尺寸)、layout(计算位置)和 draw(绘制)。onDraw 是 draw 阶段的一部分,在 View 的尺寸和位置确定后被调用。

Draw 阶段:dispatchDraw 和 onDraw

draw(Canvas canvas) — 是 View 的公共方法,调用 onDraw 和 dispatchDraw。draw 方法不用于重写 — 取而代之的是重写 onDraw。在为当前 View 执行 onDraw 之后,会调用 dispatchDraw,它会递归地为所有子 View 调用 draw(如果当前 View 是 ViewGroup 容器)。

系统何时调用 onDraw

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 API

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 对象

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 性能

onDraw 在主 UI 线程中执行,其持续时间直接影响帧率 (fps)。为了维持 60 fps,onDraw 方法必须在不超过 16 毫秒内完成。任何减速都会导致丢帧 (jank),在视觉上表现为动画卡顿或滚动延迟。

高性能 onDraw 的规则

基本规则 onDraw 优化包括:1) 不要在 onDraw 内部创建对象 — 所有 Paint、Path、Rect 都应该是类的字段;2) 不要分配内存 (new) — 这会触发垃圾回收 (GC) 和丢帧;3) 通过 Bitmap.createBitmap 将复杂操作缓存到 Bitmap 中并绘制准备好的 Bitmap;4) 不要执行输入输出操作或加载资源。

使用 clipRect 进行优化

clipRect 将绘制区域限制为指定的矩形。用于避免 Canvas 浪费时间渲染可见区域外的像素。在滚动大列表或缩放时特别有效:将 clipRect 设置为可见区域(脏矩形),Canvas 将只绘制可见的内容。

Layer — 保存和恢复

canvas.save() 将 Canvas 的当前状态(变换和裁剪)保存到堆栈中。canvas.restore() 恢复最后保存的状态。这对于临时变换是必需的:保存状态,应用 translate/rotate,绘制,然后恢复。save 和 restore 很轻量,在合理使用下(少于 10–15 层嵌套)不会影响性能。

invalidate 和 postInvalidate

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 通过仅重绘脏矩形来优化渲染,这显著提高了部分更新动画的性能。

Kotlin 代码示例

让我们来看一下在 Android 中使用 Kotlin 重写 onDraw 创建自定义 View 的实际示例

自定义 View — 圆形进度条

示例创建了一个带有动画填充的圆形进度指示器。onDraw 绘制两个圆:灰色背景和彩色填充,其弧线由当前进度决定。在 setProgress 中调用 invalidate 确保值更改时重新绘制。

kotlin
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)
    }
}

自定义 View — 带文本的渐变背景

示例绘制了一个带有渐变的矩形和其上的文本。渐变通过 LinearGradient(Shader)创建,文本通过 drawText 创建。两个 Paint 都在构造函数中创建一次,在 onDraw 中仅根据 View 的当前宽度更改矩形的大小。

kotlin
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)
    }
}

使用 invalidate 的动画时钟

示例创建了一个动画时钟,每 1000 毫秒更新一次绘制。onDraw 中的 postDelayed 在一秒后调用 invalidate,创建了一个无限的重绘循环。时钟指针通过 drawLine 以不同的长度和粗细绘制。

kotlin
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 吗?

不可以,不应直接调用 onDraw。要请求重绘,请使用 invalidate()(从 UI 线程)或 postInvalidate()(从后台线程)。系统自行决定何时调用 onDraw,并通过将多个 invalidate 合并为一次传递来优化渲染。

为什么 invalidate 后 onDraw 没有被调用?

原因:View 被隐藏(setVisibility(GONE) 或 INVISIBLE);View 未添加到 Window 层次结构中;View 的尺寸为 0(width 或 height = 0);调用了 setWillNotDraw(true)(ViewGroup 默认为 true)。调试时检查这些要点。

onDraw 可以被调用多频繁?

在 60 fps 下,onDraw 每秒最多可被调用 60 次。在 120 fps 下(在具有 120 Hz 显示屏的设备上)— 最多 120 次。每次调用应在 ~8–16 毫秒内完成。在通过 ObjectAnimator 或 ValueAnimator 进行的活跃动画中,频率由动画帧数决定。

onDraw 和 dispatchDraw 有什么区别?

onDraw 负责绘制 View 自身的内容。dispatchDraw 负责绘制子 View(在 ViewGroup 中)。dispatchDraw 在 onDraw 之后调用,并递归遍历所有子元素。对于普通 View(不是 ViewGroup),dispatchDraw 为空。

可以在 onDraw 之外使用 Canvas 吗?

可以,可以通过 Canvas(bitmap) 为 Bitmap 创建 Canvas 并在任何线程上绘制。这用于在后台预计算复杂图形。但是,在 onDraw 中传递的 Canvas 属于系统,只能在 UI 线程中的 onDraw 方法内使用。

总结

  • onDraw(Canvas) — 在 Android 中使用 Canvas API 进行自定义绘制的关键 View 方法
  • 绘制循环 由 measure、layout 和 draw 阶段组成 — onDraw 在 draw 阶段被调用
  • Canvas API 包括 drawCircle、drawRect、drawPath、drawBitmap、drawText 等方法
  • invalidate() 从 UI 线程请求重绘,postInvalidate() 从后台线程请求重绘
  • 部分重绘 通过 invalidate(Rect) 优化渲染,仅更新脏区域
  • onDraw 性能 至关重要:在方法内部创建对象会导致 GC 和丢帧
  • 硬件加速(从 API 11 起)通过 GPU 自动加速 Canvas 操作

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目