自定义视图
从头绘制自定义 UI 组件。扩展 View,使用 Canvas 和 Paint 实现 onMeasure 与 onDraw,定义自定义 XML 属性,并构建复合视图。
自定义视图 是 CoddyKit 上的免费 Android Academy 课时。 这是第 3 节课,共 5 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Android Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Android Academy 课程共包含 5 节课。
为什么需要自定义视图?
有时,Android 内置视图无法完全满足您的需求。自定义视图可以让您:
- 绘制 SDK 中不存在的形状、图表、进度环或统计图
- 创建可在整个应用中保持一致使用的品牌化界面组件
- 将多个视图组合成一个可复用的组件(组合视图)
- 通过消除嵌套的布局层级来提升性能
扩展 View
通过扩展 View(或类似 ImageView 的子类)创建自定义视图。重写所需的构造函数:
class CircleProgressView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
var progress: Float = 0f
set(value) {
field = value.coerceIn(0f, 100f)
invalidate() // trigger redraw
}
// onMeasure and onDraw go here
}onMeasure()
onMeasure() 会告知父视图此视图希望占用多大空间。最后始终调用 setMeasuredDimension():
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val desiredSize = 200 // dp → pixels
val px = (desiredSize * resources.displayMetrics.density).toInt()
val width = resolveSize(px, widthMeasureSpec)
val height = resolveSize(px, heightMeasureSpec)
// Square: same width and height
val size = minOf(width, height)
setMeasuredDimension(size, size)
}Paint — 绘图工具
Paint 对象保存绘图所需的样式信息(颜色、描边宽度和抗锯齿设置)。创建一次即可 — 切勿在 onDraw() 中创建:
class CircleProgressView ... : View(...) {
// Create Paint in init, not in onDraw!
private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.LTGRAY
style = Paint.Style.STROKE
strokeWidth = 20f
strokeCap = Paint.Cap.ROUND
}
private val progressPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.parseColor("#6750A4")
style = Paint.Style.STROKE
strokeWidth = 20f
strokeCap = Paint.Cap.ROUND
}
}onDraw() — 绘制视图
每当视图需要重绘时,都会调用 onDraw(canvas)。使用 Canvas 绘制形状、文本和位图:
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val cx = width / 2f
val cy = height / 2f
val radius = (minOf(width, height) / 2f) - 30f
// Draw background circle
canvas.drawCircle(cx, cy, radius, backgroundPaint)
// Draw progress arc
val oval = RectF(cx - radius, cy - radius, cx + radius, cy + radius)
val sweepAngle = 360f * (progress / 100f)
canvas.drawArc(oval, -90f, sweepAngle, false, progressPaint)
}invalidate() 与 requestLayout()
触发更新的两种方法:
invalidate()— 重绘视图(调用onDraw)。当内容发生变化但大小保持不变时使用。requestLayout()— 重新测量并重绘(先调用onMeasure,再调用onDraw)。当视图大小发生变化时使用。
自定义属性(attrs.xml)
声明自定义 XML 属性,以便从布局文件配置视图:
<!-- res/values/attrs.xml -->
<resources>
<declare-styleable name="CircleProgressView">
<attr name="progressColor" format="color" />
<attr name="trackColor" format="color" />
<attr name="strokeWidth" format="dimension" />
<attr name="progress" format="float" />
</declare-styleable>
</resources>读取自定义属性
在构造函数中使用 obtainStyledAttributes 读取属性:
init {
context.obtainStyledAttributes(attrs, R.styleable.CircleProgressView).use { ta ->
progressPaint.color = ta.getColor(
R.styleable.CircleProgressView_progressColor,
Color.parseColor("#6750A4")
)
backgroundPaint.color = ta.getColor(
R.styleable.CircleProgressView_trackColor,
Color.LTGRAY
)
val sw = ta.getDimension(R.styleable.CircleProgressView_strokeWidth, 20f)
progressPaint.strokeWidth = sw
backgroundPaint.strokeWidth = sw
progress = ta.getFloat(R.styleable.CircleProgressView_progress, 0f)
}
}在 XML 中使用自定义视图
在布局 XML 中使用完整类路径和自定义属性来使用自定义视图:
<!-- In any layout XML -->
<com.example.myapp.CircleProgressView
android:id="@+id/progressView"
android:layout_width="120dp"
android:layout_height="120dp"
app:progress="65"
app:progressColor="@color/primary"
app:trackColor="@color/surface_variant"
app:strokeWidth="12dp" />
<!-- Control in code: -->
binding.progressView.progress = 75f组合视图
组合视图 会将现有视图组合成一个可复用的组件。扩展类似 LinearLayout 的布局类:
class SearchBarView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : LinearLayout(context, attrs) {
private val binding = ViewSearchBarBinding.inflate(LayoutInflater.from(context), this)
init {
orientation = HORIZONTAL
binding.ivSearch.setOnClickListener {
onSearchClick?.invoke(binding.etSearch.text.toString())
}
}
var onSearchClick: ((String) -> Unit)? = null
fun setHint(hint: String) { binding.etSearch.hint = hint }
}绘制文本
使用 drawText() 在 Canvas 上绘制文本。使用 Paint.getFontMetrics() 精确地将文本居中:
private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.BLACK
textSize = 48f
textAlign = Paint.Align.CENTER
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val cx = width / 2f
val cy = height / 2f
// Vertically center the text:
val fm = textPaint.fontMetrics
val textY = cy - (fm.ascent + fm.descent) / 2f
canvas.drawText("${progress.toInt()}%", cx, textY, textPaint)
}快速检查
当数据发生变化但大小保持不变时,应该调用哪个方法来触发自定义视图重绘?
回顾:自定义视图
现在您可以在屏幕上绘制任何内容:
- 使用
@JvmOverloads constructor扩展View onMeasure()— 使用setMeasuredDimension()报告所需大小Paint— 定义样式;创建一次,不要在onDraw()中创建onDraw(canvas)— 绘制圆形、弧线、文本和位图- 使用
invalidate()重绘,使用requestLayout()重新测量 - 通过
attrs.xml使用自定义属性进行 XML 配置 - 组合视图 — 扩展布局以组合现有视图
下一步:使用动画和过渡让界面充满活力。
常见问题解答
「自定义视图」课时是免费的吗?
是的 — 「自定义视图」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Android Academy 课程的其余内容,请升级到 CoddyKit PRO。 Android Academy 课程共包含 5 节课。
「自定义视图」这节课中我会学到什么?
从头绘制自定义 UI 组件。扩展 View,使用 Canvas 和 Paint 实现 onMeasure 与 onDraw,定义自定义 XML 属性,并构建复合视图。 你通过在浏览器中直接运行的动手代码来练习 Android Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 Android Academy 需要有经验吗?
无需任何先前经验。CoddyKit 上的 Android Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 3 节课,共 5 节。
「自定义视图」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 Android Academy 课中编写并运行代码吗?
能。每节 Android Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。