Android 自定义 View 的测量与绘制

举报
yd_223002268 发表于 2026/09/14 08:54:00 2026/09/14
【摘要】 Android 自定义 View 的测量与绘制自定义 View 适合实现进度环、行情曲线、刻度盘等标准控件难以表达的视觉效果。它的难点不在于画出图形,而在于遵守测量协议、处理不同尺寸和密度、减少绘制阶段分配,并让状态变化以正确方式触发重绘。 一、明确 View 的职责自定义 View 应接收已经整理好的展示数据,只负责测量、绘制和局部交互。数据请求、业务计算和长期状态不应放在 View 内...

Android 自定义 View 的测量与绘制

自定义 View 适合实现进度环、行情曲线、刻度盘等标准控件难以表达的视觉效果。它的难点不在于画出图形,而在于遵守测量协议、处理不同尺寸和密度、减少绘制阶段分配,并让状态变化以正确方式触发重绘。

一、明确 View 的职责

自定义 View 应接收已经整理好的展示数据,只负责测量、绘制和局部交互。数据请求、业务计算和长期状态不应放在 View 内部。

data class ProgressViewState(
    val progress: Float,
    val label: String
)

fun setState(state: ProgressViewState) {
    currentState = state.copy(
        progress = state.progress.coerceIn(0f, 1f)
    )
    invalidate()
}

这里限制进度范围,是控件本身的输入契约。对于业务字段,不应在 View 中猜测错误值代表什么。

二、理解测量规格

父容器通过 MeasureSpec 把尺寸约束传给子视图:

  • EXACTLY:父容器已经确定尺寸。
  • AT_MOST:子视图可以自行决定,但不能超过上限。
  • UNSPECIFIED:父容器不提供明确限制。

自定义 View 如果不重写 onMeasure,在 wrap_content 下可能无法获得期望尺寸。可以提供基于内容的建议大小:

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    val desiredWidth = paddingLeft + contentDiameter + paddingRight
    val desiredHeight = paddingTop + contentDiameter + paddingBottom

    val measuredWidth = resolveSize(desiredWidth, widthMeasureSpec)
    val measuredHeight = resolveSize(desiredHeight, heightMeasureSpec)

    setMeasuredDimension(measuredWidth, measuredHeight)
}

resolveSize 能遵守父容器传入的约束,比直接设置固定像素更安全。即使父容器给出非正方形区域,内部圆形也可以按较短边计算,而不能通过测量结果违反父容器的精确尺寸要求。

三、统一处理密度

代码中的视觉尺寸不能直接使用裸像素。可以把默认描边、间距等 dp 值按屏幕密度转换:

private fun Float.toPx(): Float {
    return this * resources.displayMetrics.density
}

private val strokeWidthPx = 6f.toPx()
private val defaultDiameterPx = 120f.toPx().roundToInt()

文字绘制也应遵循项目统一字号适配规则。若项目规定字号使用 dp,就使用相同密度转换,不混入另一套缩放体系。

四、在尺寸变化时计算几何信息

绘制区域通常只在 View 尺寸改变时需要重新计算,可以放在 onSizeChanged 中:

private val arcBounds = RectF()

override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
    super.onSizeChanged(w, h, oldw, oldh)
    val availableWidth = w - paddingLeft - paddingRight
    val availableHeight = h - paddingTop - paddingBottom
    val diameter = minOf(availableWidth, availableHeight).toFloat()
    val contentLeft = paddingLeft + (availableWidth - diameter) / 2f
    val contentTop = paddingTop + (availableHeight - diameter) / 2f
    val halfStroke = strokeWidthPx / 2f
    arcBounds.set(
        contentLeft + halfStroke,
        contentTop + halfStroke,
        contentLeft + diameter - halfStroke,
        contentTop + diameter - halfStroke
    )
}

避免在每次 onDraw 中重新创建矩形、画笔和路径,可以减少滚动或动画过程中的垃圾回收压力。

五、保持 onDraw 轻量

override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)

    canvas.drawArc(arcBounds, -90f, 360f, false, backgroundPaint)
    canvas.drawArc(
        arcBounds,
        -90f,
        360f * currentState.progress,
        false,
        progressPaint
    )

    val centerX = width / 2f
    val baseline = height / 2f -
        (textPaint.ascent() + textPaint.descent()) / 2f
    canvas.drawText(currentState.label, centerX, baseline, textPaint)
}

绘制阶段不要进行数据库查询、图片解码或复杂集合处理。曲线点位等计算结果应在数据变化时提前准备。

六、区分 invalidate 与 requestLayout

invalidate 表示视觉内容改变,需要重新绘制,但尺寸不变。requestLayout 表示尺寸或布局关系可能改变,需要重新测量和布局。

进度值、颜色变化通常只需 invalidate;文字长度导致控件期望尺寸改变时,才需要 requestLayout。无条件同时调用两者会增加不必要的布局工作。

七、实现平滑动画

动画每帧更新展示值并触发重绘,不应每帧重建对象。

fun animateTo(target: Float) {
    progressAnimator?.cancel()
    progressAnimator = ValueAnimator.ofFloat(
        currentState.progress,
        target.coerceIn(0f, 1f)
    ).apply {
        addUpdateListener { animator ->
            currentState = currentState.copy(
                progress = animator.animatedValue as Float
            )
            invalidate()
        }
        start()
    }
}

override fun onDetachedFromWindow() {
    progressAnimator?.cancel()
    progressAnimator = null
    super.onDetachedFromWindow()
}

控件离开窗口时停止动画,避免无效刷新。动画时长和插值器应复用项目现有动效规范。

八、支持状态与可访问性

如果控件本身可点击,要实现 performClick。如果图形表达了进度或状态,还应提供可访问性文本,让读屏用户获得等价信息。

配置变化后的业务状态通常由页面模型重新传入。只有控件包含无法从外部恢复的临时交互状态时,才需要实现实例状态保存。

九、兼容性与测试

绘制 API、混合模式和硬件加速行为在不同系统版本可能存在差异。使用新能力前必须确认项目最低兼容版本,优先沿用已有兼容实现。

测试应覆盖:

  • wrap_content、精确尺寸和受限尺寸。
  • 不同密度与横竖屏。
  • 零进度、满进度和边界输入。
  • 长文本、空文本和多语言。
  • 列表快速复用及动画中途移除。
  • 深色主题与可访问性读取。

总结

稳定的自定义 View 需要正确遵守测量约束,在尺寸变化时缓存几何信息,并让 onDraw 只承担绘制。区分重绘与重新布局、及时停止动画,再补齐密度和可访问性处理,才能让控件既流畅又易于复用。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。