鸿蒙原生应用开发进阶:使用 ArkTS Canvas 绘制复杂几何图形与动态数据图表
全文通过20+关键代码片段,系统讲解了从基础几何图形到复杂数据可视化的实现方案,涵盖坐标系转换、路径绘制、状态管理、递归算法等核心技术,为鸿蒙生态的高性能自定义UI开发提供实践指导。

文章目录
前言
在现代大前端开发与移动端开发中,数据可视化和复杂的自定义 UI 渲染一直是衡量开发者技术深度的重要标准之一。在华为的 HarmonyOS (鸿蒙) 系统中,ArkTS 为我们提供了非常强大的原生 Canvas 组件。
虽然市面上有很多现成的图表库(如 ECharts),但在某些特定场景下(例如极致的性能要求、极度定制化的交互UI、物联网设备的数据看板等),我们需要直接操作底层 API 来进行图形渲染。
本文将基于一份非常经典的 ArkTS Canvas 源码,带大家从零开始剖析如何利用 CanvasRenderingContext2D 绘制复杂的数学几何图形(如分形树、贝塞尔花瓣、太极图)以及企业级动态数据图表(如柱状图、折线图、饼图、雷达图等)。文章字数与干货拉满,建议先收藏再阅读!
一、 核心基础:Canvas 画布的环境初始化
在 ArkTS 中使用 Canvas,我们需要先准备好绘图上下文。这与 Web 前端的 HTML5 Canvas 非常类似,但写法上更贴合声明式 UI 的特点。
1.1 声明渲染上下文
我们需要实例化 RenderingContextSettings 和 CanvasRenderingContext2D。在这段代码中,作者创建了两个上下文,分别用于绘制静态几何图形和动态图表。
@Entry
@Component
struct Index {
// 1. 开启抗锯齿设置,保证图形边缘平滑
private settings: RenderingContextSettings = new RenderingContextSettings(true)
// 2. 静态几何图形的上下文
private ctx: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)
// 3. 动态图表的上下文
private chartCtx: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)
aboutToAppear() {
// 组件即将出现时,启动动画循环
this.animate()
}
// ... build() 方法略
}
💡 技术解析:
RenderingContextSettings(true)中的true代表开启抗锯齿 (Anti-aliasing)。对于需要绘制圆形、斜线、曲线的场景,抗锯齿是必不可少的,否则图形边缘会出现明显的像素马赛克。Canvas组件在build()中通过.onReady()回调来确保画布已经挂载完毕,此时才可以调用drawGeometry()进行绘制。
二、 复杂几何图形的数学原理与代码剖析
图形学的基础是数学。在二维画布中,我们主要利用极坐标系与直角坐标系的转换来完成复杂图形的计算。
2.1 正多边形与星形:三角函数的巧妙运用
无论是正六边形还是五角星,核心思想都是在一个圆周上等间距地获取顶点,然后将它们连接起来。
核心代码截取
drawStar(ctx: CanvasRenderingContext2D, points: number, outerR: number, innerR: number, fill: string, stroke: string) {
ctx.beginPath()
// 乘以 2 是因为星形有内外两个顶点交替
for (let i = 0; i <= points * 2; i++) {
// 计算当前顶点的角度:从 -90度 (12点钟方向) 开始
const angle = (i / (points * 2)) * 2 * Math.PI - Math.PI / 2
// 偶数索引取外半径,奇数索引取内半径
const r = i % 2 === 0 ? outerR : innerR
// 极坐标转直角坐标公式
const x = r * Math.cos(angle)
const y = r * Math.sin(angle)
if (i === 0) ctx.moveTo(x, y)
else ctx.lineTo(x, y)
}
ctx.closePath()
ctx.fillStyle = fill
ctx.fill()
ctx.strokeStyle = stroke
ctx.lineWidth = 2
ctx.stroke()
}
📐 数学原理解析
在屏幕坐标系中,向右是 X 轴正方向,向下是 Y 轴正方向。
我们在圆上取点时,使用的是极坐标转直角坐标的经典公式:
x = r ⋅ cos ( θ ) x = r \cdot \cos(\theta) x=r⋅cos(θ)
y = r ⋅ sin ( θ ) y = r \cdot \sin(\theta) y=r⋅sin(θ)
代码中 - Math.PI / 2 的作用是将起始角度逆时针旋转 90 度,使得多边形或星形的第一个顶点正对上方(即 12 点钟方向),这样看起来更符合人类的视觉习惯。
2.2 贝塞尔曲线花瓣:高级路径绘制
贝塞尔曲线(Bézier curve)是计算机图形学中最重要的一环,常用于绘制平滑的复杂曲线。
// ---- 4. 贝塞尔曲线花瓣 ----
ctx.save()
ctx.translate(340, 200) // 将坐标原点移至画布中心
const petalCount = 6 // 花瓣数量
for (let i = 0; i < petalCount; i++) {
const angle = (i / petalCount) * 2 * Math.PI
ctx.save()
ctx.rotate(angle) // 根据花瓣索引旋转画布
ctx.beginPath()
ctx.moveTo(0, 0)
// 三次贝塞尔曲线 (cp1x, cp1y, cp2x, cp2y, x, y)
ctx.bezierCurveTo(20, -30, 50, -30, 40, 0)
ctx.bezierCurveTo(50, 30, 20, 30, 0, 0)
ctx.fillStyle = `hsla(${i * 60}, 80%, 65%, 0.7)` // 使用 HSL 颜色模型实现渐变色环
ctx.fill()
ctx.restore()
}
💡 技术解析:
- 状态隔离: 这里大量使用了
ctx.save()和ctx.restore()。在循环内旋转画布(ctx.rotate)前保存状态,画完一片花瓣后恢复状态,确保每一片花瓣的计算都是独立的,不会发生旋转角度累加的错误。 - 三次贝塞尔 API:
bezierCurveTo需要两个控制点和一个终点。通过控制点拉伸曲线,两段贝塞尔曲线完美拼凑成了一个水滴/花瓣的形状。
2.3 分形树:递归算法的艺术
分形树是典型的递归 (Recursion) 在图形界面的应用。每一个树枝的分叉都可以看作是一棵更小的树。
drawBranch(ctx: CanvasRenderingContext2D, x: number, y: number, angle: number, len: number, depth: number) {
if (depth <= 0) return // 递归终止条件
// 计算树枝终点坐标
const nx = x + len * Math.cos(angle)
const ny = y + len * Math.sin(angle)
ctx.beginPath()
ctx.moveTo(x, y)
ctx.lineTo(nx, ny)
// 随着深度减小,树枝颜色变浅,线条变细
ctx.strokeStyle = `hsl(30, 60%, ${30 + depth * 8}%)`
ctx.lineWidth = depth * 1.2
ctx.stroke()
// 递归调用:向左侧和右侧偏转产生新分支
this.drawBranch(ctx, nx, ny, angle - 0.5, len * 0.7, depth - 1)
this.drawBranch(ctx, nx, ny, angle + 0.5, len * 0.7, depth - 1)
}
核心逻辑:
每深入一层,树枝长度 len 缩减为原来的 70% (len * 0.7),角度分别向两侧偏移 0.5 弧度。当递归深度 depth 归零时停止。这寥寥数行代码就能渲染出极其复杂的自然树木形态。
三、 动态数据可视化:手搓各种图表
现代应用离不开数据报表。接下来我们将拆解图表绘制模块。
3.1 动态渲染机制与假数据生成
要让图表“动”起来,我们需要引入定时器。
animate() {
// 每 2 秒刷新一次图表模拟数据更新
setInterval(() => {
this.drawCharts()
}, 2000)
setTimeout((): void => this.drawCharts(), 100)
}
// 随机数据生成器
randomData(count: number, min: number, max: number): number[] {
const arr: number[] = []
for (let i = 0; i < count; i++) {
arr.push(Math.floor(min + Math.random() * (max - min)))
}
return arr
}
提示:在实际生产环境中,请尽量使用 requestAnimationFrame 来处理丝滑的高帧率动画,setInterval 主要用于定时拉取后端数据。
3.2 柱状图 (Bar Chart):坐标系映射与渐变填充
柱状图的核心在于将实际数据映射到 Canvas 像素高度上。
// 数据映射逻辑片段
data.forEach((v, i) => {
// 计算柱子高度,假设满分 100 对应 chartH
const barH = (v / 100) * chartH
const x = 35 + i * (barW + 8)
const y = chartTop + chartH - barH // Y轴向下为正,需要反向计算起点
// 创建线性渐变对象
const grad = ctx.createLinearGradient(x, y, x, chartTop + chartH)
grad.addColorStop(0, colors[i % colors.length])
grad.addColorStop(1, colors[i % colors.length] + '44') // 增加透明度
ctx.fillStyle = grad
ctx.beginPath()
// 鸿蒙专属增强 API:带圆角的矩形 [左上, 右上, 右下, 左下]
ctx.roundRect(x, y, barW, barH, [4, 4, 0, 0])
ctx.fill()
})
亮点: 代码使用了 ctx.roundRect。传统的 HTML5 Canvas 画圆角矩形极其痛苦,需要用四个 arcTo 拼接,而 ArkTS 提供了非常贴心的 roundRect 原生支持,极大提高了绘制效率。
3.3 雷达图 (Radar Chart):多维数据呈现
雷达图是游戏面板和能力评估中最常见的图表。它其实是正多边形代码的变体。
// 绘制数据填充区域
ctx.beginPath()
data.forEach((v, i) => {
const angle = (i / sides) * 2 * Math.PI - Math.PI / 2
// 根据数据百分比计算当前点距离圆心的实际半径
const radius = (v / 100) * r
const x = radius * Math.cos(angle)
const y = radius * Math.sin(angle)
if (i === 0) ctx.moveTo(x, y)
else ctx.lineTo(x, y)
})
ctx.closePath()
// 半透明填充
ctx.fillStyle = 'rgba(171, 71, 188, 0.25)'
ctx.fill()
3.4 堆叠面积图 (Stacked Area Chart)
面积图是在折线图的基础上填充下方区域。堆叠面积图的难点在于第二组数据的值需要累加在第一组数据之上。
// 第二层面积(叠加)
ctx.beginPath()
ctx.moveTo(20, chartTop + chartH)
d2.forEach((v, i) => {
// 核心:dv 是两组数据的总和
const dv = d1[i] + v
ctx.lineTo(20 + i * stepX, toY(dv))
})
ctx.lineTo(20 + (points - 1) * stepX, chartTop + chartH)
ctx.closePath()
四、运行图片展示


