首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >[Android 从零到一] Custom View 自定义绘制:从 onDraw 到完整交互

[Android 从零到一] Custom View 自定义绘制:从 onDraw 到完整交互

原创
作者头像
hunter android
发布2026-07-01 10:12:05
发布2026-07-01 10:12:05
930
举报

为什么需要 Custom View?

Android 提供了丰富的内置控件(TextView、Button、ImageView 等),但实际开发中经常会遇到「现有控件不够用」的场景:

  • 需要一个环形进度条
  • 需要一个自定义的图表
  • 需要一个特殊形状的按钮
  • 需要一个手势识别的画板

这时候就需要 Custom View —— 自己绘制、自己处理交互。

View 的绘制流程

每个 View 从创建到显示在屏幕上,会经历三个核心阶段:

代码语言:javascript
复制
measure(测量)→ layout(布局)→ draw(绘制)

阶段

方法

作用

测量

`onMeasure()`

确定 View 的宽高

布局

`onLayout()`

确定子 View 的位置(ViewGroup 用)

绘制

`onDraw()`

在 Canvas 上绘制内容

对于自定义单个 View,主要关注 onMeasure()onDraw()onLayout() 在自定义 ViewGroup 时才需要重写。

第一步:最简单的 Custom View

创建一个继承自 View 的类,重写 onDraw()

代码语言:javascript
复制
class SimpleView(context: Context, attrs: AttributeSet?) : View(context, attrs) {



    private val paint = Paint().apply {

        color = Color.BLUE

        style = Paint.Style.FILL

        isAntiAlias = true

    }



    override fun onDraw(canvas: Canvas) {

        super.onDraw(canvas)

        // 画一个蓝色圆形

        canvas.drawCircle(width / 2f, height / 2f, 100f, paint)

    }

}

这就完成了一个最简单的自定义 View —— 在屏幕中央画一个蓝色圆。

第二步:理解 onMeasure

View 的尺寸不是你想多大就多大,需要和父布局"协商"。这就是 onMeasure() 的作用。

MeasureSpec 三种模式

模式

含义

场景

`EXACTLY`

精确值

设置了具体 dp 值或 `match_parent`

`AT_MOST`

最大值

`wrap_content`

`UNSPECIFIED`

不限制

ScrollView 内嵌 View

重写 onMeasure

代码语言:javascript
复制
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {

    val widthMode = MeasureSpec.getMode(widthMeasureSpec)

    val widthSize = MeasureSpec.getSize(widthMeasureSpec)

    val heightMode = MeasureSpec.getMode(heightMeasureSpec)

    val heightSize = MeasureSpec.getSize(heightMeasureSpec)



    // 默认尺寸

    val desiredWidth = 200

    val desiredHeight = 200



    val width = when (widthMode) {

        MeasureSpec.EXACTLY -> widthSize

        MeasureSpec.AT_MOST -> minOf(desiredWidth, widthSize)

        else -> desiredWidth

    }



    val height = when (heightMode) {

        MeasureSpec.EXACTLY -> heightSize

        MeasureSpec.AT_MOST -> minOf(desiredHeight, heightSize)

        else -> desiredHeight

    }



    setMeasuredDimension(width, height)

}

⚠️ 如果不重写 onMeasure(),使用 wrap_content 时会占满父布局,表现得和 match_parent 一样。

第三步:Canvas 与 Paint

自定义绘制的核心就是两个类:Canvas(画布)和 Paint(画笔)。

Canvas 常用绘制方法

代码语言:javascript
复制
// 画矩形

canvas.drawRect(left, top, right, bottom, paint)



// 画圆角矩形

canvas.drawRoundRect(rect, rx, ry, paint)



// 画文字

canvas.drawText("Hello", x, y, paint)



// 画路径(最灵活)

val path = Path().apply {

    moveTo(100f, 100f)

    lineTo(200f, 200f)

    lineTo(100f, 200f)

    close()

}

canvas.drawPath(path, paint)



// 画 Bitmap

canvas.drawBitmap(bitmap, left, top, paint)

