Flutter for OpenHarmony 打造沉浸式呼吸引导应用:用动画疗愈身心

Flutter for OpenHarmony 打造沉浸式呼吸引导应用:用动画疗愈身心

在快节奏的现代生活中,呼吸------这一最自然却常被忽视的生命节律------正成为连接身心、缓解焦虑的关键工具。科学研究表明,有意识的深呼吸练习能有效降低心率、减轻压力、提升专注力。然而,许多人虽知其益,却苦于缺乏引导而难以坚持。

🌐 加入社区 欢迎加入 开源鸿蒙跨平台开发者社区 ,获取最新资源与技术支持: 👉 开源鸿蒙跨平台开发者社区


完整效果


一、设计理念:让呼吸"可见"

该应用的核心思想是 "可视化呼吸"

  • 中心呼吸球:随呼吸节奏放大(吸气)与缩小(呼气),模拟肺部的扩张与收缩;
  • 动态色彩系统:每个阶段使用不同颜色,强化心理暗示;
  • 实时状态反馈:顶部显示循环次数,底部指示当前阶段与操作指令;
  • 进度条:直观展示四阶段循环的进程。

💡 目标:用户无需思考"现在该做什么",只需跟随视觉引导,自然进入呼吸节奏。


二、呼吸训练模型:4-7-8 的变体

虽然代码中未显式写出各阶段时长,但从 AnimationController(duration: const Duration(seconds: 24)) 和四阶段均分可推断,每阶段约 6 秒 ,形成一个 6-6-6-6 的对称循环:

  1. 吸气(Inhale):6 秒,缓慢深吸;
  2. 屏息(Hold):6 秒,保持气息;
  3. 呼气(Exhale):6 秒,缓慢深呼;
  4. 空息(Hold) :6 秒,保持空腔。

🌿 这种对称设计简化了认知负担,适合初学者建立呼吸节奏感。


三、核心技术实现

1. 动画驱动:AnimationController + CurvedAnimation

dart 复制代码
_animationController = AnimationController(
  duration: const Duration(seconds: 24), // 一个完整循环24秒
  vsync: this,
);

_breathAnimation = Tween<double>(begin: 0.3, end: 1.0).animate(
  CurvedAnimation(parent: _animationController, curve: Curves.easeInOut),
);
  • Tween<double> :将动画值从 0.3(最小缩放)映射到 1.0(最大缩放);
  • Curves.easeInOut:使呼吸球的膨胀/收缩更符合自然呼吸的加速度变化(非线性);
  • addListener:监听动画值变化,实时计算当前所处阶段。

2. 阶段识别:从连续动画到离散状态

dart 复制代码
_breathAnimation.addListener(() {
  setState(() {
    _currentPhase = (_animationController.value * 4).floor();
    if (_currentPhase >= 4) _currentPhase = 3;
  });
});
  • [0, 1) 的动画值乘以 4,得到 [0, 4) 的区间;
  • floor() 取整后得到 0, 1, 2, 3,分别对应四个阶段;
  • 边界处理确保 _currentPhase 永远不会越界。

3. 循环控制:自动重置与计数

dart 复制代码
..addStatusListener((status) {
  if (status == AnimationStatus.completed) {
    setState(() { _cycleCount++; _currentPhase = 0; });
    _animationController.reset();
    if (_isRunning) _animationController.forward(); // 自动开始下一循环
  }
});
  • 完成一次循环后自动重置并递增计数器;
  • 若训练仍在进行,则无缝衔接下一轮,实现"无限循环"。

四、UI/UX 设计亮点

1. 色彩心理学应用

阶段 颜色 心理暗示
吸气 🟢 绿色 (green.shade400) 生长、能量、吸入生命力
屏息(吸后) 🟡 琥珀色 (amber.shade300) 温暖、稳定、蓄势待发
呼气 🔴 红色 (red.shade400) 释放、排出、代谢废物
屏息(呼后) 🔵 蓝色 (blue.shade300) 冷静、空灵、内在平静

每种颜色不仅用于中心球,还同步应用于:

  • 背景渐变
  • 阶段指示器文字
  • 指导文字
  • 进度条

2. 多层次视觉反馈

  • 背景脉动圆:大范围柔和光晕,营造氛围;
  • 中心呼吸球:高对比度、带发光阴影,成为视觉焦点;
  • 图标指引:↑(吸)、⏸(屏)、↓(呼),直观易懂;
  • 底部状态栏:明确告知当前动作;
  • 顶部状态:显示整体进度(循环次数)和运行状态。

