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

举报
bug菌 发表于 2025/12/25 14:50:53 2025/12/25
【摘要】 🏆本文收录于「滚雪球学SpringBoot」专栏(全网一个名),手把手带你零基础入门Spring Boot,从入门到就业,助你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!环境说明:Windows 10 + IntelliJ IDEA 2021.3.2 + Jdk 1.8 🎨 摘要啊哈,说到 Canvas / SVG 绘图,我这人就有点兴奋...

🏆本文收录于「滚雪球学SpringBoot」专栏(全网一个名),手把手带你零基础入门Spring Boot,从入门到就业,助你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!

环境说明:Windows 10 + IntelliJ IDEA 2021.3.2 + Jdk 1.8

🎨 摘要

啊哈,说到 Canvas / SVG 绘图,我这人就有点兴奋😆。因为它属于那种——**你写 UI 写久了会麻木,但你一画起来就立刻“有手感”**的领域。尤其在鸿蒙里,Canvas 既能搞 2D 绘图、Path 曲线、粒子动画,还能做自定义视图(比如仪表盘、波纹进度、涂鸦板)。
  但我也得吐槽一句:很多同学一上来就想“做个粒子特效炸裂全场”,结果 10 分钟后手机风扇都快起飞了🥲。所以这节我们按你大纲来,先打基础,再玩花活,最后给你一个完整“自定义视图 + 粒子动画”的实战示例,保证你看完能立刻开撸🔥

🧭目录(画画也得讲章法🖌️)

说明:ArkUI 的绘图组件和 API 在不同版本/设备形态上会有差异(比如 Canvas 组件、绘图上下文能力等)。我下面用的是“可迁移的通用写法 + 常见思路”,你可以直接套进工程里,再根据 IDE 提示微调方法名即可😄

🧱 Canvas 2D 绘图:坐标系、画笔、常用 API

Canvas 2D 你可以把它当成一块“画布”,我们干的事就两步:

  1. 拿到绘图上下文(ctx)🧠
  2. 用 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-

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

评论(0

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

全部回复

上滑加载中

设置昵称

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

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

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