一、技术与禅意的奇妙碰撞
(一)观察者模式下的状态管理:数据驱动的交互灵魂
less
@ObservedV2 // 观察者模式装饰器,自动追踪状态变化
class Cell {
value: string = '功德+1';
@Trace opacity: number = 0; // 透明度追踪
@Trace y: number = 0; // 位移追踪
}
通过@ObservedV2
装饰器,Cell
类实现数据响应式:当opacity
或y
属性变化时,界面自动触发重绘。这种设计模式将「功德文字飘升」的视觉效果转化为数据驱动的状态迁移,使代码逻辑与UI表现解耦,提升可维护性。
(二)功德累加的双模式实现:手动点击与自动挂机的交互平衡
scss
@State autoClick: boolean = false; // 自动点击开关
@State time: number = 0; // 定时器句柄
// 手动点击事件
Image($r('app.media.Snipaste'))
.onClick(() => {
this.totalMerit += 1; // 功德+1
this.triggerAnimation(); // 触发文字飘升动画
})
// 自动点击逻辑
Toggle({ type: ToggleType.Switch, isOn: this.autoClick })
.onChange((isOn) => {
isOn ? this.startAutoClick() : this.stopAutoClick();
})
通过Toggle
开关组件实现手动/自动模式切换:
-
手动模式 :用户点击木鱼图片时,触发
triggerAnimation
生成飘升文字,模拟真实敲木鱼的交互反馈。 -
自动模式 :通过
setInterval
定时器每秒自动执行点击逻辑,解放双手的同时保留视觉反馈,满足不同用户的「积功德」需求。
二、动画美学:用代码绘制数字禅意
功德文字的飘升动画是交互体验的核心,其实现基于ArkUI的animateTo
动画接口:
ini
private triggerAnimation(index: number) {
// 初始状态(底部显示)
animateTo({ duration: 0 }, () => {
this.list[index].y = 0;
this.list[index].opacity = 1;
})
// 结束状态(飘升消失)
.then(() => {
animateTo({ duration: 1000, curve: Curve.EaseOut }, () => {
this.list[index].y = -200;
this.list[index].opacity = 0;
});
});
}
-
双阶段动画 :先瞬间重置文字位置与透明度(
duration: 0
),再通过1秒缓出动画(Curve.EaseOut
)模拟自然飘落轨迹。 -
循环复用 :通过
indexCount % count
取模运算,实现有限数量的Cell
实例循环利用,避免内存泄漏的同时保证动画流畅性。
三、界面设计:暗黑美学与赛博符号的视觉叙事
(一)沉浸式暗黑背景
scss
.backgroundColor(Color.Black) // 全局黑色背景
纯黑背景凸显白色文字与木鱼图标,减少视觉干扰的同时营造「禅修」氛围。功德统计与开关组件通过白色字体形成高对比度视觉焦点。
(二)木鱼图标的交互强化
php
Image($r('app.media.Snipaste'))
.clickEffect({ scale: 0.5, level: ClickEffectLevel.LIGHT }) // 点击缩放效果
.width('300lpx')
.height('300lpx')
通过clickEffect
为木鱼图标添加轻量级点击缩放动效,模拟物理按压反馈。ImageFit.Contain
属性确保图片不失真,保持视觉完整性。
(三)文字队列的堆叠布局
scss
Stack() { // 堆叠容器实现多层文字重叠
ForEach(this.list, (item, index) => {
Text(item.value)
.translate({ y: `${item.y}lpx` }) // 垂直位移
.opacity(item.opacity) // 透明度渐变
})
}
使用Stack
容器使多个功德文字层叠显示,结合translate
与opacity
属性实现文字从底部飘升至消失的动态效果,营造「功德源源不断」的视觉暗示。
四、附源文件
scss
// 观察者模式装饰器
@ObservedV2
class Cell {
value: string = '功德+1';
@Trace opacity: number = 0;
@Trace y: number = 0;
}
// 主入口组件
@Component
export struct play_muyu {
@State list: Cell[] = [];
indexCount: number = 0;
count: number = 10;
@State totalMerit: number = 0; // 总功德数
@State autoClick: boolean = false; // 是否开启自动点击
@State time:number = 0;
// 组件显示时初始化音频
aboutToAppear(): void {
for (let i = 0; i < this.count; i++) {
this.list.push(new Cell());
}
}
build() {
Column() {
// 功德统计和自动点击开关
Row() {
Text(`总功德: ${this.totalMerit}`)
.fontColor(Color.White)
.fontSize('30lpx')
.padding(10)
Toggle({ type: ToggleType.Switch, isOn: this.autoClick })
.width(50)
.height(30)
.onChange((isOn: boolean) => {
this.autoClick = isOn;
if (isOn) {
// 开启自动点击时启动定时器
this.time = setInterval(() => {
if (this.autoClick) {
this.totalMerit += 1;
const index = this.indexCount % this.count;
this.indexCount++;
animateTo({
duration: 0,
onFinish: () => {
animateTo({
duration: 1000,
}, () => {
this.list[index].y = -200;
this.list[index].opacity = 0;
});
}
}, () => {
this.list[index].y = 0;
this.list[index].opacity = 1;
});
}
}, 1000); // 每秒自动点击一次
} else {
// 关闭自动点击时清除定时器
clearInterval(this.time);
}
})
Text(this.autoClick ? '自动点击: 开' : '自动点击: 关')
.fontColor(Color.White)
.fontSize('30lpx')
.padding(10)
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.alignItems(VerticalAlign.Center)
.margin({ top: 20 })
Stack() {
ForEach(this.list, (item: Cell, index: number) => {
Text(item.value)
.fontColor(Color.White)
.fontSize('50lpx')
.translate({ x: 0, y: `${item.y}lpx` })
.opacity(item.opacity)
})
}
.width('300lpx')
.height('300lpx')
.align(Alignment.BottomEnd)
Image($r('app.media.Snipaste'))
.width('300lpx')
.height('300lpx')
.objectFit(ImageFit.Contain)
.clickEffect({ scale: 0.5, level: ClickEffectLevel.LIGHT })
.onClick(() => {
// 增加功德
this.totalMerit += 1;
// 动画逻辑
const index = this.indexCount % this.count;
this.indexCount++;
animateTo({
duration: 0,
onFinish: () => {
animateTo({
duration: 1000,
}, () => {
this.list[index].y = -200
this.list[index].opacity = 0
})
}
}, () => {
this.list[index].y = 0
this.list[index].opacity = 1
})
})
}
.height('100%')
.width('100%')
.backgroundColor(Color.Black)
}
}