3. 交互设计

  • 主按钮:绿色"开始" / 红色"暂停",符合直觉;
  • 重置按钮:独立于主流程,方便重新开始;
  • 运行状态标签:顶部右侧实时显示"进行中"或"已暂停",配色与状态一致。

五、代码结构与健壮性

  • with TickerProviderStateMixin :为 AnimationController 提供 vsync,防止后台动画消耗资源;
  • dispose():正确释放动画控制器,避免内存泄漏;
  • setState() 优化:仅在必要时更新 UI,保证流畅性;
  • 深色主题Color(0xFF0F172A) 营造宁静、专注的冥想环境,减少视觉刺激。

六、应用场景与扩展可能

适用场景

  • 日常减压:工作间隙快速放松;
  • 睡前助眠:帮助大脑从活跃状态过渡到平静;
  • 冥想辅助:作为正念练习的入门工具;
  • 呼吸训练:提升肺活量与呼吸控制能力。

可扩展方向

  • 自定义节奏:允许用户设置各阶段时长(如经典的 4-7-8 法);
  • 声音引导:加入白噪音或提示音;
  • 数据记录:统计每日训练时长与循环次数;
  • 多模式:提供"能量唤醒"、"深度放松"等不同配色与节奏方案。

七、结语:技术为身心服务

这段代码远不止是一个动画演示,它体现了 "科技向善" 的理念------用精巧的技术手段,服务于最基础的人类需求:呼吸

完整代码

bash 复制代码
import 'package:flutter/material.dart';

void main() {
  runApp(const BreathTrainerApp());
}

class BreathTrainerApp extends StatelessWidget {
  const BreathTrainerApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: '🌬️ 呼吸引导',
      theme: ThemeData(
        brightness: Brightness.dark,
        scaffoldBackgroundColor: const Color(0xFF0F172A),
        primarySwatch: Colors.blue,
        textTheme: const TextTheme(
          displayLarge:
              TextStyle(fontFamily: 'Arial', fontWeight: FontWeight.w300),
        ),
      ),
      home: const BreathTrainerScreen(),
    );
  }
}

class BreathTrainerScreen extends StatefulWidget {
  const BreathTrainerScreen({super.key});

  @override
  State<BreathTrainerScreen> createState() => _BreathTrainerScreenState();
}

