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 环形进度图绘制——底层灰色轨道 + 渐变进度弧 + 端点圆点
#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
| 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 环形进度图的绘制:
下一篇文章将介绍 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
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!






