你还在用图片硬怼特效?Canvas/Path/粒子动画这么香,你不想亲手“画”一个吗?

🏆本文收录于「滚雪球学SpringBoot」专栏(全网一个名),手把手带你零基础入门Spring Boot,从入门到就业,助你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
环境说明:Windows 10 + IntelliJ IDEA 2021.3.2 + Jdk 1.8
🎨 摘要
啊哈,说到 Canvas / SVG 绘图,我这人就有点兴奋😆。因为它属于那种——**你写 UI 写久了会麻木,但你一画起来就立刻“有手感”**的领域。尤其在鸿蒙里,Canvas 既能搞 2D 绘图、Path 曲线、粒子动画,还能做自定义视图(比如仪表盘、波纹进度、涂鸦板)。
但我也得吐槽一句:很多同学一上来就想“做个粒子特效炸裂全场”,结果 10 分钟后手机风扇都快起飞了🥲。所以这节我们按你大纲来,先打基础,再玩花活,最后给你一个完整“自定义视图 + 粒子动画”的实战示例,保证你看完能立刻开撸🔥
🧭目录(画画也得讲章法🖌️)
- 🧱 Canvas 2D 绘图:坐标系、画笔、常用 API
- 🌀 Path / Path2D:曲线与形状的灵魂(含示例)
- ✨ 粒子动画:从“会动”到“好看还不掉帧”】【重点】
- 🧩 自定义视图:封装成组件,像搭积木一样复用
- 🕳️ 性能与坑位:我替你踩过了😭
说明:ArkUI 的绘图组件和 API 在不同版本/设备形态上会有差异(比如 Canvas 组件、绘图上下文能力等)。我下面用的是“可迁移的通用写法 + 常见思路”,你可以直接套进工程里,再根据 IDE 提示微调方法名即可😄
🧱 Canvas 2D 绘图:坐标系、画笔、常用 API
Canvas 2D 你可以把它当成一块“画布”,我们干的事就两步:
- 拿到绘图上下文(ctx)🧠
- 用 ctx 画图形/文字/图片🎨
✅ 一个最小可用 Canvas 示例:画网格 + 圆点(特别适合练手😆)
@Entry
@Component
struct CanvasBasicDemo {
private size: number = 320
build() {
Column() {
Text('🧱 Canvas 2D:网格 + 圆点示例')
.fontSize(18)
.margin({ bottom: 12 })
// 具体 Canvas 组件的 onReady / onDraw 回调名字,
// 不同版本可能略有差异,你按 IDE 提示对齐即可😄
Canvas()
.width(this.size)
.height(this.size)
.onReady((ctx: any) => {
this.draw(ctx)
})
.backgroundColor('#111')
}
.padding(20)
}
private draw(ctx: any) {
const w = this.size
const h = this.size
// 1) 清空
ctx.clearRect(0, 0, w, h)
// 2) 画网格
ctx.strokeStyle = 'rgba(255,255,255,0.12)'
ctx.lineWidth = 1
const step = 20
for (let x = 0; x <= w; x += step) {
ctx.beginPath()
ctx.moveTo(x, 0)
ctx.lineTo(x, h)
ctx.stroke()
}
for (let y = 0; y <= h; y += step) {
ctx.beginPath()
ctx.moveTo(0, y)
ctx.lineTo(w, y)
ctx.stroke()
}
// 3) 画个圆点
ctx.fillStyle = '#00E0FF'
ctx.beginPath()
ctx.arc(w / 2, h / 2, 10, 0, Math.PI * 2)
ctx.fill()
// 4) 写字(可选)
ctx.fillStyle = 'rgba(255,255,255,0.9)'
ctx.font = '14px sans-serif'
ctx.fillText('😄 center', w / 2 + 14, h / 2 + 5)
}
}
你别小看这个网格,它是你后面画复杂 Path、做粒子运动最好的“参考系”。
没有网格,你调参数就像闭眼投骰子🎲🤣
🌀 Path / Path2D:曲线与形状的灵魂(含示例)
Canvas 里画复杂图形,靠的就是 Path。你可以把 Path 理解成:
“先用一根线把形状勾出来,再 stroke / fill 上色。”
如果有 Path2D,那就更爽:你能把路径当对象复用(比如重复绘制 logo、轨迹等)。
✅ 示例:用 Path 画一个“心形”(别笑,我真用它做过动效🤣💗)
private drawHeart(ctx: any, cx: number, cy: number, s: number) {
ctx.save()
ctx.translate(cx, cy)
ctx.scale(s, s)
ctx.beginPath()
ctx.moveTo(0, -2)
ctx.bezierCurveTo(2, -4, 6, -2, 0, 4)
ctx.bezierCurveTo(-6, -2, -2, -4, 0, -2)
ctx.closePath()
ctx.fillStyle = 'rgba(255, 80, 120, 0.9)'
ctx.fill()
ctx.restore()
}
调用:
this.drawHeart(ctx, 160, 140, 18)
这里的关键点你抓住两个就够了:
bezierCurveTo是画“丝滑曲线”的核心🧈save/restore + translate/scale是做图形复用与定位的“王炸组合”💣
✅ Path2D 思路(如果你环境支持)
如果支持 Path2D,你可以这么干:
- 创建一次 Path2D
- 每一帧只做 transform + draw
这样能减少重复构建路径的开销,动画更稳⚡
✨ 粒子动画:从“会动”到“好看还不掉帧”(重点)
粒子动画说白了就是:
- 一堆点(粒子)
- 每帧更新位置、速度、寿命
- 然后重新绘制
听起来简单对吧?但“好看且不卡”就不简单了😅
🎯 粒子系统的最小模型(够你做 80% 特效)
class Particle {
x: number
y: number
vx: number
vy: number
life: number
size: number
constructor(x: number, y: number) {
this.x = x
this.y = y
this.vx = (Math.random() - 0.5) * 2.4
this.vy = (Math.random() - 0.5) * 2.4
this.life = 60 + Math.floor(Math.random() * 60) // 帧寿命
this.size = 1 + Math.random() * 2.5
}
step() {
this.x += this.vx
this.y += this.vy
this.vy += 0.02 // 重力一点点,立刻“有感觉”😄
this.life -= 1
}
get alive(): boolean {
return this.life > 0
}
}
✅ 粒子动画组件(带“发射器”+ 渐隐效果)
@Entry
@Component
struct ParticleCanvasDemo {
private w: number = 340
private h: number = 220
@State private running: boolean = true
private particles: Particle[] = []
private ctx: any = null
private timer: number = -1
build() {
Column() {
Text('✨ 粒子动画:轻量发射器(不卡为王)')
.fontSize(18)
.margin({ bottom: 10 })
Row() {
Button(this.running ? '⏸️ 暂停' : '▶️ 继续')
.onClick(() => {
this.running = !this.running
if (this.running) this.start()
else this.stop()
})
}.margin({ bottom: 10 })
Canvas()
.width(this.w)
.height(this.h)
.onReady((ctx: any) => {
this.ctx = ctx
this.start()
})
.backgroundColor('#0B0F14')
.borderRadius(12)
}
.padding(20)
}
private start() {
this.stop()
// 用 setInterval 模拟一帧一帧刷新(你也可以换成更“系统级”的帧回调)
this.timer = setInterval(() => {
if (!this.running || !this.ctx) return
this.emit()
this.update()
this.render()
}, 16) as unknown as number // ~60fps
}
private stop() {
if (this.timer !== -1) {
clearInterval(this.timer)
this.timer = -1
}
}
private emit() {
// 每帧发射 4 个粒子(别贪多,贪多就掉帧😭)
for (let i = 0; i < 4; i++) {
this.particles.push(new Particle(this.w * 0.2, this.h * 0.7))
}
// 控制上限:永远别让粒子无限增长
if (this.particles.length > 600) {
this.particles.splice(0, this.particles.length - 600)
}
}
private update() {
for (const p of this.particles) p.step()
this.particles = this.particles.filter(p => p.alive)
}
private render() {
const ctx = this.ctx
ctx.clearRect(0, 0, this.w, this.h)
// 背景淡淡拖影(有动感但不刺眼😄)
ctx.fillStyle = 'rgba(11,15,20,0.35)'
ctx.fillRect(0, 0, this.w, this.h)
for (const p of this.particles) {
const alpha = Math.max(0, Math.min(1, p.life / 120))
ctx.fillStyle = `rgba(0, 224, 255, ${alpha})`
ctx.beginPath()
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2)
ctx.fill()
}
}
}
🧠 为啥这套不容易卡?
- ✅ 粒子数量有上限(你不限制,它就无限膨胀)
- ✅ 每帧只做 O(n) 更新 + 绘制(没有复杂计算)
- ✅ 用拖影减少“闪烁”,视觉更顺滑✨
小提醒:如果你要做更高级的“爆炸、烟雾、流体感”,建议引入噪声场/向量场,但那又是另一个宇宙了🤣
🧩 自定义视图:封装成组件,像搭积木一样复用 🧱
Canvas 真正的价值,是你能把“绘制能力”封装成一个组件,外部只喂数据,内部自己画。
这就像你写了一个“仪表盘组件”,别的页面想用,直接 <Gauge value=... /> 就行,爽爆😎
✅ 示例:自定义“波纹进度条”组件(Path + 动画味儿)
@Component
struct WaveProgress {
@Prop value: number // 0~1
private w: number = 260
private h: number = 120
private phase: number = 0
private ctx: any = null
private timer: number = -1
build() {
Canvas()
.width(this.w)
.height(this.h)
.onReady((ctx: any) => {
this.ctx = ctx
this.start()
})
.borderRadius(14)
.backgroundColor('#081018')
}
private start() {
this.stop()
this.timer = setInterval(() => {
this.phase += 0.15
this.draw()
}, 16) as unknown as number
}
private stop() {
if (this.timer !== -1) {
clearInterval(this.timer)
this.timer = -1
}
}
private draw() {
if (!this.ctx) return
const ctx = this.ctx
ctx.clearRect(0, 0, this.w, this.h)
ctx.fillStyle = '#081018'
ctx.fillRect(0, 0, this.w, this.h)
// 进度高度
const level = this.h * (1 - Math.max(0, Math.min(1, this.value)))
// 波纹路径
ctx.beginPath()
ctx.moveTo(0, this.h)
const amp = 6
const freq = 0.045
for (let x = 0; x <= this.w; x += 6) {
const y = level + Math.sin(x * freq + this.phase) * amp
ctx.lineTo(x, y)
}
ctx.lineTo(this.w, this.h)
ctx.closePath()
ctx.fillStyle = 'rgba(0, 224, 255, 0.75)'
ctx.fill()
// 文本
ctx.fillStyle = 'rgba(255,255,255,0.92)'
ctx.font = '16px sans-serif'
const pct = Math.round(this.value * 100)
ctx.fillText(`🌊 ${pct}%`, 14, 26)
}
}
使用:
@Entry
@Component
struct WaveDemoPage {
@State v: number = 0.34
build() {
Column() {
Text('🧩 自定义视图:波纹进度条').fontSize(18).margin({ bottom: 10 })
WaveProgress({ value: this.v })
Slider({ value: this.v, min: 0, max: 1 })
.onChange(val => this.v = val)
.margin({ top: 12 })
}.padding(20)
}
}
你看,这就是我最喜欢的那种开发体验:
外部“喂值”,内部“画图”,组件像积木一样复用🧱😆
🕳️ 性能与坑位:我替你踩过了😭
1)😵 每帧 new 一堆对象(最常见掉帧元凶)
- ❌ 每帧都
new Particle()一大堆还不限制数量 - ✅ 控制上限、复用对象(对象池)更稳
2)🥲 粒子数量无上限增长
- ✅ 永远设置
maxParticles - ✅ 超出就丢弃旧的 or 不再发射
3)🌀 Path 计算太精细(for 循环步长太小)
- ❌
x += 1画满屏 1000+ 点,每帧算到你哭 - ✅ 视觉允许时,
x += 4/6/8也很好看
4)🧨 没做 clear / 背景处理导致“脏画布”
- ✅ 每帧
clearRect或者做“半透明覆盖拖影”
5)😤 动画计时器没停,页面切走还在跑
- ✅ 组件生命周期里记得 stop(比如
aboutToDisappear)
(不然你的电量会对你有意见🔋🙃)
✅ 小结(把话说得更直白一点😄)
- Canvas 2D 负责“画”🎨
- Path/Path2D 负责“画得好看”🌀
- 粒子系统负责“画得会动还不掉帧”✨
- 自定义视图负责“画完还能复用、还能维护”🧩
🧧福利赠与你🧧
无论你是计算机专业的学生,还是对编程有兴趣的小伙伴,都建议直接毫无顾忌的学习此专栏「滚雪球学SpringBoot」专栏(全网一个名),bug菌郑重承诺,凡是学习此专栏的同学,均能获取到所需的知识和技能,全网最快速入门SpringBoot,就像滚雪球一样,越滚越大, 无边无际,指数级提升。
最后,如果这篇文章对你有所帮助,帮忙给作者来个一键三连,关注、点赞、收藏,您的支持就是我坚持写作最大的动力。
同时欢迎大家关注公众号:「猿圈奇妙屋」 ,以便学习更多同类型的技术文章,免费白嫖最新BAT互联网公司面试题、4000G pdf电子书籍、简历模板、技术文章Markdown文档等海量资料。
✨️ Who am I?
我是bug菌(全网一个名),CSDN | 掘金 | InfoQ | 51CTO | 华为云 | 阿里云 | 腾讯云 等社区博客专家,C站博客之星Top30,华为云多年度十佳博主/价值贡献奖,掘金多年度人气作者Top40,掘金等各大社区平台签约作者,51CTO年度博主Top12,掘金/InfoQ/51CTO等社区优质创作者;全网粉丝合计 30w+;更多精彩福利点击这里;硬核微信公众号「猿圈奇妙屋」,欢迎你的加入!免费白嫖最新BAT互联网公司面试真题、4000G PDF电子书籍、简历模板等海量资料,你想要的我都有,关键是你不来拿。

-End-
- 点赞
- 收藏
- 关注作者
评论(0)