四、 图表类型与适用场景对比汇总
为了帮助大家在实际业务中更好地选择图表类型,特整理如下表格:
| 图表类型 (Chart Type) | 代码实现核心 API | 视觉表现与难点 | 适用业务场景 |
|---|---|---|---|
| 柱状图 (Bar) | roundRect, createLinearGradient |
矩形高度映射,底部对齐,渐变渲染效果好。 | 各组别数据大小对比(如:各月销售额)。 |
| 折线图 (Line) | lineTo, arc (画数据点) |
需计算坐标系步长(stepX),通过 moveTo 和 lineTo 连接。 |
表现数据随时间的变化趋势(如:股票走势)。 |
| 饼图 (Pie) | arc, 角度累加器 |
需先 reduce 求和,然后按比例将 2 π 2\pi 2π 划分成不同扇形角度。 |
展现各部分占整体的比例(如:用户终端占比)。 |
| 雷达图 (Radar) | lineTo + 极坐标转换数学公式 |
背景需绘制多层同心多边形,数据点通过极坐标按比例计算。 | 多维度能力指标评估(如:游戏人物五维图、绩效考核)。 |
| 面积图 (Area) | lineTo, fill() 闭合路径 |
折线图向下闭合路径并填充透明度颜色;堆叠需累加 Y 值。 | 强调数量随时间演变趋势的同时,突出累积量。 |
五、 ArkTS Canvas 核心 API 速查表
在源码中,我们大量使用了图形绘制的底层 API。为了巩固知识,整理核心方法说明表:
| 方法/属性名 | 核心作用与说明 | 最佳实践/注意事项 |
|---|---|---|
save() / restore() |
保存/恢复当前的绘图状态(如旋转角度、原点位置、当前颜色配置)。 | 必考点。改变坐标系前必须 save,画完必须 restore,否则后续所有图形都会乱套。 |
translate(x, y) |
移动画布的原点(0, 0)至指定坐标。 | 画复杂图形前将原点移至图形中心,可极大简化坐标数学计算。 |
beginPath() |
开启一条全新的路径。 | 每次画独立的线/多边形前必须调用,否则 stroke() 会把之前的线重新画一遍,导致严重卡顿。 |
closePath() |
闭合路径(自动将终点与起点连线)。 | 适用于画封闭多边形,不仅视觉闭合,还能防止线段交接处出现缺口(LineJoin)。 |
bezierCurveTo |
绘制三次贝塞尔曲线。 | 用于需要完美弧度平滑过渡的场景(如花瓣、水滴、拟态 UI)。 |
createRadialGradient |
创建径向渐变(从内向外发散)。 | 源码中的“渐变圆环”即使用了此 API。需配合 addColorStop 使用。 |
六、 进阶与性能优化建议 (Bonus)
当你在生产环境中大规模手撸 Canvas 图表时,可能会遇到性能瓶颈(尤其是在低端设备或刷新率要求极高的动画场景中)。以下是给中高级开发者的优化建议:
- 避免在
render/draw循环中做对象分配:
在animate或者setInterval回调中,尽量避免反复new新的对象或反复定义庞大的数组。应当把变量提为类属性(复用对象内存),减少 GC(垃圾回收)带来的瞬时卡顿。 - 离屏渲染 (Offscreen Canvas):
如果背景网格(如雷达图的网格背景、折线图的 XY 轴与坐标线)是不变的,只有数据在动,应该用一个隐藏的离屏 Canvas 预先画好背景,然后在主动画循环中利用drawImage直接把背景贴上去。这能节省 50% 以上的绘制开销。 - 减少浮点运算:
Canvas 渲染时,坐标如果带有过多的小数位,底层会触发次像素渲染(Sub-pixel rendering)从而消耗更多 CPU。对于不需要极致精度的坐标,可以用Math.floor()或位运算| 0进行取整处理。 - 提防
save/restore滥用:
虽然save和restore非常好用,但它们涉及到状态栈的压栈出栈操作。如果只是改一下颜色(fillStyle),直接覆盖赋值即可,没必要save/restore。只在涉及矩阵变换(translate,rotate,scale)时才使用它们。
总结
通过对这段 ArkTS 代码的拆解,我们不仅复习了三角函数、极坐标等数学知识在图形学上的应用,还完整走通了企业级图表可视化的底层渲染逻辑。
Canvas 就像是一张白纸和一盒水彩笔,框架为你提供的是笔触的大小、颜色的调配,而最终能画出什么惊艳的作品,完全取决于开发者对数学的理解与对代码架构的把控力。
更多推荐



所有评论(0)