欢迎光临
我们一直在努力

49 匹配度圆环:MatchRing 环形进度图绘制

49 匹配度圆环:MatchRing 环形进度图绘制

前言

在这里插入图片描述

图:49 匹配度圆环:MatchRing 环形进度图绘制 运行效果截图(HarmonyOS NEXT)

环形进度图是一种简洁直观的数据可视化形式。在"鹿鹿·笔迹心理分析"App 的双人合盘页面中,MatchRing 组件以百分比圆环展示两人的匹配程度,配合品牌色渐变和端点圆点装饰,让匹配度一目了然。

本文以 [MatchRing.ets](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/common/components/MatchRing.ets) 为例,详细解析 Canvas 环形进度图的绘制。

鸿蒙官方·Canvas arc API:developer.huawei.com 项目源码:[MatchRing.ets](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/common/components/MatchRing.ets)

MatchRing 圆环绘制流程

图:MatchRing 环形进度图绘制——底层灰色轨道 + 渐变进度弧 + 端点圆点

#mermaid-svg-BOdXgUGXivf8yQha{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-BOdXgUGXivf8yQha .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-BOdXgUGXivf8yQha .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-BOdXgUGXivf8yQha .error-icon{fill:#552222;}#mermaid-svg-BOdXgUGXivf8yQha .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-BOdXgUGXivf8yQha .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-BOdXgUGXivf8yQha .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-BOdXgUGXivf8yQha .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-BOdXgUGXivf8yQha .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-BOdXgUGXivf8yQha .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-BOdXgUGXivf8yQha .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-BOdXgUGXivf8yQha .marker{fill:#333333;stroke:#333333;}#mermaid-svg-BOdXgUGXivf8yQha .marker.cross{stroke:#333333;}#mermaid-svg-BOdXgUGXivf8yQha svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-BOdXgUGXivf8yQha p{margin:0;}#mermaid-svg-BOdXgUGXivf8yQha .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-BOdXgUGXivf8yQha .cluster-label text{fill:#333;}#mermaid-svg-BOdXgUGXivf8yQha .cluster-label span{color:#333;}#mermaid-svg-BOdXgUGXivf8yQha .cluster-label span p{background-color:transparent;}#mermaid-svg-BOdXgUGXivf8yQha .label text,#mermaid-svg-BOdXgUGXivf8yQha span{fill:#333;color:#333;}#mermaid-svg-BOdXgUGXivf8yQha .node rect,#mermaid-svg-BOdXgUGXivf8yQha .node circle,#mermaid-svg-BOdXgUGXivf8yQha .node ellipse,#mermaid-svg-BOdXgUGXivf8yQha .node polygon,#mermaid-svg-BOdXgUGXivf8yQha .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-BOdXgUGXivf8yQha .rough-node .label text,#mermaid-svg-BOdXgUGXivf8yQha .node .label text,#mermaid-svg-BOdXgUGXivf8yQha .image-shape .label,#mermaid-svg-BOdXgUGXivf8yQha .icon-shape .label{text-anchor:middle;}#mermaid-svg-BOdXgUGXivf8yQha .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-BOdXgUGXivf8yQha .rough-node .label,#mermaid-svg-BOdXgUGXivf8yQha .node .label,#mermaid-svg-BOdXgUGXivf8yQha .image-shape .label,#mermaid-svg-BOdXgUGXivf8yQha .icon-shape .label{text-align:center;}#mermaid-svg-BOdXgUGXivf8yQha .node.clickable{cursor:pointer;}#mermaid-svg-BOdXgUGXivf8yQha .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-BOdXgUGXivf8yQha .arrowheadPath{fill:#333333;}#mermaid-svg-BOdXgUGXivf8yQha .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-BOdXgUGXivf8yQha .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-BOdXgUGXivf8yQha .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-BOdXgUGXivf8yQha .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-BOdXgUGXivf8yQha .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-BOdXgUGXivf8yQha .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-BOdXgUGXivf8yQha .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-BOdXgUGXivf8yQha .cluster text{fill:#333;}#mermaid-svg-BOdXgUGXivf8yQha .cluster span{color:#333;}#mermaid-svg-BOdXgUGXivf8yQha div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-BOdXgUGXivf8yQha .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-BOdXgUGXivf8yQha rect.text{fill:none;stroke-width:0;}#mermaid-svg-BOdXgUGXivf8yQha .icon-shape,#mermaid-svg-BOdXgUGXivf8yQha .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-BOdXgUGXivf8yQha .icon-shape p,#mermaid-svg-BOdXgUGXivf8yQha .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-BOdXgUGXivf8yQha .icon-shape .label rect,#mermaid-svg-BOdXgUGXivf8yQha .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-BOdXgUGXivf8yQha .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-BOdXgUGXivf8yQha .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-BOdXgUGXivf8yQha :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