Paint 常用属性

代码语言:javascript
复制
val paint = Paint().apply {

    color = Color.RED              // 颜色

    style = Paint.Style.STROKE     // 描边 / FILL 填充 / FILL_AND_STROKE

    strokeWidth = 4f               // 描边宽度

    isAntiAlias = true             // 抗锯齿(必开)

    textSize = 36f                 // 文字大小

    textAlign = Paint.Align.CENTER // 文字对齐

    setShadowLayer(8f, 0f, 0f, Color.GRAY) // 阴影

}

Canvas 变换操作

Canvas 还支持平移、旋转、缩放、裁剪,用来实现复杂效果:

代码语言:javascript
复制
canvas.save()                     // 保存当前状态

canvas.translate(cx, cy)          // 平移原点到中心

canvas.rotate(angle)              // 旋转

canvas.drawArc(rectF, startAngle, sweepAngle, true, paint)

canvas.restore()                  // 恢复

save()restore() 要配对使用,避免影响后续绘制。

实战:环形进度条

把上面的知识综合起来,画一个环形进度条:

代码语言:javascript
复制
class CircleProgressBar(context: Context, attrs: AttributeSet?) : View(context, attrs) {



    private var progress: Float = 0f  // 0~100



    private val bgPaint = Paint().apply {

        color = Color.LTGRAY

        style = Paint.Style.STROKE

        strokeWidth = 20f

        isAntiAlias = true

        strokeCap = Paint.Cap.ROUND

    }



    private val progressPaint = Paint().apply {

        color = Color.parseColor("#4CAF50")

        style = Paint.Style.STROKE

        strokeWidth = 20f

        isAntiAlias = true

        strokeCap = Paint.Cap.ROUND

    }



    private val textPaint = Paint().apply {

        color = Color.DKGRAY

        textSize = 48f

        textAlign = Paint.Align.CENTER

        isAntiAlias = true

    }



    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {

        val size = 200

        val w = resolveSize(size, widthMeasureSpec)

        val h = resolveSize(size, heightMeasureSpec)

        val min = minOf(w, h)

        setMeasuredDimension(min, min)

    }



    override fun onDraw(canvas: Canvas) {

        super.onDraw(canvas)



        val cx = width / 2f

        val cy = height / 2f

        val radius = minOf(cx, cy) - 30f

        val rectF = RectF(cx - radius, cy - radius, cx + radius, cy + radius)



        // 背景圆环

        canvas.drawArc(rectF, 0f, 360f, false, bgPaint)



        // 进度弧

        val sweepAngle = 360f * (progress / 100f)

        canvas.drawArc(rectF, -90f, sweepAngle, false, progressPaint)



        // 文字

        val text = "${progress.toInt()}%"

        val textY = cy - (textPaint.descent() + textPaint.ascent()) / 2f

        canvas.drawText(text, cx, textY, textPaint)

    }



    fun setProgress(value: Float) {

        progress = value.coerceIn(0f, 100f)

        invalidate() // 触发重绘

    }

}

使用方法:

代码语言:javascript
复制
val progressBar = findViewById<CircleProgressBar>(R.id.circle_progress)

progressBar.setProgress(75f)

第四步:处理触摸事件

Custom View 不仅可以画,还可以交互。重写 onTouchEvent()

代码语言:javascript
复制
override fun onTouchEvent(event: MotionEvent): Boolean {

    when (event.action) {

        MotionEvent.ACTION_DOWN -> {

            // 手指按下

            return true  // 返回 true 表示消费此事件

        }

        MotionEvent.ACTION_MOVE -> {

            // 手指移动

            invalidate() // 根据触摸位置重绘

        }

        MotionEvent.ACTION_UP -> {

            // 手指抬起

        }

    }

    return super.onTouchEvent(event)

}

实战:简单画板

代码语言:javascript
复制
class DrawingBoard(context: Context, attrs: AttributeSet?) : View(context, attrs) {



    private val path = Path()

    private val paint = Paint().apply {

        color = Color.BLACK

        style = Paint.Style.STROKE

        strokeWidth = 5f

        isAntiAlias = true

        strokeJoin = Paint.Join.ROUND

    }



