HarmonyOS 鸿蒙Next中Canvas绘制动态粒子卡顿乃至系统“重启”

HarmonyOS 鸿蒙Next中Canvas绘制动态粒子卡顿乃至系统“重启” 最近使用鸿蒙原生绘制一个烟花升空爆炸的效果,之前是在 flutter下尝试的,效果很流畅,现在想试试鸿蒙下的效果,发现启动之后界面就卡主了,不一会系统直接重启,不知道是哪里没弄好。代码如下:

import hilog from '@ohos.hilog';
// import { GlobalContext } from '@ohos.ablilit';

const TAG = '[ENTRY_MAINABILITY]';
const DOMAIN = 0xFF00;

const COLORS = [
  'rgba(228,180,175,1)',
  'rgba(192,134,130,1)',
  'rgba(212,157,163,1)',
  'rgba(229,0,44,1)',
  'rgba(192,79,52,1)',
  'rgba(177,78,78,1)',
  'rgba(251,177,182,1)',
  'rgba(225,81,116,1)',
  'rgba(225,187,190,1)',
  'rgba(135,87,71,1)',
  'rgba(253,132,69,1)',
  'rgba(254,140,75,1)',
  'rgba(249,169,121,1)',
  'rgba(134,62,81,1)',
];

interface Point {
  x: number;
  y: number;
}

interface Size {
  width: number,
  height: number
}

// MARK: - 数据模型
/// 粒子数据模型
class Particle {
  id: number = Date.now() + Math.random();
  position: Point;
  velocity: Point;
  color: string;
  decay: number;
  alpha: number = 1.0;
  trail: Point[] = [];

  constructor(x: number, y: number, velocityX: number, velocityY: number, color: string, decay: number) {
    this.position = { x: x, y: y };
    this.velocity = { x: velocityX, y: velocityY };
    this.color = color;
    this.decay = decay;
  }
}

/// 烟花数据模型
class Firework {
  id: number = Date.now() + Math.random();
  position: Point;
  velocity: Point;
  targetY: number;
  color: string;
  exploded: boolean = false;

  constructor(x: number, y: number, velocityX: number, velocityY: number, targetY: number, color: string) {
    this.position = { x: x, y: y };
    this.velocity = { x: velocityX, y: velocityY };
    this.targetY = targetY;
    this.color = color;
  }
}

// MARK: - 烟花控制器
/// 控制器类,管理烟花和粒子的物理效果及生命周期
class FireworksController {
  public fireworks: Firework[] = [];
  public particles: Particle[] = [];
  public  canvasSize: Size = { width: 0, height: 0 };
  private animationInterval: number | null = null;
  private launchInterval: number | null = null;

  public readonly GRAVITY: number = 0.05;
  public readonly FRICTION: number = 0.95;
  public readonly MAX_TRAIL_LENGTH: number = 10;
  public readonly MAX_PARTICLES: number = 10;
  public readonly MAX_FIREWORKS: number = 1;
  private context: CanvasRenderingContext2D


  constructor(context: CanvasRenderingContext2D) {
    this.context = context
  }

  setCanvasSize(width: number, height: number) {
    hilog.info(DOMAIN, "canvas_size", `w: ${width}, h: ${height}` )
    this.canvasSize.width = width;
    this.canvasSize.height = height;
  }

  /// 启动动画和烟花发射
  start() {
    if (this.animationInterval !== null) return;

    hilog.info(DOMAIN, TAG, 'Fireworks animation started');

    this.animationInterval = setInterval(() => {
      this.updatePhysics();
      this.draw();
    }, 16); // 大约 60FPS

    this.launchInterval = setInterval(() => {
      this.launchFirework();
    }, 500);
  }

  /// 停止动画和清理所有对象
  stop() {
    if (this.animationInterval !== null) {
      clearInterval(this.animationInterval);
      this.animationInterval = null;
    }
    if (this.launchInterval !== null) {
      clearInterval(this.launchInterval);
      this.launchInterval = null;
    }

    this.fireworks = [];
    this.particles = [];
    hilog.info(DOMAIN, TAG, 'Fireworks animation stopped');
  }