class _BreathTrainerScreenState extends State<BreathTrainerScreen>
    with TickerProviderStateMixin {
  late AnimationController _animationController;
  late Animation<double> _breathAnimation;
  int _currentPhase = 0; // 0: inhale, 1: hold, 2: exhale, 3: hold
  bool _isRunning = false;
  int _cycleCount = 0;
  final List<String> _phases = ['吸气', '屏息', '呼气', '屏息'];
  final List<Color> _phaseColors = [
    Colors.green.shade400,
    Colors.amber.shade300,
    Colors.red.shade400,
    Colors.blue.shade300,
  ];
  final List<String> _instructions = [
    '缓慢深吸气...',
    '保持呼吸...',
    '缓慢深呼气...',
    '保持空息...',
  ];
  final List<IconData> _phaseIcons = [
    Icons.arrow_upward,
    Icons.pause_circle_outline,
    Icons.arrow_downward,
    Icons.pause_circle_outline,
  ];

  @override
  void initState() {
    super.initState();
    _animationController = AnimationController(
      duration: const Duration(seconds: 24),
      vsync: this,
    )..addStatusListener((status) {
        if (status == AnimationStatus.completed) {
          setState(() {
            _cycleCount++;
            _currentPhase = 0;
          });
          _animationController.reset();
          if (_isRunning) _animationController.forward();
        }
      });

    _breathAnimation = Tween<double>(begin: 0.3, end: 1.0).animate(
      CurvedAnimation(parent: _animationController, curve: Curves.easeInOut),
    )..addListener(() {
        setState(() {
          _currentPhase = (_animationController.value * 4).floor();
          if (_currentPhase >= 4) _currentPhase = 3;
        });
      });
  }

  @override
  void dispose() {
    _animationController.dispose();
    super.dispose();
  }

  void _toggleTraining() {
    setState(() {
      _isRunning = !_isRunning;
      if (_isRunning) {
        _animationController.forward();
      } else {
        _animationController.stop();
      }
    });
  }

  void _resetTraining() {
    setState(() {
      _isRunning = false;
      _cycleCount = 0;
      _currentPhase = 0;
      _animationController.reset();
    });
  }

  @override
  Widget build(BuildContext context) {
    final currentColor = _phaseColors[_currentPhase];
    final safeAreaHeight = MediaQuery.of(context).padding.top;

    return Scaffold(
      body: Container(
        decoration: BoxDecoration(
          gradient: LinearGradient(
            begin: Alignment.topCenter,
            end: Alignment.bottomCenter,
            colors: [
              Colors.black87,
              Color.lerp(Colors.black87, currentColor.withOpacity(0.15), 0.3)!,
              Color.lerp(Colors.black87, currentColor.withOpacity(0.05), 0.6)!,
              Colors.black87,
            ],
          ),
        ),
        child: SafeArea(
          child: Column(
            children: [
              // 顶部状态栏
              Padding(
                padding: EdgeInsets.only(
                    top: safeAreaHeight + 8, left: 20, right: 20),
                child: Row(
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  children: [
                    Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        const Text(
                          '🌬️ 呼吸引导',
                          style: TextStyle(
                              fontSize: 28, fontWeight: FontWeight.bold),
                        ),
                        const SizedBox(height: 4),
                        Text(
                          '${_cycleCount} 次循环',
                          style:
                              const TextStyle(fontSize: 16, color: Colors.grey),
                        ),
                      ],
                    ),
                    Container(
                      padding: const EdgeInsets.symmetric(
                          horizontal: 16, vertical: 6),
                      decoration: BoxDecoration(
                        color: _isRunning
                            ? Colors.green.withOpacity(0.2)
                            : Colors.red.withOpacity(0.2),
                        borderRadius: BorderRadius.circular(20),
                      ),
                      child: Row(
                        children: [
                          Icon(
                            _isRunning ? Icons.play_arrow : Icons.stop,
                            size: 18,
                            color: _isRunning ? Colors.green : Colors.red,
                          ),
                          const SizedBox(width: 4),
                          Text(
                            _isRunning ? '进行中' : '已暂停',
                            style: TextStyle(
                              fontSize: 14,
                              color: _isRunning ? Colors.green : Colors.red,
                              fontWeight: FontWeight.w600,
                            ),
                          ),
                        ],
                      ),
                    ),
                  ],
                ),
              ),

              const SizedBox(height: 30),

              // 呼吸可视化区域
              Expanded(
                child: Stack(
                  alignment: Alignment.center,
                  children: [
                    // 背景脉动圆
                    AnimatedBuilder(
                      animation: _breathAnimation,
                      builder: (context, child) {
                        return Container(
                          width: 320 * _breathAnimation.value,
                          height: 320 * _breathAnimation.value,
                          decoration: BoxDecoration(
                            shape: BoxShape.circle,
                            gradient: RadialGradient(
                              colors: [
                                currentColor.withOpacity(0.15),
                                currentColor.withOpacity(0.05),
                              ],
                            ),
                          ),
                        );
                      },
                    ),

                    // 中心呼吸球
                    AnimatedBuilder(
                      animation: _breathAnimation,
                      builder: (context, child) {
                        return Container(
                          width: 180 * _breathAnimation.value,
                          height: 180 * _breathAnimation.value,
                          decoration: BoxDecoration(
                            shape: BoxShape.circle,
                            gradient: RadialGradient(
                              colors: [
                                currentColor.withOpacity(0.9),
                                currentColor.withOpacity(0.7),
                              ],
                            ),
                            boxShadow: [
                              BoxShadow(
                                color: currentColor.withOpacity(0.4),
                                blurRadius: 30,
                                spreadRadius: 10,
                              ),
                            ],
                          ),
                          child: Center(
                            child: Icon(
                              _phaseIcons[_currentPhase],
                              size: 60 * _breathAnimation.value,
                              color: Colors.white,
                            ),
                          ),
                        );
                      },
                    ),

                    // 阶段指示器
                    Positioned(
                      bottom: 40,
                      child: Container(
                        padding: const EdgeInsets.symmetric(
                            horizontal: 24, vertical: 12),
                        decoration: BoxDecoration(
                          color: Colors.black87.withOpacity(0.7),
                          borderRadius: BorderRadius.circular(30),
                          border:
                              Border.all(color: currentColor.withOpacity(0.5)),
                        ),
                        child: Row(
                          mainAxisSize: MainAxisSize.min,
                          children: [
                            Icon(
                              _phaseIcons[_currentPhase],
                              color: currentColor,
                              size: 24,
                            ),
                            const SizedBox(width: 12),
                            Text(
                              _phases[_currentPhase],
                              style: TextStyle(
                                fontSize: 20,
                                fontWeight: FontWeight.bold,
                                color: currentColor,
                              ),
                            ),
                          ],
                        ),
                      ),
                    ),
                  ],
                ),
              ),

              // 指导文字
              Padding(
                padding: const EdgeInsets.only(bottom: 24),
                child: Text(
                  _instructions[_currentPhase],
                  style: TextStyle(
                    fontSize: 22,
                    fontWeight: FontWeight.w300,
                    color: currentColor,
                    height: 1.5,
                  ),
                  textAlign: TextAlign.center,
                ),
              ),

              // 控制按钮
              Container(
                padding:
                    const EdgeInsets.symmetric(horizontal: 24, vertical: 20),
                decoration: BoxDecoration(
                  color: Colors.black87.withOpacity(0.8),
                  borderRadius:
                      const BorderRadius.vertical(top: Radius.circular(30)),
                ),
                child: Column(
                  children: [
                    // 进度指示器
                    Row(
                      children: List.generate(4, (index) {
                        final isActive = index == _currentPhase;
                        return Expanded(
                          child: Padding(
                            padding: const EdgeInsets.symmetric(horizontal: 4),
                            child: Container(
                              height: 8,
                              decoration: BoxDecoration(
                                color: isActive
                                    ? _phaseColors[index]
                                    : _phaseColors[index].withOpacity(0.3),
                                borderRadius: BorderRadius.circular(4),
                              ),
                            ),
                          ),
                        );
                      }),
                    ),

                    const SizedBox(height: 24),

                    // 主控制按钮
                    Row(
                      children: [
                        Expanded(
                          child: OutlinedButton.icon(
                            onPressed: _resetTraining,
                            icon: const Icon(Icons.refresh, size: 20),
                            label: const Text('重置',
                                style: TextStyle(fontSize: 16)),
                            style: OutlinedButton.styleFrom(
                              foregroundColor: Colors.grey,
                              side: const BorderSide(color: Colors.grey),
                              padding: const EdgeInsets.symmetric(vertical: 16),
                              shape: RoundedRectangleBorder(
                                  borderRadius: BorderRadius.circular(16)),
                            ),
                          ),
                        ),
                        const SizedBox(width: 16),
                        Expanded(
                          flex: 2,
                          child: ElevatedButton.icon(
                            onPressed: _toggleTraining,
                            icon: Icon(
                              _isRunning ? Icons.pause : Icons.play_arrow,
                              size: 28,
                            ),
                            label: Text(
                              _isRunning ? '暂停训练' : '开始训练',
                              style: const TextStyle(
                                  fontSize: 18, fontWeight: FontWeight.bold),
                            ),
                            style: ElevatedButton.styleFrom(
                              backgroundColor:
                                  _isRunning ? Colors.red : Colors.green,
                              padding: const EdgeInsets.symmetric(vertical: 18),
                              shape: RoundedRectangleBorder(
                                  borderRadius: BorderRadius.circular(20)),
                              elevation: 4,
                            ),
                          ),
                        ),
                      ],
                    ),
                  ],
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
相关推荐
喵叔哟5 小时前
67.【.NET8 实战--孢子记账--从单体到微服务--转向微服务】--新增功能--分摊功能总体设计与业务流程
数据库·微服务·架构
CHENKONG_CK5 小时前
化工危化品桶装追溯:RFID 全流程可视化解决方案
网络
roman_日积跬步-终至千里5 小时前
【MLOps(1)】MLOps 架构总览与全方位基础:从实验室到生产环境的实战指南
架构
Fcy6485 小时前
Linux下 进程(二)(进程状态、僵尸进程和孤儿进程)
linux·运维·服务器·僵尸进程·孤儿进程·进程状态
ID_180079054735 小时前
Python结合淘宝关键词API进行商品价格监控与预警
服务器·数据库·python
renke33645 小时前
Flutter for OpenHarmony:数字涟漪 - 基于扩散算法的逻辑解谜游戏设计与实现
算法·flutter·游戏
落笔画忧愁e5 小时前
腾讯云轻量服务器 + OpenClaw 部署全攻略:从购买到飞书接入
服务器·飞书·腾讯云
第七序章5 小时前
【Linux学习笔记】初识Linux —— 理解gcc编译器
linux·运维·服务器·开发语言·人工智能·笔记·学习
AI科技星5 小时前
从ZUFT光速螺旋运动求导推出自然常数e
服务器·人工智能·线性代数·算法·矩阵