鸿蒙 ArkUI 实现敲木鱼小游戏

ops/2025/3/1 22:36:31/

敲木鱼是一款具有禅意的趣味小游戏,本文将通过鸿蒙 ArkUI 框架的实现代码,逐步解析其核心技术点,包括动画驱动状态管理音效震动反馈等。

一、架构设计与工程搭建

1.1 项目结构解析

完整项目包含以下核心模块:

├── entry/src/main/ets/
│   ├── components/         // 自定义组件库
│   ├── model/              // 数据模型(如StateArray)
│   ├── pages/              // 页面组件(WoodenFishGame.ets)
│   └── resources/          // 多媒体资源(木鱼图标、音效)

通过模块化设计分离 UI层(pages)、逻辑层(model)、资源层(resources),符合鸿蒙应用开发规范。

1.2 组件化开发模式

使用 @Component 装饰器创建独立可复用的 UI 单元,@Entry 标记为页面入口。关键状态通过 @State 管理:

@Entry
@Component
struct WoodenFishGame {@State count: number = 0;                // 功德计数器@State scaleWood: number = 1;           // 木鱼缩放系数@State rotateWood: number = 0;          // 木鱼旋转角度@State animateTexts: Array<StateArray> = []; // 动画队列private audioPlayer?: media.AVPlayer;    // 音频播放器实例private autoPlay: boolean = false;       // 自动敲击标志位
}

@State 实现了 响应式编程:当变量值变化时,ArkUI 自动触发关联 UI 的重新渲染。

二、动画系统深度解析

2.1 木鱼敲击复合动画

动画分为 按压收缩(100ms)和 弹性恢复(200ms)两个阶段,通过 animateTo 实现平滑过渡:

playAnimation() {// 第一阶段:快速收缩+左旋animateTo({duration: 100, curve: Curve.Friction // 摩擦曲线模拟物理阻力}, () => {this.scaleWood = 0.9; // X/Y轴缩放到90%this.rotateWood = -2; // 逆时针旋转2度});// 第二阶段:弹性恢复setTimeout(() => {animateTo({duration: 200,curve: Curve.Linear // 线性恢复保证流畅性}, () => {this.scaleWood = 1; this.rotateWood = 0;});}, 100); // 延迟100ms衔接动画
}

曲线选择

  • Curve.Friction 模拟木槌敲击时的瞬间阻力
  • Curve.Linear 确保恢复过程无加速度干扰

2.2 功德文字飘浮动画

采用 动态数组管理 + 唯一ID标识 实现多实例独立控制:

countAnimation() {const animId = new Date().getTime(); // 时间戳生成唯一ID// 添加新动画元素this.animateTexts = [...this.animateTexts, { id: animId, opacity: 1, offsetY: 20 }];// 启动渐隐上移动画animateTo({duration: 800,curve: Curve.EaseOut // 缓出效果模拟惯性}, () => {this.animateTexts = this.animateTexts.map(item => item.id === animId ? { ...item, opacity: 0, offsetY: -100 } : item);});// 动画完成后清理内存setTimeout(() => {this.animateTexts = this.animateTexts.filter(t => t.id !== animId);}, 800); // 与动画时长严格同步
}

关键技术点

  1. 数据驱动:通过修改 animateTexts 数组触发 ForEach 重新渲染
  2. 分层动画opacity 控制透明度,offsetY 控制垂直位移
  3. 内存优化:定时清理已完成动画元素,防止数组膨胀

三、多模态交互实现

3.1 触觉震动反馈

调用 @kit.SensorServiceKit 的振动模块实现触觉反馈:

vibrator.startVibration({type: "time",       // 按时间模式振动duration: 50        // 50ms短震动
}, {id: 0,              // 振动器IDusage: 'alarm'      // 资源使用场景标识
});

参数调优建议

  • 时长:50ms 短震动模拟木鱼敲击的“清脆感”
  • 强度鸿蒙系统自动根据 usage 分配最佳强度等级

3.2 音频播放与资源管理

通过 media.AVPlayer 实现音效播放:

