敲木魚是一款具有禪意的趣味小游戲,本文將通過鴻蒙 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); // 與動畫時長嚴格同步
}
關鍵技術點:
- 數據驅動:通過修改
animateTexts
數組觸發 ForEach 重新渲染 - 分層動畫:
opacity
控制透明度,offsetY
控制垂直位移 - 內存優化:定時清理已完成動畫元素,防止數組膨脹
三、多模態交互實現
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(); // 播放音效
}
最佳實踐:
- 預加載資源:在
aboutToAppear
階段提前初始化播放器 - 避免延遲:調用
seek(0)
確保每次點擊即時發聲 - 資源釋放:需在
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')}
}