鸿蒙NEXT开发案例:垃圾分类
【引言】
随着人们对环保意识的提升,正确分类垃圾成为了一个重要的社会议题。本文将探讨一个基于HarmonyOS NEXT的垃圾分类小游戏,该游戏利用了ArkUI框架提供的动画功能以及一些简单的算法来实现交互式的学习体验。
【环境准备】
电脑系统:windows 10
开发工具:DevEco Studio NEXT Beta1 Build Version: 5.0.3.806
工程版本:API 12
真机:Mate 60 Pro
语言:ArkTS、ArkUI
【算法分析】
1、动画算法:Spring Curve
为了增强用户体验,游戏中采用了弹簧曲线(springCurve)来模拟现实世界中的物理现象。springCurve是一种常用的缓动曲线,可以用来模拟带有阻尼的弹簧效果。在游戏中,当用户将垃圾物品拖拽到正确的分类区域时,动画会产生一种自然的回弹效果,使得整个过程更加生动有趣。
animateToImmediately({
duration:800,
curve: curves.springCurve(0, 20, 90, 20),
// 其他配置
});
这里curves.springCurve()接受四个参数,分别代表初始偏移量、振幅、周期和阻尼系数,通过这些参数可以控制动画的弹性效果。
2、随机算法:Fisher-Yates洗牌算法
为了保持游戏的新鲜感,每次启动游戏时都需要随机打乱垃圾物品的顺序。游戏中采用的是Fisher-Yates洗牌算法,这是一种在线性时间内生成一个有限集合的随机排列的方法。
shuffleItems() {
for (let i = this.garbageItems.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
let temp = this.garbageItems[i];
this.garbageItems[i] = this.garbageItems[j];
this.garbageItems[j] = temp;
}
}
Fisher-Yates洗牌算法的核心思想是从最后一个元素开始,逐个向前交换元素的位置,直到第一个元素为止。每次选择一个从当前位置到数组末尾之间的随机元素与当前位置的元素交换位置。
3、粒子动画:Particle System
除了上述的动画,游戏中还引入了粒子系统来增强视觉效果。粒子系统是计算机图形学中的一种特效技术,它可以用来模拟火焰、水流、爆炸等效果。在这个游戏中,当用户成功分类垃圾时,会在屏幕中央产生粒子效果,以此来庆祝用户的正确分类。
if (this.showAnimation) {
Particle({
particles: [
...
【完整代码】
import { curves } from '@kit.ArkUI'; // 导入ArkUI工具包中的曲线模块
// 定义垃圾项目类
class GarbageItem {
name: string; // 垃圾名称
type: number; // 垃圾类型
description?: string; // 垃圾描述,可选
// 构造函数初始化垃圾项目
constructor(name: string, type: number, description?: string) {
this.name = name; // 设置垃圾名称
this.type = type; // 设置垃圾类型
this.description = description || ""; // 设置垃圾描述,默认为空字符串
}
}
// 定义垃圾类别类
class GarbageCategory {
name: string; // 类别名称
type: number; // 类别类型
color: string; // 类别颜色
description: string; // 类别描述
// 构造函数初始化垃圾类别
constructor(name: string, type: number, color: string, description: string) {
this.name = name; // 设置类别名称
this.type = type; // 设置类别类型
this.color = color; // 设置类别颜色
this.description = description; // 设置类别描述
}
}
// 使用组件装饰器定义一个名为Index的应用入口
@Entry
@Component
struct Index {
// 定义状态变量
@State currentQuestionIndex: number = 0; // 当前题目索引
@State quizResults: string[] = []; // 测验结果数组
@State totalScore: number = 0; // 总得分
@State showAnimation: boolean = false; // 是否显示动画
@State scaleOptions: ScaleOptions = { x: 1, y: 1 }; // 缩放选项
@State itemXPosition: number = 0; // 物品X轴位置
@State itemOpacity: number = 1.0; // 物品透明度
// 初始化垃圾类别数组
@State garbageCategories: GarbageCategory[] = [
new GarbageCategory("有害垃圾", 0, "#e2413f", "对人体健康或自然环境可能造成直接或潜在危害的生活垃圾"), // 创建有害垃圾类别
new GarbageCategory("可回收物", 1, "#1c6bb5", "适宜回收和资源利用的物品"), // 创建可回收物类别
new GarbageCategory("厨余垃圾", 2, "#4ca84e", "上海称湿垃圾,易腐烂的、含有机质的生活垃圾"), // 创建厨余垃圾类别
new GarbageCategory("其他垃圾", 3, "#5f5f5f", "上海称干垃圾,不能归类于以上三类的生活垃圾"),// 创建其他垃圾类别
];
// 初始化垃圾项目数组
@State garbageItems: GarbageItem[] = [
new GarbageItem("菜帮菜叶", 2), // 创建厨余垃圾项目
new GarbageItem("剩菜剩饭", 2), // 创建厨余垃圾项目
new GarbageItem("过期食品", 2), // 创建厨余垃圾项目
new GarbageItem("瓜果皮壳", 2), // 创建厨余垃圾项目
new GarbageItem("鱼骨鱼刺", 2), // 创建厨余垃圾项目
new GarbageItem("鸡蛋及蛋壳", 2), // 创建厨余垃圾项目
new GarbageItem("残枝落叶", 2), // 创建厨余垃圾项目
new GarbageItem("茶叶渣", 2), // 创建厨余垃圾项目
new GarbageItem("酒瓶", 1), // 创建可回收物项目
new GarbageItem("玻璃杯", 1), // 创建可回收物项目
new GarbageItem("调味瓶", 1), // 创建可回收物项目
new GarbageItem("图书", 1), // 创建可回收物项目
new GarbageItem("打印纸", 1), // 创建可回收物项目
new GarbageItem("信封", 1), // 创建可回收物项目
new GarbageItem("易拉罐", 1), // 创建可回收物项目
new GarbageItem("金属刀具", 1), // 创建可回收物项目
new GarbageItem("奶粉桶", 1), // 创建可回收物项目
new GarbageItem("衣服裤子", 1), // 创建可回收物项目
new GarbageItem("毛绒玩具", 1), // 创建可回收物项目
new GarbageItem("鞋", 1), // 创建可回收物项目
new GarbageItem("饮料瓶", 1), // 创建可回收物项目
new GarbageItem("塑料盆", 1), // 创建可回收物项目
new GarbageItem("食用油桶", 1), // 创建可回收物项目
new GarbageItem("洗衣机", 1), // 创建可回收物项目
new GarbageItem("电烤箱", 1), // 创建可回收物项目
new GarbageItem("电视机", 1), // 创建可回收物项目
new GarbageItem("充电电池", 0), // 创建有害垃圾项目
new GarbageItem("废含汞荧光灯管", 0), // 创建有害垃圾项目
new GarbageItem("过期药品及其包装物", 0), // 创建有害垃圾项目
new GarbageItem("油漆桶", 0), // 创建有害垃圾项目
new GarbageItem("血压计", 0), // 创建有害垃圾项目
new GarbageItem("废水银温度计", 0), // 创建有害垃圾项目
new GarbageItem("杀虫喷雾罐", 0), // 创建有害垃圾项目
new GarbageItem("废X光片等感光胶片", 0), // 创建有害垃圾项目
new GarbageItem("食品袋", 3), // 创建其他垃圾项目
new GarbageItem("大棒骨", 3), // 创建其他垃圾项目
new GarbageItem("创可贴", 3), // 创建其他垃圾项目
new GarbageItem("污损塑料袋", 3), // 创建其他垃圾项目
new GarbageItem("烟屁", 3), // 创建其他垃圾项目
new GarbageItem("陶瓷碎片", 3), // 创建其他垃圾项目
new GarbageItem("餐巾纸", 3, "厕纸、卫生纸遇水即溶,不算可回收的“纸张”,类似的还有烟盒等。"), // 创建其他垃圾项目
new GarbageItem("卫生纸", 3, "厕纸、卫生纸遇水即溶,不算可回收的“纸张”,类似的还有烟盒等。"),// 创建其他垃圾项目
];
// 在组件即将出现时重置测验
aboutToAppear(): void {
this.resetQuiz(); // 调用重置测验方法
}
// 重置测验状态
resetQuiz() {
this.quizResults = []; // 清空测验结果
this.totalScore = 0; // 清零总得分
this.currentQuestionIndex = 0; // 重置当前题目索引
this.shuffleItems(); // 打乱垃圾项目顺序
}
// 打乱垃圾项目顺序
shuffleItems() {
for (let i = this.garbageItems.length - 1; i > 0; i--) { // 从后往前遍历垃圾项目
const j = Math.floor(Math.random() * (i + 1)); // 随机索引
let temp = this.garbageItems[i]; // 临时存储当前项目
this.garbageItems[i] = this.garbageItems[j]; // 交换位置
this.garbageItems[j] = temp; // 交换位置
}
}
// 检查用户选择的答案
checkAnswer(categoryType: number) {
const currentItem = this.garbageItems[this.currentQuestionIndex]; // 获取当前垃圾项目
// 添加测验结果,包含项目名称和用户选择的类别
this.quizResults.push(`${currentItem.name}(${this.garbageCategories[categoryType].name})【${currentItem.type ===
categoryType ? "✔" : this.garbageCategories[currentItem.type].name}】`);
if (currentItem.type === categoryType) { // 如果答案正确
this.totalScore += 10; // 加十分
}
this.currentQuestionIndex++; // 进入下一个题目
if (this.currentQuestionIndex >= 10) { // 如果完成了十个题目
this.displayResults(); // 显示结果
this.resetQuiz(); // 重置测验
}
}
// 显示测验结果
displayResults() {
let sheets: SheetInfo[] = []; // 初始化结果页面列表
for (let i = 0; i < this.quizResults.length; i++) { // 循环添加每一个结果
sheets.push({
title: this.quizResults[i], // 设置标题为测验结果
action: () => { // 不执行任何操作
}
});
}
this.getUIContext().showActionSheet({
// 显示结果页
title: '成绩单', // 标题
message: `总分数:${this.totalScore}`, // 分数信息
confirm: {
// 确认按钮
defaultFocus: true, // 默认焦点
value: '我知道了', // 按钮文本
action: () => { // 点击后的动作
}
},
onWillDismiss: () => { // 关闭前的动作
},
alignment: DialogAlignment.Center, // 对齐方式为中心
sheets: sheets // 结果页面列表
});
}
// 构建用户界面
build() {
Column() { // 创建列布局
Text(`垃圾分类测验:${this.currentQuestionIndex + 1}/10`)// 显示当前题目序号
.fontSize('30lpx')// 设置字体大小
.margin(20); // 设置外边距
Stack() { // 创建堆栈布局
Text(`${this.garbageItems[this.currentQuestionIndex].name}`)// 显示当前垃圾项目名称
.textAlign(TextAlign.Center)// 居中对齐
.width('130lpx')// 设置宽度
.height('130lpx')// 设置高度
.border({ width: 1 })// 设置边框宽度
.borderRadius(5)// 设置圆角半径
.fontColor(Color.White)// 设置字体颜色
.backgroundColor(Color.Orange)// 设置背景颜色
.fontSize('20lpx')// 设置字体大小
.padding(2)// 设置内边距
.scale(this.scaleOptions); // 设置缩放比例
if (this.showAnimation) { // 如果显示动画
Particle({
// 创建粒子效果
particles: [// 初始化粒子数组
{
emitter: {
// 粒子发射器配置
particle: {
// 粒子类型配置
type: ParticleType.POINT, // 粒子类型为点
config: {
// 配置
radius: 5 // 点的半径
},
count: 50, // 粒子数量
lifetime: -1, // 生命周期
lifetimeRange: -1 // 生命周期范围
},
emitRate: 100, // 发射速率
position: ['25%', 0], // 发射位置
size: ['100lpx', '100lpx'], // 发射器大小
shape: ParticleEmitterShape.RECTANGLE // 发射器形状为矩形
},
color: {
// 粒子颜色配置
range: [Color.Orange, Color.Orange], // 颜色范围
updater: {
// 更新器配置
type: ParticleUpdater.CURVE, // 变化方式为曲线变化
config: [// 配置项
{
from: Color.Orange, // 起始颜色
to: Color.Orange, // 终止颜色
startMillis: 0, // 开始时间
endMillis: -1, // 结束时间
curve: Curve.FastOutLinearIn // 曲线类型
}
]
}
},
scale: {
// 粒子大小配置
range: [0.8, 1.2], // 大小范围
updater: {
// 更新器配置
type: ParticleUpdater.CURVE, // 变化方式为曲线变化
config: [// 配置项
{
from: 1.0, // 起始大小
to: 1.0, // 终止大小
startMillis: 0, // 开始时间
endMillis: -1, // 结束时间
curve: Curve.EaseIn
}
]
}
},
// 粒子加速度配置
acceleration: {
speed: {
range: [8000, 10000], // 向下减速,模拟重力
updater: {
type: ParticleUpdater.RANDOM, // 加速度线性变化
config: [400, 500]
}
},
angle: {
range: [90, 90] // 方向固定向下
}
},
}
]
}).width("100%").height("100%");
}
}
.width('150lpx') // 设置列布局宽度为150像素
.height('300lpx') // 设置列布局高度为300像素
.align(Alignment.Top) // 对齐方式为顶部
.translate({ x: `${this.itemXPosition}lpx`, y: 0 }); // 平移布局,X轴位置根据itemXPosition动态调整
Row() { // 创建行布局
ForEach(this.garbageCategories, (category: GarbageCategory) => { // 遍历垃圾类别数组
Column() { // 创建列布局
Text(category.name)// 显示类别名称
.fontColor(Color.White)// 设置字体颜色为白色
.fontSize('30lpx')// 设置字体大小
.padding(5); // 设置内边距
Divider(); // 添加分隔线
Text(category.description)// 显示类别描述
.fontColor(Color.White)// 设置字体颜色为白色
.fontSize('28lpx')// 设置字体大小
.padding(5); // 设置内边距
}
.backgroundColor(category.color) // 设置背景颜色为类别颜色
.height('280lpx') // 设置高度
.width("24%") // 设置宽度占比
.border({ width: 1 }) // 设置边框宽度
.borderRadius(5) // 设置圆角半径
.margin({ left: 1, right: 1 }) // 设置外边距
.clickEffect({ scale: 0.8, level: ClickEffectLevel.MIDDLE }) // 点击效果
.onClick(() => { // 点击事件
if (this.showAnimation) { // 如果正在显示动画
return // 不执行后续操作
}
this.showAnimation = true; // 设置显示动画为true
let itemX: number = 0; // 初始化物品X轴位置
if (category.type == 0) { // 如果类别为有害垃圾
itemX = -270; // 设置X轴位置
} else if (category.type == 1) { // 如果类别为可回收物
itemX = -90; // 设置X轴位置
} else if (category.type == 2) { // 如果类别为厨余垃圾
itemX = 90; // 设置X轴位置
} else if (category.type == 3) { // 如果类别为其他垃圾
itemX = 270; // 设置X轴位置
}
animateToImmediately({
// 立即执行动画
duration: 200, // 持续时间200毫秒
onFinish: () => { // 动画完成后的回调
animateToImmediately({
// 立即执行动画
duration: 800, // 持续时间800毫秒
curve: curves.springCurve(0, 20, 90, 20), // 设置弹簧曲线动画
onFinish: () => { // 动画完成后的回调
animateToImmediately({
// 立即执行动画
duration: 200, // 持续时间200毫秒
onFinish: () => { // 动画完成后的回调
this.itemXPosition = 0; // 重置物品X轴位置
this.checkAnswer(category.type); // 检查用户选择的答案
this.showAnimation = false; // 设置显示动画为false
}
}, () => { // 动画更新回调
this.itemXPosition = 0; // 重置物品X轴位置
this.scaleOptions = { x: 1.0, y: 1.0 }; // 重置缩放选项
})
}
}, () => { // 动画更新回调
this.scaleOptions = { x: 1.3, y: 1.3 }; // 设置缩放选项
})
}
}, () => { // 动画更新回调
this.itemXPosition = itemX; // 设置物品X轴位置
})
});
});
}
Button('重新开始')// 创建一个按钮,文本为“重新开始”
.clickEffect({ level: ClickEffectLevel.LIGHT })// 设置点击效果
.margin({ top: 50 })// 设置按钮上边距为50
.onClick(() => { // 点击事件
this.resetQuiz(); // 调用重置测验方法
}); // 结束按钮定义
}.width('100%'); // 设置列布局宽度为100%
} // 结束build方法
} // 结束Index类