0Pricing
Android Academy · 课时

自定义视图

从头绘制自定义 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 反馈 — 无需本地设置。

此课程中的所有课时

  1. Material Design 基础
  2. 主题与样式
  3. 自定义视图
  4. 动画与过渡
  5. 底部导航与标签页
← 返回 Android Academy