aboutToAppear(): void {media.createAVPlayer().then(player => {this.audioPlayer = player;this.audioPlayer.url = ""; this.audioPlayer.loop = false; // 禁用循环播放});
}// 敲击时重置播放进度
if (this.audioPlayer) {this.audioPlayer.seek(0);    // 定位到0毫秒this.audioPlayer.play();     // 播放音效
}

最佳实践

  1. 预加载资源:在 aboutToAppear 阶段提前初始化播放器
  2. 避免延迟:调用 seek(0) 确保每次点击即时发声
  3. 资源释放:需在 onPageHide 中调用 release() 防止内存泄漏

四、自动敲击功能实现

4.1 定时器与状态联动

通过 Toggle 组件切换自动敲击模式:

// 状态切换回调
Toggle({ type: ToggleType.Checkbox, isOn: false }).onChange((isOn: boolean) => {this.autoPlay = isOn;if (isOn) {this.startAutoPlay();} else {clearInterval(this.intervalId); // 清除指定定时器}});// 启动定时器
private intervalId: number = 0;
startAutoPlay() {this.intervalId = setInterval(() => {if (this.autoPlay) this.handleTap();}, 400); // 400ms间隔模拟人类点击频率
}

关键改进点

  • 使用 intervalId 保存定时器引用,避免 clearInterval() 失效
  • 间隔时间 400ms 平衡流畅度与性能消耗

4.2 线程安全与性能保障

风险点:频繁的定时器触发可能导致 UI 线程阻塞
解决方案

// 在 aboutToDisappear 中清除定时器
aboutToDisappear() {clearInterval(this.intervalId);
}

确保页面隐藏时释放资源,避免后台线程持续运行。

五、UI 布局与渲染优化

5.1 层叠布局与动画合成

使用 Stack 实现多层 UI 元素的叠加渲染:

Stack() {// 木鱼主体(底层)Image($r("app.media.icon_wooden_fish")).width(280).height(280).margin({ top: -10 }).scale({ x: this.scaleWood, y: this.scaleWood }).rotate({ angle: this.rotateWood });// 功德文字(上层)ForEach(this.animateTexts, (item, index) => {Text(`+1`).translate({ y: -item.offsetY * index }) // 按索引错位显示});
}

渲染优化技巧

  • 为静态图片资源添加 fixedSize(true) 避免重复计算
  • 使用 translate 代替 margin 实现位移,减少布局重排

5.2 状态到 UI 的高效绑定

通过 链式调用 实现样式动态绑定:

Text(`功德 +${this.count}`).fontSize(20).fontColor('#4A4A4A').margin({ top: 20 + AppUtil.getStatusBarHeight() // 动态适配刘海屏})

适配方案

  • AppUtil.getStatusBarHeight() 获取状态栏高度,避免顶部遮挡
  • 使用鸿蒙弹性布局(Flex)自动适应不同屏幕尺寸

六、完整代码

import { media } from '@kit.MediaKit';
import { vibrator } from '@kit.SensorServiceKit';
import { AppUtil, ToastUtil } from '@pura/harmony-utils';
import { StateArray } from '../model/HomeModel';@Entry
@Component
struct WoodenFishGame {@State count: number = 0;@State scaleWood: number = 1;@State rotateWood: number = 0;audioPlayer?: media.AVPlayer;// 添加自动敲击功能autoPlay: boolean = false;// 新增状态变量@State animateTexts: Array<StateArray> = []aboutToAppear(): void {media.createAVPlayer().then(player => {this.audioPlayer = playerthis.audioPlayer.url = ""})}startAutoPlay() {setInterval(() => {if (this.autoPlay) {this.handleTap();}}, 400);}// 敲击动画playAnimation() {animateTo({duration: 100,curve: Curve.Friction}, () => {this.scaleWood = 0.9;this.rotateWood = -2;});setTimeout(() => {animateTo({duration: 200,curve: Curve.Linear}, () => {this.scaleWood = 1;this.rotateWood = 0;});}, 100);}// 敲击处理handleTap() {this.count++;this.playAnimation();this.countAnimation();// 在handleTap中添加:vibrator.startVibration({type: "time",duration: 50}, {id: 0,usage: 'alarm'});// 播放音效if (this.audioPlayer) {this.audioPlayer.seek(0);this.audioPlayer.play();}}countAnimation(){// 生成唯一ID防止动画冲突const animId = new Date().getTime()// 初始化动画状态this.animateTexts = [...this.animateTexts, {id: animId, opacity: 1, offsetY: 20}]// 执行动画animateTo({duration: 800,curve: Curve.EaseOut}, () => {this.animateTexts = this.animateTexts.map(item => {if (item.id === animId) {return { id:item.id, opacity: 0, offsetY: -100 }}return item})})// 动画完成后清理setTimeout(() => {this.animateTexts = this.animateTexts.filter(t => t.id !== animId)}, 800)}build() {Column() {// 计数显示Text(`功德 +${this.count}`).fontSize(20).margin({ top: 20+AppUtil.getStatusBarHeight() })// 木鱼主体Stack() {// 可敲击部位Image($r("app.media.icon_wooden_fish")).width(280).height(280).margin({ top: -10 }).scale({ x: this.scaleWood, y: this.scaleWood }).rotate({ angle: this.rotateWood }).onClick(() => this.handleTap())// 功德文字动画容器ForEach(this.animateTexts, (item:StateArray,index) => {Text(`+1`).fontSize(24).fontColor('#FFD700').opacity(item.opacity).margin({ top: -100}) // 初始位置调整.translate({ y: -item.offsetY*index }) // 使用translateY实现位移.animation({ duration: 800, curve: Curve.EaseOut })})}.margin({ top: 50 })Row(){// 自动敲击开关(扩展功能)Toggle({ type: ToggleType.Checkbox, isOn: false }).onChange((isOn: boolean) => {// 可扩展自动敲击功能this.autoPlay = isOn;if (isOn) {this.startAutoPlay();} else {clearInterval();}})Text("自动敲击")}.alignItems(VerticalAlign.Center).justifyContent(FlexAlign.Center).width("100%").position({bottom:100})}.width('100%').height('100%').backgroundColor('#f0f0f0')}
}

http://www.ppmy.cn/ops/162341.html

相关文章

linux中安装部署Jenkins,成功构建springboot项目详细教程

参考别人配置Jenkins的git地址为https&#xff0c;无法连上github拉取项目&#xff0c;所以本章节介绍通过配置SSH地址来连github拉取项目 目录&#xff1a; 1、springboot项目 1.1 创建名为springcloudproject的springboot项目工程 1.2 已将工程上传到github中&#xff0c;g…

linux学习笔记2

认知权限信息 -或d或l:-表示文件,d表示文件夹,l表示软链接 rwx:r表示读权限,w表示写权限,x表示执行权限 文件类型所属用户权限所属用户组权限其它用户权限 修改权限chmod [-R] 权限 文件或文件夹 -R对文件夹内的全部内容应用同样的操作 chmod -R urwx,grx,ox 可使用简单表示r4…

【Prometheus】prometheus服务发现与relabel原理解析与应用实战

✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。 🏆《博客》:Python全…

Vue 3指令全解析:内置指令与自定义指令实战指南

Vue指令是模板语法的核心武器&#xff0c;它们以v-前缀的形式为HTML元素添加特殊功能。本文将深入探讨Vue 3中的指令系统&#xff0c;覆盖10个核心指令的妙用&#xff0c;并手把手教你打造专属自定义指令。 一、Vue指令基础认知 指令本质上是DOM操作的语法糖&#xff0c;它们&…

从 Spring Boot 2 升级到 Spring Boot 3 的终极指南

一、升级前的核心准备 1. JDK 版本升级 Spring Boot 3 强制要求 Java 17 及以上版本。若当前项目使用 Java 8 或 11&#xff0c;需按以下步骤操作&#xff1a; 安装 JDK 17&#xff1a;从 Oracle 或 OpenJDK 官网下载&#xff0c;配置环境变量&#xff08;如 JAVA_HOME&…

记一次命令行启动springboot项目的问题 java -jar的问题

错误的写法 java -jar ruoyi-admin.jar -Dloader.path.\lib 正确的写法 java -Dloader.path./lib -jar ruoyi-admin.jar 或者 java -jar -Dloader.path./lib ruoyi-admin.jar -Dloader.path必须卸载 -jar ruoyi-admin.jar之前&#xff0c;其实我试过了-Dloader.path命令只要…

Ubuntu 下 nginx-1.24.0 源码分析 - ngx_init_cycle 函数 - 详解(11)

详解&#xff08;11&#xff09; 初始化配置解析上下文 senv environ;ngx_memzero(&conf, sizeof(ngx_conf_t));/* STUB: init array ? */conf.args ngx_array_create(pool, 10, sizeof(ngx_str_t));if (conf.args NULL) {ngx_destroy_pool(pool);return NULL;}conf.te…

Go语言学习笔记(五)

文章目录 十八、go操作MySQL、RedisMySQLRedis 十九、泛型泛型函数泛型类型泛型约束泛型特化泛型接口 二十、workspaces核心概念示例 二十一、模糊测试 十八、go操作MySQL、Redis MySQL package mainimport ("database/sql""errors""fmt"_ &qu…