matchScore 0-100

底层轨道灰色完整圆弧

计算进度角度score / 100 × 2π

渐变进度弧createLinearGradient + arc

端点装饰arc 小圆点

中心文字百分比数值

入场动画drawProgress 0→1

一、组件结构

1.1 接口定义

@Component
export struct MatchRing {
@Prop matchValue: number = 78 // 匹配度 0-100
@Prop ringSize: number = 200 // 画布尺寸
@Prop label: string = '匹配度' // 中心标签

private context: CanvasRenderingContext2D

@State @Watch('onDrawProgressChange') drawProgress: number = 0
}

1.2 最终效果

┌───────────────────────┐
│ ╭──────────╮ │
│ ╱ ╭──────╮ ╲ │
│ │ │ 78% │ │ │
│ ╲ ╰──────╯ ╱ │
│ ╰────●─────╯ │ ← 渐变弧 + 端点圆点
│ 匹配度 % │
└───────────────────────┘

二、背景轨道

2.1 完整圆环

// MatchRing.ets — 背景轨道绘制
const W = this.ringSize // 200
const cx = W / 2 // 100
const cy = W / 2 // 100
const radius = W * 0.4 // 80(画布的 40%)
const lineW = 14 // 环宽度

// 背景轨道(整圆,浅色描边)
ctx.beginPath()
ctx.arc(cx, cy, radius, 0, Math.PI * 2) // 从 0 到 2π 整圆
ctx.strokeStyle = AppColors.LINE // #E5D5BC
ctx.lineWidth = lineW
ctx.stroke()

2.2 尺寸参数

参数值说明
ringSize 200px 画布正方形尺寸
radius 80px(40%) 圆环半径
lineW 14px 圆环轨道宽度
cx/cy 100px 圆心位置

三、渐变进度弧

3.1 弧线绘制

// 起始角度:-π/2(12点钟方向)
const start = Math.PI / 2

// 结束角度:起始角度 + 匹配度对应的弧度
const end = start + (Math.PI * 2 * this.matchValue / 100) * this.drawProgress

// 创建渐变
const grad = ctx.createLinearGradient(0, 0, W, W) // 从左上到右下
grad.addColorStop(0, AppColors.PRIMARY_LIGHT) // 浅棕色
grad.addColorStop(1, AppColors.PRIMARY_DEEP) // 深棕色

// 绘制进度弧
ctx.beginPath()
ctx.arc(cx, cy, radius, start, end)
ctx.strokeStyle = grad
ctx.lineWidth = lineW
ctx.lineCap = 'round' // 圆头端点
ctx.stroke()

3.2 角度计算

// 匹配度 78% → 弧角度 = 2π × 78/100 ≈ 4.9 rad
// 整圆 2π = 360°
// 1% = 3.6°

// 起始点:-π/2(12 点钟)
// 终点:-π/2 + 2π × matchValue/100

matchValue | 弧角度 | 显示效果
0% | 0 | 只有起点圆点
25% | π/2 | 右下四分之一
50% | π | 下半圆
78% | 4.9 | 接近完整,留小缺口
100% | 2π | 闭合整圆

3.3 lineCap = ‘round’

lineCap 控制线端点的形状:

butt ──────── (默认) 平头端点,长度精确
round ●───────● 圆头端点,端点向外延伸半个线宽
square █───────█ 方头端点,端点向外延伸半个线宽