  /// 更新所有烟花和粒子的物理状态
  private updatePhysics() {
    // 更新并移除已爆炸的烟花
    this.fireworks = this.fireworks.filter(firework => {
      if (firework.exploded) return false;

      firework.position.y += firework.velocity.y;
      firework.velocity.y += this.GRAVITY * 0.5;

      if (firework.velocity.y > 0 || firework.position.y <= firework.targetY) {
        firework.exploded = true;
        this.explode(firework.position, firework.color);
        return false;
      }
      return true;
    });

    // 更新并移除已消失的粒子
    this.particles = this.particles.filter(particle => {
      // 更新拖尾
      particle.trail.push({ x: particle.position.x, y: particle.position.y });
      if (particle.trail.length > this.MAX_TRAIL_LENGTH) {
        particle.trail.shift();
      }

      // 应用摩擦力和重力
      particle.velocity.x *= this.FRICTION;
      particle.velocity.y *= this.FRICTION;
      particle.velocity.y += this.GRAVITY;

      // 移动粒子
      particle.position.x += particle.velocity.x;
      particle.position.y += particle.velocity.y;

      // 逐渐消失
      particle.alpha -= particle.decay;

      return particle.alpha > 0;
    });
  }

  /// 随机发射一个新烟花
  private launchFirework() {
    if (this.fireworks.length < this.MAX_FIREWORKS && this.canvasSize.width > 0) {
      const startX = Math.random() * this.canvasSize.width;
      const startY = this.canvasSize.height;
      const targetY = Math.random() * this.canvasSize.height * 0.4;
      const color = this.randomColor();
      if(color){
        const firework = new Firework(
          startX,
          startY,
          0,
          -(Math.random() * 3 + 2),
          targetY,
          color
        );
        this.fireworks.push(firework);
      }
    }
  }

  /// 在指定位置爆炸并生成粒子
  private explode(position: Point, color: string) {
    const particleCount = Math.floor(Math.random() * 50) + 70;
    for (let i = 0; i < particleCount; i++) {
      if (this.particles.length < this.MAX_PARTICLES) {
        const angle = Math.random() * 2 * Math.PI;
        const speed = Math.random() * 5 + 1;
        const velocityX = Math.cos(angle) * speed;
        const velocityY = Math.sin(angle) * speed;
        const decay = Math.random() * 0.03 + 0.01;
        this.particles.push(new Particle(position.x, position.y, velocityX, velocityY, color, decay));
      }
    }
  }

  /// 生成一个随机的颜色
  public  randomColor(): string {
    const index =  Math.floor(-0.5 + Math.random() * (COLORS.length + 0.5));
    return COLORS[index % COLORS.length];
  }

  /// 绘制所有烟花和粒子
  private draw() {
    if (!this.context) return;
    const ctx = this.context;
    // 绘制半透明黑色矩形,制造拖尾效果
    ctx.globalCompositeOperation = 'source-over';
    ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
    ctx.fillRect(0, 0, this.canvasSize.width, this.canvasSize.height);

    // 使用 'lighter' 混合模式,使颜色叠加更亮
    ctx.globalCompositeOperation = 'lighter';

    // 绘制烟花
    this.fireworks.forEach(firework => {
      ctx.beginPath();
      ctx.arc(firework.position.x, firework.position.y, 2, 0, 2 * Math.PI);
      ctx.fillStyle = firework.color;
      ctx.fill();
    });

    // 绘制粒子和拖尾
    this.particles.forEach(particle => {
      // 绘制粒子本体
      ctx.beginPath();
      ctx.arc(particle.position.x, particle.position.y, 1.0, 0, 2 * Math.PI);
      let  particleColor = particle.color;
      // 'rgba(134,62,81,1)'
      // const colorParts = particleColor.split(',')
      // colorParts[colorParts.length - 1] = `${particle.alpha}})`
      ctx.fillStyle = Color.Pink// colorParts.join(',')// particle.color.replace(')', `, ${particle.alpha})`).replace('hsl', 'hsla');
      ctx.fill();

      // 绘制拖尾
      for (let i = 0; i < particle.trail.length; i++) {
        const trailAlpha = particle.alpha * (i / particle.trail.length);
        const trailRadius = 1.0 * (i / particle.trail.length);
        const trailPosition = particle.trail[i];
        ctx.beginPath();
        ctx.arc(trailPosition.x, trailPosition.y, trailRadius, 0, 2 * Math.PI);
        // colorParts[colorParts.length - 1] = `${trailAlpha}})`
        ctx.fillStyle = Color.Red// colorParts.join(',')// particle.color.replace(')', `, ${trailAlpha})`).replace('hsl', 'hsla');
        ctx.fill();
      }
    });
  }
}

