AtomGit Flutter 鸿蒙客户端:白噪音场景的视觉设计
没有音频播放?没关系——通过色彩心理学和脉冲动画,E-Brufen 用纯视觉手段打造了沉浸式放松体验。本文详解背后的设计决策与实现细节。

一、设计背景:没有音频的"白噪音"应用?
E-Brufen 的白噪音模块面临一个有趣的约束:鸿蒙 Flutter 的音频播放插件生态尚不成熟。audioplayers、just_audio 等主流音频插件在鸿蒙上均无法正常工作。
面对这个限制,我们有两个选择:
| 方案 | 做法 | 结果 |
|---|---|---|
| A | 等待音频插件适配鸿蒙 | 功能缺失,用户看到的是空白页 |
| B | 用视觉设计填补听觉空缺 | 用户得到完整的放松体验 |
E-Brufen 选择了 B。核心理念是:视觉本身就是一种疗愈手段——色彩、动效、光影的组合可以创造与音频类似的放松效果。
二、四种场景的色彩体系
// lib/pages/soundscape/soundscape_page.dart
const _scenes = [
_SceneData('海边入眠', '🌊', Color(0xFF4FC3F7)), // 天蓝 — 平静、广阔
_SceneData('雨中办公', '🌧️', Color(0xFF81D4FA)), // 浅蓝 — 清新、专注
_SceneData('森林冥想', '🌲', Color(0xFF66BB6A)), // 森林绿 — 自然、疗愈
_SceneData('篝火夜读', '🔥', Color(0xFFFFB74D)), // 暖橙 — 温暖、安全
];
2.1 色彩选择的心理学依据
| 场景 | 颜色 | Hex | 心理暗示 | 适用时刻 |
|---|---|---|---|---|
| 🌊 海边 | 天蓝 | #4FC3F7 | 平静、广阔、凉爽 | 睡前放松 |
| 🌧️ 雨中 | 浅蓝 | #81D4FA | 清新、专注、安宁 | 办公学习 |
| 🌲 森林 | 绿色 | #66BB6A | 自然、生长、疗愈 | 冥想正念 |
| 🔥 篝火 | 橙色 | #FFB74D | 温暖、舒适、安全 | 夜晚阅读 |
★ Insight ─────────────────────────────────────
这些颜色并非随意选择。它们与 E-Brufen 首页三大卡片的渐变色形成呼应:蓝色系对应白噪音卡片、绿色系是品牌主色、橙色系对应情绪日记。用户在首页看到的颜色,进入功能页后会感受到"熟悉的延伸"——这是跨页面的色彩连续性设计。─────────────────────────────────────────────────
三、场景选择器的视觉设计
3.1 选中/未选中的二元对比
GestureDetector(
onTap: () {
setState(() => _selectedScene = i);
widget.settings.selectedScene = s.name; // 持久化偏好
},
child: Container(
width: 100,
margin: const EdgeInsets.only(right: 12),
decoration: BoxDecoration(
color: s.color.withValues(
alpha: active ? 1.0 : 0.5), // 选中全饱和,未选半透明
borderRadius: BorderRadius.circular(16),
border: active
? Border.all(color: Colors.white, width: 2) // 白色边框强化选中
: null,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(s.emoji, style: const TextStyle(fontSize: 28)),
const SizedBox(height: 4),
Text(s.name, style: const TextStyle(
fontSize: 12, color: Colors.white,
fontWeight: FontWeight.w600,
)),
],
),
),
)
视觉对比策略:
| 属性 | 未选中 | 选中 | 目的 |
|---|---|---|---|
| 不透明度 | 50% | 100% | 让选中项"跳出来" |
| 边框 | 无 | 白色 2px solid | 明确的选中标记 |
| 色彩饱和度 | 减半 | 全饱和 | 色彩本身就是状态指示器 |
这种"半透明→全饱和"的对比比传统的单选按钮更优雅——不需要额外的 UI 元素(如 Radio),场景色本身就是选中指示。
四、脉冲动画:用视觉模拟呼吸
4.1 AnimationController 配置
class _SoundscapePageState extends State<SoundscapePage>
with SingleTickerProviderStateMixin {
late AnimationController _pulse;
void initState() {
super.initState();
_pulse = AnimationController(
vsync: this,
duration: const Duration(seconds: 2), // 2秒一个完整呼吸周期
);
}
}
为什么是 2 秒?
- 成年人静息呼吸频率约 12-20 次/分钟
- 2 秒周期 = 15 次/分钟 → 缓慢放松状态
- 1 秒太急促(焦虑感),4 秒太慢(沉闷感)
4.2 .repeat(reverse: true) 的关键作用
void _togglePlay() {
if (_isPlaying) {
_pulse.repeat(reverse: true); // 0→1→0→1... 平滑往返
} else {
_pulse.stop();
_pulse.reset(); // 回到初始位置,避免"跳闪"
}
}
| 方法 | 动画轨迹 | 视觉效果 |
|---|---|---|
.repeat() |
0→1→(跳回0)→1→… | 不连续的"卡顿"感 |
.repeat(reverse: true) |
0→1→0→1→… | 平滑的"呼吸"感 |
4.3 单信号驱动的三重复合动画
AnimatedBuilder(
animation: _pulse,
builder: (context, _) {
return Container(
// ①大小:120→170px (扩张42%)
width: 120 + _pulse.value * 50,
height: 120 + _pulse.value * 50,
decoration: BoxDecoration(
shape: BoxShape.circle,
// ②不透明度:0.25→0.45 (呼吸式明暗)
color: scene.color.withValues(
alpha: _isPlaying ? 0.25 + _pulse.value * 0.2 : 0.08),
// ③光晕:blurRadius 20→50 (脉冲式扩散)
boxShadow: _isPlaying
? [BoxShadow(
color: scene.color.withValues(alpha: 0.3),
blurRadius: 20 + _pulse.value * 30,
spreadRadius: 5,
)]
: [],
),
);
},
)
一个动画值 → 三个视觉属性的同步变化:
| _pulse.value | 0.0 (呼气) | 0.5 | 1.0 (吸气) |
|---|---|---|---|
| 直径 | 120px | 145px | 170px |
| 不透明度 | 0.25 | 0.35 | 0.45 |
| 光晕模糊 | 20px | 35px | 50px |
★ Insight ─────────────────────────────────────
三条属性都来自_pulse.value这一个信号——这是"单信号多输出"的设计模式。好处是三条曲线永远同步,不会出现"球变大了但光晕没跟上"的视觉 Bug。如果用三个独立的 AnimationController,同步将是一个噩梦。─────────────────────────────────────────────────
五、引导文案的状态切换
Text(
_isPlaying
? '闭眼深呼吸,放轻松… 🌿'
: '选择场景,按下播放开始放松',
style: TextStyle(fontSize: 14, color: Colors.grey.shade600),
)
两个状态两套文案——未播放时引导操作,播放时引导放松。这是"上下文感知"的微文案设计。
六、与呼吸球模块的设计对比
| 维度 | 白噪音脉冲 | 呼吸球 |
|---|---|---|
| 动画周期 | 2 秒 | 随模式变化 (10-19秒/周期) |
| 动画方向 | 始终 repeat(reverse) | 吸气 forward,呼气 reverse |
| 视觉元素 | 大小+不透明度+光晕 | 大小+不透明度+光晕 |
| 用户控制 | 播放/停止 | 开始/暂停/恢复/退出 |
| 色彩来源 | 场景色(4种) | 品牌紫色(固定) |
两者使用相同的技术栈(AnimatedBuilder + AnimationController),但服务于不同的体验目标:脉冲是"环境性的背景律动",呼吸球是"引导性的主动练习"。
七、设备的性能考量
在低端鸿蒙设备上(2GB RAM),持续 60fps 的脉冲动画可能造成轻微发热。优化建议:
// 如果需要在低端设备上降级
_pulse = AnimationController(
vsync: this,
duration: const Duration(seconds: 3), // 放慢周期
);
// 或使用 const 构造函数减少重建
上一篇:暂停与恢复:呼吸练习的完整生命周期
下一篇:倒计时与触觉反馈
作者简介:E-Brufen Dev,Flutter & 鸿蒙开发者。本文的视觉设计基于 E-Brufen 项目在鸿蒙真机上的实际表现撰写。
更多推荐


所有评论(0)