在环形进度图中使用 'round' 带来两个视觉效果:

  • 进度弧的起点:圆润自然,不突兀
  • 进度弧的终点:配合端点装饰圆点,视觉平滑
  • 四、端点装饰圆点

    4.1 圆点绘制

    // 仅当进度 >= 50% 时显示端点圆点(避免过早出现)
    if (this.drawProgress >= 0.5) {
    // 计算弧终点坐标
    const cosEnd = Math.cos(end)
    const sinEnd = Math.sin(end)
    const dotRadius = 5

    // 终点坐标 = 圆心 + 半径 × 方向
    const dx = cx + radius * cosEnd
    const dy = cy + radius * sinEnd

    // 绘制白色填充圆点 + 深色边框
    ctx.beginPath()
    ctx.arc(dx, dy, dotRadius, 0, Math.PI * 2)
    ctx.fillStyle = '#ffffff' // 白色填充
    ctx.fill()

    ctx.strokeStyle = AppColors.PRIMARY_DEEP // 深色边框
    ctx.lineWidth = 2
    ctx.stroke()
    }

    4.2 圆点位置计算

    终点在弧的结束位置,坐标公式:
    dx = cx + radius × cos(end)
    dy = cy + radius × sin(end)

    例如匹配度 78% 时(end ≈ -π/2 + 4.9):
    角度接近 4.9 rad ≈ 281°
    终点在左下方

    五、中心文本

    5.1 匹配度数字

    // 匹配度数值(大字,加粗)
    ctx.fillStyle = AppColors.TEXT
    ctx.font = 'bold 36px HarmonyOS Sans SC'
    ctx.textAlign = 'center'
    ctx.textBaseline = 'middle'
    ctx.fillText(`${this.matchValue}`, cx, cy 6) // 略微上移

    5.2 副标题

    // 百分比标签(小字)
    ctx.fillStyle = AppColors.TEXT_2
    ctx.font = '12px HarmonyOS Sans SC'
    ctx.fillText(`${this.label} %`, cx, cy + 22) // 在数字下方

    文字布局示意:

    ┌──────┐
    │ 78 │ ← bold 36px(中心,偏上 6px)
    │匹配度%│ ← 12px(中心,偏下 22px)
    └──────┘

    六、颜色系统

    6.1 渐变颜色

    // 渐变从左上角到右下角
    const grad = ctx.createLinearGradient(0, 0, W, W)
    grad.addColorStop(0, AppColors.PRIMARY_LIGHT) // #C9B09E
    grad.addColorStop(1, AppColors.PRIMARY_DEEP) // #8A7A6A

    颜色变量HEX在圆环中的位置
    PRIMARY_LIGHT #C9B09E 弧起点(较浅)
    PRIMARY_DEEP #8A7A6A 弧终点(较深)

    6.2 动态颜色(扩展方案)

    可以根据匹配度高低改变渐变颜色:

    function getRingGradient(ctx: CanvasRenderingContext2D, value: number, W: number): CanvasGradient {
    const grad = ctx.createLinearGradient(0, 0, W, W)
    if (value >= 80) {
    // 高匹配度 → 暖色
    grad.addColorStop(0, '#F4D5C5')
    grad.addColorStop(1, '#D9A689')
    } else if (value >= 60) {
    // 中等匹配度 → 品牌色(当前方案)
    grad.addColorStop(0, AppColors.PRIMARY_LIGHT)
    grad.addColorStop(1, AppColors.PRIMARY_DEEP)
    } else {
    // 低匹配度 → 冷色
    grad.addColorStop(0, '#C5D4E4')
    grad.addColorStop(1, '#5B7FB6')
    }
    return grad
    }

    七、动画实现

    7.1 展开动画

    aboutToAppear(): void {
    // 从 12 点方向顺时针展开进度弧
    animateTo({
    duration: AppAnimations.RING_DRAW_DURATION, // 1200ms
    curve: AppAnimations.CURVE_SMOOTH
    }, () => {
    this.drawProgress = 1
    })
    }

    onDrawProgressChange(): void {
    this.draw()
    }

    7.2 动画三个阶段

    阶段 1 (0-50%): 进度弧从 12 点方向开始顺时针展开
    端点圆点隐藏(drawProgress < 0.5)

    阶段 2 (50-99%): 进度弧继续展开
    端点圆点开始显示(drawProgress >= 0.5)

    阶段 3 (100%): 进度弧到位
    圆点 + 文字 + 渐变弧完整呈现

    八、完整绘制流程

    8.1 绘制顺序

    ① clearRect(0, 0, W, W) 清除画布
    ② 背景轨道(整圆 arc) → 底层灰色圆环
    ③ 渐变进度弧(部分 arc) → 上层彩色弧
    ④ 端点装饰圆点(arc + fill) → 弧终点的白色圆点
    ⑤ 中心数字(fillText) → 匹配度数值
    ⑥ 副标题(fillText) → "匹配度 %"

    8.2 在 DuetReportPage 中的使用

    // DuetReportPage.ets — 展示双方匹配度
    MatchRing({
    matchValue: this.matchScore, // 从数据库计算
    ringSize: 200,
    label: '匹配度'
    })

    九、MatchRing 参数调整

    9.1 尺寸比例

    // 不同场景下的 ringSize
    const sizes = {
    default: 200, // DuetReportPage 标准展示
    compact: 140, // 紧凑卡片
    large: 260 // 分享海报中心
    }

    9.2 线宽建议

    // 圆环线宽与尺寸的比例建议
    const recommendedLineWidth = Math.round(this.ringSize * 0.07)
    // ringSize=200 → lineW=14 ✅
    // ringSize=140 → lineW=10 ✅
    // ringSize=260 → lineW=18 ✅

    十、与其他环形进度方案的对比

    方案适用场景优点缺点
    Canvas arc(本项目) 自定义 UI 完全可控,动画丰富 需手写绘制逻辑
    ArkUI Circle 组件 简单环形 API 简单 不支持进度弧
    SVG circle Web 场景 可缩放 Canvas 不适用

    总结

    本文完整解析了 MatchRing 环形进度图的绘制:

  • 背景轨道:arc() 整圆 + 浅色描边
  • 渐变进度弧:arc(开始角度, 结束角度) + lineCap = 'round' + 线性渐变
  • 端点圆点:计算弧终点坐标 + 白色填充 + 深色边框
  • 中心文本:大字数字 + 小字标签,垂直居中
  • 动画控制:drawProgress 驱动弧从 -π/2 顺时针展开
  • 下一篇文章将介绍 Canvas 动画与绘制进度驱动——总结三个图表组件中的 @Watch + animateTo 动画模式。

    如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


    参考资源:

    • Canvas arc API
    • Canvas createLinearGradient
    • lineCap 端点样式
    • [MatchRing 组件源码](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/common/components/MatchRing.ets)
    • [DuetReportPage 合盘页源码](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/pages/DuetReportPage.ets)
    • [AppColors 颜色常量](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/common/theme/AppColors.ets)
    • [AppAnimations 动画常量](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/common/theme/AppAnimations.ets)
    • HarmonyOS 开发文档

    七、环形图高级技巧

    7.1 lineCap 圆形端点

    使用 lineCap = 'round' 可以让弧形的起点和终点变成圆形,视觉效果更柔和:

    ctx.lineCap = 'round' // 端点圆形
    ctx.lineWidth = ringWidth
    ctx.arc(cx, cy, radius, startAngle, endAngle)
    ctx.stroke()

    7.2 渐变圆弧

    使用线性渐变让圆弧产生颜色渐变效果:

    const gradient = ctx.createLinearGradient(cx radius, cy, cx + radius, cy)
    gradient.addColorStop(0, '#FF6B6B') // 起始色(玫红)
    gradient.addColorStop(0.5, '#FF9500') // 中间色(橙色)
    gradient.addColorStop(1, '#FFD700') // 结束色(金色)
    ctx.strokeStyle = gradient

    7.3 圆环宽度参数化

    将圆环宽度设计为相对于画布尺寸的比例,支持响应式缩放:

    const ringWidth = this.canvasSize * 0.08 // 圆环宽度为画布尺寸的 8%
    const radius = (this.canvasSize ringWidth) / 2 * 0.85

    如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!

    赞(0)
    未经允许不得转载:171主机测评 » 49 匹配度圆环:MatchRing 环形进度图绘制
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址