    override fun onDraw(canvas: Canvas) {

        super.onDraw(canvas)

        canvas.drawPath(path, paint)

    }



    override fun onTouchEvent(event: MotionEvent): Boolean {

        when (event.action) {

            MotionEvent.ACTION_DOWN -> {

                path.moveTo(event.x, event.y)

                return true

            }

            MotionEvent.ACTION_MOVE -> {

                path.lineTo(event.x, event.y)

                invalidate()

            }

        }

        return true

    }

}

性能优化

自定义绘制最容易成为性能瓶颈,注意以下几点:

1. 避免在 onDraw 中创建对象

代码语言:javascript
复制
// ❌ 错误:每次 onDraw 都会创建新对象

override fun onDraw(canvas: Canvas) {

    val paint = Paint() // 每帧创建,GC 压力大

    canvas.drawCircle(...)

}



// ✅ 正确:在 init 或成员变量中创建

private val paint = Paint().apply { ... }

2. 局部刷新

代码语言:javascript
复制
// ❌ 全量刷新

invalidate()



// ✅ 只刷新变化区域

invalidate(dirtyRect)

3. 使用硬件加速

Android 默认开启硬件加速,但有些 Canvas 操作不支持 GPU 渲染。遇到性能问题时检查:

代码语言:javascript
复制
// 关闭硬件加速(某些复杂绘制需要)

setLayerType(LAYER_TYPE_SOFTWARE, null)

4. 善用 Canvas.saveLayer

saveLayer() 会离屏渲染,开销较大。只在需要混合模式(PorterDuff)等效果时使用。

自定义属性

让 Custom View 支持 XML 配置:

代码语言:javascript
复制
<!-- res/values/attrs.xml -->

<declare-styleable name="CircleProgressBar">

    <attr name="cpb_progress" format="float" />

    <attr name="cpb_bg_color" format="color" />

    <attr name="cpb_progress_color" format="color" />

</declare-styleable>
代码语言:javascript
复制
// 在 init 中读取属性

init {

    context.obtainStyledAttributes(attrs, R.styleable.CircleProgressBar).apply {

        progress = getFloat(R.styleable.CircleProgressBar_cpb_progress, 0f)

        bgPaint.color = getColor(R.styleable.CircleProgressBar_cpb_bg_color, Color.LTGRAY)

        progressPaint.color = getColor(R.styleable.CircleProgressBar_cpb_progress_color, Color.GREEN)

        recycle()

    }

}

XML 中使用:

代码语言:javascript
复制
<com.example.CircleProgressBar

    android:layout_width="200dp"

    android:layout_height="200dp"

    app:cpb_progress="60"

    app:cpb_progress_color="#FF5722" />

总结

Custom View 的核心就三步:

步骤

要点

**onMeasure**

正确处理 wrap_content,调用 `setMeasuredDimension()`

**onDraw**

用 Canvas + Paint 绘制,避免创建对象

**onTouchEvent**

处理交互,返回 true 表示消费事件

掌握这三个方法,再加上 Canvas 的各种绘制 API,几乎可以实现任何 UI 效果。Custom View 是 Android 开发中从"会用控件"到"理解控件"的关键一步。


系列导航: 上一篇:Hilt 依赖注入 | 下一篇:ContentProvider 与跨进程通信

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 为什么需要 Custom View?
  • View 的绘制流程
  • 第一步:最简单的 Custom View
  • 第二步:理解 onMeasure
    • MeasureSpec 三种模式
    • 重写 onMeasure
  • 第三步:Canvas 与 Paint
    • Canvas 常用绘制方法
    • Paint 常用属性
    • Canvas 变换操作
  • 实战:环形进度条
  • 第四步:处理触摸事件
    • 实战:简单画板
  • 性能优化
    • 1. 避免在 onDraw 中创建对象
    • 2. 局部刷新
    • 3. 使用硬件加速
    • 4. 善用 Canvas.saveLayer
  • 自定义属性
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档