// MARK: - 主页面
@Component
export struct FireworksPage {
  private settings: RenderingContextSettings = new RenderingContextSettings(true);
  private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);
  @State  controller: FireworksController = new FireworksController(this.context);

  aboutToAppear(): void {
    this.controller.start();
  }
  onPageShow() {
    hilog.info(DOMAIN, TAG, 'FireworksPage onPageShow');
  }

  onPageHide() {
    this.controller.stop();
    hilog.info(DOMAIN, TAG, 'FireworksPage onPageHide');
  }

  build() {
    Stack({ alignContent: Alignment.Bottom }) {
      Canvas(this.context)
        .width('100%')
        .height('100%')
        .backgroundColor(Color.Black)
        .onReady(() => {
          hilog.info(DOMAIN, TAG, 'Canvas onReady');
          const color =  this.controller.randomColor();
          hilog.info(DOMAIN, "color", color)
          if(color){
              this.context.fillStyle = color
              this.context.fillRect(50, 50, 100, 100)
          } else {
            hilog.info(DOMAIN, "color", color)
          }
          // this.controller.start()
        })
        .onAreaChange((_oldArea, newArea) => {
          // 更新 canvas 大小
          hilog.info(DOMAIN, TAG, 'Canvas onAreaChange');
          this.controller.setCanvasSize(newArea.width as number, newArea.height as number);
        })

      Row({space: 12}) {
        Button('开始')
          .onClick(() => {
            this.controller.start();
          })
        Button('停止')
          .onClick(() => {
            this.controller.stop();
          })
      }
      .justifyContent(FlexAlign.Center)
      .padding({ bottom: 32 })
      .width('100%')
    }
  }
}

更多关于HarmonyOS 鸿蒙Next中Canvas绘制动态粒子卡顿乃至系统“重启”的实战教程也可以访问 https://www.itying.com/category-93-b0.html

6 回复

【问题定位】

观察代码发现draw()方法,发现存在globalCompositeOperation从source-over来回切换lighter操作;由于draw()方法一直在频繁调用(16毫秒),所以导致非常卡顿。

【分析结论】

globalCompositeOperation从source-over和lighter之间来回切换。

【修改建议】

注释掉如下代码:

// ctx.globalCompositeOperation = 'lighter';

更多关于HarmonyOS 鸿蒙Next中Canvas绘制动态粒子卡顿乃至系统“重启”的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


确实可行,

我这运行是好的,没有出现重启,就是烟花有点卡,楼主用的是什么手机,版本多少?

nova 14 pro

在HarmonyOS Next中,Canvas绘制动态粒子卡顿或导致系统“重启”,通常涉及渲染性能问题。可能原因包括:Canvas绘制逻辑频繁触发重绘,粒子数量过多超出渲染负载,或未合理使用离屏Canvas优化。建议检查绘制代码,减少不必要的重绘操作,并利用HarmonyOS提供的图形引擎接口进行性能优化。

根据你的代码,问题主要出在Canvas上下文的管理和动画循环的实现方式上。在HarmonyOS Next中,Canvas的RenderingContext2D对象必须在Canvas组件的onReady回调中获取,而不能在组件构建时直接创建。

以下是导致卡顿和系统重启的关键问题及修改建议:

  1. Canvas上下文获取时机错误

    // 错误:在组件构建时创建context
    private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);
    
    // 正确:应该在onReady回调中获取
    .onReady(() => {
      this.context = this.controller.getContext();
    })
    
  2. 动画循环使用setInterval: 在HarmonyOS中,频繁的setInterval会导致性能问题。建议使用requestAnimationFrame

    private animate() {
      this.updatePhysics();
      this.draw();
      this.animationId = requestAnimationFrame(() => this.animate());
    }
    
  3. 粒子数量控制: 你的代码中MAX_PARTICLES设置为10,但爆炸时生成70-120个粒子,这会导致粒子数组无限增长。需要修正粒子数量限制逻辑。

  4. 颜色处理问题randomColor()方法中的索引计算可能返回负数,导致数组访问越界。

建议重构代码结构,确保:

  • Canvas上下文在onReady中正确获取
  • 使用requestAnimationFrame实现动画循环
  • 严格控制粒子生命周期和数量
  • 修复数组越界等潜在错误

这些修改应该能解决卡顿和系统重启的问题。

回到顶部