Android 提供了丰富的内置控件(TextView、Button、ImageView 等),但实际开发中经常会遇到「现有控件不够用」的场景:
这时候就需要 Custom View —— 自己绘制、自己处理交互。
每个 View 从创建到显示在屏幕上,会经历三个核心阶段:
measure(测量)→ layout(布局)→ draw(绘制)阶段 | 方法 | 作用 |
|---|---|---|
测量 | `onMeasure()` | 确定 View 的宽高 |
布局 | `onLayout()` | 确定子 View 的位置(ViewGroup 用) |
绘制 | `onDraw()` | 在 Canvas 上绘制内容 |
对于自定义单个 View,主要关注
onMeasure()和onDraw();onLayout()在自定义 ViewGroup 时才需要重写。
创建一个继承自 View 的类,重写 onDraw():
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 —— 在屏幕中央画一个蓝色圆。
View 的尺寸不是你想多大就多大,需要和父布局"协商"。这就是 onMeasure() 的作用。
模式 | 含义 | 场景 |
|---|---|---|
`EXACTLY` | 精确值 | 设置了具体 dp 值或 `match_parent` |
`AT_MOST` | 最大值 | `wrap_content` |
`UNSPECIFIED` | 不限制 | ScrollView 内嵌 View |
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.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)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.save() // 保存当前状态
canvas.translate(cx, cy) // 平移原点到中心
canvas.rotate(angle) // 旋转
canvas.drawArc(rectF, startAngle, sweepAngle, true, paint)
canvas.restore() // 恢复
save()和restore()要配对使用,避免影响后续绘制。
把上面的知识综合起来,画一个环形进度条:
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() // 触发重绘
}
}使用方法:
val progressBar = findViewById<CircleProgressBar>(R.id.circle_progress)
progressBar.setProgress(75f)Custom View 不仅可以画,还可以交互。重写 onTouchEvent():
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)
}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
}
}自定义绘制最容易成为性能瓶颈,注意以下几点:
// ❌ 错误:每次 onDraw 都会创建新对象
override fun onDraw(canvas: Canvas) {
val paint = Paint() // 每帧创建,GC 压力大
canvas.drawCircle(...)
}
// ✅ 正确:在 init 或成员变量中创建
private val paint = Paint().apply { ... }// ❌ 全量刷新
invalidate()
// ✅ 只刷新变化区域
invalidate(dirtyRect)Android 默认开启硬件加速,但有些 Canvas 操作不支持 GPU 渲染。遇到性能问题时检查:
// 关闭硬件加速(某些复杂绘制需要)
setLayerType(LAYER_TYPE_SOFTWARE, null)saveLayer() 会离屏渲染,开销较大。只在需要混合模式(PorterDuff)等效果时使用。
让 Custom View 支持 XML 配置:
<!-- 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>// 在 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 中使用:
<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 删除。