倒计时不只是数字递减——配合触觉反馈,可以为放松体验增加一层"物理感",让用户即使闭着眼睛也能感知时间的流动。
一、设计理念:让用户不必看屏幕
白噪音模块的核心使用场景是:用户闭上眼睛,沉浸在放松状态中。在这种场景下,屏幕上的视觉信息对用户是"不可见"的。
传统的解决方式是音频提示("叮"的一声),但 E-Brufen 当前没有音频播放能力。于是我们转向了触觉通道:
视觉通道(眼睛闭着) ↓ 不可用 听觉通道(无音频播放) ↓ 不可用 触觉通道 ← E-Brufen 的主反馈通道! ↓ 用户感知到震动 → 知道时间在流逝 → 保持放松状态二、定时器选项设计
2.1 预设时长
finalList<int>_durations=[15,30,60,90];Wrap(spacing:8,children:[..._durations.map((d)=>ChoiceChip(label:Text('$d分'),selected:_remainingSec>0&&_remainingSec~/60==d,onSelected:(_)=>setState((){_remainingSec=d*60;widget.settings.timerDuration=d;// 持久化偏好}),)),ChoiceChip(label:constText('不限'),selected:_remainingSec==0,onSelected:(_)=>setState(()=>_remainingSec=0),),])设计考量:
| 时长 | 适用场景 | 触觉反馈次数 |
|---|---|---|
| 15 分钟 | 快速放松、课间休息 | 14 次轻触 + 1 次重触 |
| 30 分钟 | 午休小憩 | 29 次轻触 + 1 次重触 |
| 60 分钟 | 深度放松 | 59 次轻触 + 1 次重触 |
| 90 分钟 | 完整睡眠周期 | 89 次轻触 + 1 次重触 |
| 不限 | 背景白噪音(办公等) | 无(不启动 Timer) |
⚠️ "不限"选项不启动 Timer,
_remainingSec == 0作为特殊标记。此时_startCountdown()直接 return,不会消耗系统资源。
三、Timer.periodic 倒计时实现
3.1 核心代码
void_startCountdown(){_countdown?.cancel();// 取消旧的 Timer(防重复)if(_remainingSec<=0)return;// "不限"模式直接返回_countdown=Timer.periodic(constDuration(seconds:1),(timer){if(!_isPlaying){// 手动停止 → 取消计时timer.cancel();return;}setState((){_remainingSec--;if(_remainingSec<=0){// 时间到_onTimerDone();timer.cancel();return;}// ⏰ 每分钟一次轻触觉——"无声的报时"if(_remainingSec%60==0){HapticFeedback.lightImpact();}});});}3.2 状态流程图
用户点击播放 ↓ _startCountdown() → Timer.periodic(每秒) ↓ 每秒回调: ├─ !_isPlaying? → timer.cancel() (用户手动停止) ├─ _remainingSec == 0? → _onTimerDone() (自然结束) ├─ _remainingSec % 60 == 0? → lightImpact() (分钟标记) └─ _remainingSec-- (继续倒计时)
★ Insight ─────────────────────────────────────_remainingSec % 60 == 0是"无声提醒"的核心。用户闭眼放松时,每分钟一次轻触觉让他知道"又过了一分钟",而不需要睁眼看屏幕。这是 E-Brufen 最精妙的设计细节之一——它把"必须看"变成了"可以感受"。─────────────────────────────────────────────────
四、完成时的双重反馈
void_onTimerDone(){setState((){_isPlaying=false;});_pulse.stop();_pulse.reset();HapticFeedback.heavyImpact();// 强烈的"唤醒"震动if(mounted){ScaffoldMessenger.of(context).showSnackBar(constSnackBar(content:Text('时间到,放松结束 🌙'),duration:Duration(seconds:2),),);}}双重信号确保用户一定感知到结束:
- heavyImpact()— 强烈的触觉(即使在口袋中也能感知)
- SnackBar— 视觉确认(如果屏幕可见)
⚠️
if (mounted)检查是必须的——Timer 回调可能在 Widget 已销毁后触发(如用户快速返回首页),不检查会导致setState() called after dispose()崩溃。
五、HapticFeedback 类型详解
Flutter 提供四种触觉反馈强度:
| 类型 | 强度 | 持续时间 | E-Brufen 中的使用 |
|---|---|---|---|
lightImpact() | 轻 | ~10ms | ✅ 每分钟标记 |
mediumImpact() | 中 | ~15ms | 可用于场景切换确认 |
heavyImpact() | 重 | ~20ms | ✅ 定时结束唤醒 |
selectionClick() | 极轻 | ~5ms | 可用于 emoji 选择反馈 |
选择lightImpact而非selectionClick的原因:selectionClick太轻,在口袋或桌面上可能感知不到。lightImpact提供了足够的感知强度而不至于"惊醒"放松中的用户。
六、播放/停止的对称控制
void_togglePlay(){setState((){_isPlaying=!_isPlaying;if(_isPlaying){_pulse.repeat(reverse:true);// 启动脉冲动画_startCountdown();// 启动倒计时}else{_pulse.stop();// 停止动画_pulse.reset();_countdown?.cancel();// 取消倒计时}});}一对对称操作:播放时同时启动动画和 Timer,停止时同时取消两者。这确保了动画和计时永远同步。
七、资源释放
@overridevoiddispose(){_pulse.dispose();// 释放 AnimationController_countdown?.cancel();// 取消 Timer(防止内存泄漏)super.dispose();}释放顺序:先dispose动画(可能有回调引用 Timer),再cancelTimer。两个异步资源都要清理——漏掉任何一个都会导致内存泄漏。
八、时间格式化
String_formatTime(int s){finalm=s~/60;finalsec=s%60;return'${m.toString().padLeft(2, '0')}:${sec.toString().padLeft(2, '0')}';}// 03:45, 00:30, 01:00上一篇:白噪音场景的视觉设计
下一篇:ChangeNotifier 模式:轻量级状态管理
作者简介:E-Brufen Dev,Flutter & 鸿蒙开发者。触觉反馈设计基于 HapticFeedback API 在鸿蒙 6.0 真机上的实测体验,项目地址:AtomGit - E-Brufen。