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
【问题定位】
观察代码发现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
根据你的代码,问题主要出在Canvas上下文的管理和动画循环的实现方式上。在HarmonyOS Next中,Canvas的RenderingContext2D对象必须在Canvas组件的onReady回调中获取,而不能在组件构建时直接创建。
以下是导致卡顿和系统重启的关键问题及修改建议:
-
Canvas上下文获取时机错误:
// 错误:在组件构建时创建context private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings); // 正确:应该在onReady回调中获取 .onReady(() => { this.context = this.controller.getContext(); }) -
动画循环使用setInterval: 在HarmonyOS中,频繁的setInterval会导致性能问题。建议使用
requestAnimationFrame:private animate() { this.updatePhysics(); this.draw(); this.animationId = requestAnimationFrame(() => this.animate()); } -
粒子数量控制: 你的代码中
MAX_PARTICLES设置为10,但爆炸时生成70-120个粒子,这会导致粒子数组无限增长。需要修正粒子数量限制逻辑。 -
颜色处理问题:
randomColor()方法中的索引计算可能返回负数,导致数组访问越界。
建议重构代码结构,确保:
- Canvas上下文在
onReady中正确获取 - 使用
requestAnimationFrame实现动画循环 - 严格控制粒子生命周期和数量
- 修复数组越界等潜在错误
这些修改应该能解决卡顿和系统重启的问题。


