Flutter for OpenHarmony 打造沉浸式呼吸引导应用用动画疗愈身心在快节奏的现代生活中呼吸——这一最自然却常被忽视的生命节律——正成为连接身心、缓解焦虑的关键工具。科学研究表明有意识的深呼吸练习能有效降低心率、减轻压力、提升专注力。然而许多人虽知其益却苦于缺乏引导而难以坚持。 加入社区 欢迎加入开源鸿蒙跨平台开发者社区获取最新资源与技术支持 开源鸿蒙跨平台开发者社区完整效果一、设计理念让呼吸“可见”该应用的核心思想是“可视化呼吸”中心呼吸球随呼吸节奏放大吸气与缩小呼气模拟肺部的扩张与收缩动态色彩系统每个阶段使用不同颜色强化心理暗示实时状态反馈顶部显示循环次数底部指示当前阶段与操作指令进度条直观展示四阶段循环的进程。目标用户无需思考“现在该做什么”只需跟随视觉引导自然进入呼吸节奏。二、呼吸训练模型4-7-8 的变体虽然代码中未显式写出各阶段时长但从AnimationController(duration: const Duration(seconds: 24))和四阶段均分可推断每阶段约6 秒形成一个6-6-6-6的对称循环吸气Inhale6 秒缓慢深吸屏息Hold6 秒保持气息呼气Exhale6 秒缓慢深呼空息Hold6 秒保持空腔。 这种对称设计简化了认知负担适合初学者建立呼吸节奏感。三、核心技术实现1. 动画驱动AnimationControllerCurvedAnimation_animationControllerAnimationController(duration:constDuration(seconds:24),// 一个完整循环24秒vsync:this,);_breathAnimationTweendouble(begin:0.3,end:1.0).animate(CurvedAnimation(parent:_animationController,curve:Curves.easeInOut),);Tweendouble将动画值从0.3最小缩放映射到1.0最大缩放Curves.easeInOut使呼吸球的膨胀/收缩更符合自然呼吸的加速度变化非线性addListener监听动画值变化实时计算当前所处阶段。2. 阶段识别从连续动画到离散状态_breathAnimation.addListener((){setState((){_currentPhase(_animationController.value*4).floor();if(_currentPhase4)_currentPhase3;});});将[0, 1)的动画值乘以 4得到[0, 4)的区间floor()取整后得到0, 1, 2, 3分别对应四个阶段边界处理确保_currentPhase永远不会越界。3. 循环控制自动重置与计数..addStatusListener((status){if(statusAnimationStatus.completed){setState((){_cycleCount;_currentPhase0;});_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 法声音引导加入白噪音或提示音数据记录统计每日训练时长与循环次数多模式提供“能量唤醒”、“深度放松”等不同配色与节奏方案。七、结语技术为身心服务这段代码远不止是一个动画演示它体现了“科技向善”的理念——用精巧的技术手段服务于最基础的人类需求呼吸。完整代码importpackage:flutter/material.dart;voidmain(){runApp(const BreathTrainerApp());}class BreathTrainerApp extends StatelessWidget{const BreathTrainerApp({super.key});override Widget build(BuildContext context){returnMaterialApp(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 StateBreathTrainerScreencreateState()_BreathTrainerScreenState();}class _BreathTrainerScreenState extends StateBreathTrainerScreenwith TickerProviderStateMixin{late AnimationController _animationController;late Animationdouble_breathAnimation;int _currentPhase0;//0: inhale,1: hold,2: exhale,3: hold bool _isRunningfalse;int _cycleCount0;final ListString_phases[吸气,屏息,呼气,屏息];final ListColor_phaseColors[Colors.green.shade400, Colors.amber.shade300, Colors.red.shade400, Colors.blue.shade300,];final ListString_instructions[缓慢深吸气...,保持呼吸...,缓慢深呼气...,保持空息...,];final ListIconData_phaseIcons[Icons.arrow_upward, Icons.pause_circle_outline, Icons.arrow_downward, Icons.pause_circle_outline,];override voidinitState(){super.initState();_animationControllerAnimationController(duration: const Duration(seconds:24), vsync: this,)..addStatusListener((status){ if(statusAnimationStatus.completed){ setState((){ _cycleCount;_currentPhase0;});_animationController.reset();if(_isRunning)_animationController.forward();} });_breathAnimationTweendouble(begin:0.3,end:1.0).animate(CurvedAnimation(parent:_animationController,curve:Curves.easeInOut),)..addListener((){ setState((){ _currentPhase(_animationController.value*4).floor();if(_currentPhase4)_currentPhase3;});});} override void dispose(){ _animationController.dispose();super.dispose();} void _toggleTraining(){ setState((){ _isRunning!_isRunning;if(_isRunning){ _animationController.forward();} else { _animationController.stop();} });} void _resetTraining(){ setState((){ _isRunningfalse;_cycleCount0;_currentPhase0;_animationController.reset();});} override Widget build(BuildContext context){ final currentColor_phaseColors[_currentPhase];final safeAreaHeightMediaQuery.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:safeAreaHeight8,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 isActiveindex_currentPhase;returnExpanded(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,),),),],),],),),],),),),);}}