用 Flutter 做一个“会呼吸”的情绪灯 —— 不写 App,只玩光与动效**
当人们谈起 Flutter,总说:“跨平台”、“高性能 UI”、“Material Design”。
但今天,我们不做一个 App,不写列表、不连 API、不搞登录页。
我们要用 Flutter 做一盏会呼吸的 RGB 情绪灯—— 一个运行在手机或桌面端的圆形渐变灯光,能随心跳般缓慢明暗变化,还能点击切换颜色模式。它像夜里的小夜灯,温柔地照亮你的代码世界。
这不是又一个 Todo List 教程,而是一次对Flutter 动画与视觉表达能力的诗意探索。
🌌 项目目标
- 创建一个全屏柔和发光的圆形“灯”
- 使用
CustomPainter手绘光晕 - 通过
AnimationController实现“呼吸”效果(缩放 + 透明度联动) - 点击切换颜色主题:暖白 → 珊瑚红 → 靛蓝 → 薄荷绿
- 支持背景透明,营造悬浮光感
它没用,但它美。而这,正是技术浪漫主义的意义。
✅ 准备工作
flutter create mood_lampcdmood_lamp无需第三方依赖,纯原生 Flutter + Dart 实现。
🎨 第一步:设置透明全屏窗口(仅桌面/移动端适用)
为了让灯光“浮”在屏幕上,我们启用透明背景和无 AppBar 的全屏显示。
main.dart
import'package:flutter/material.dart';import'package:window_manager/window_manager.dart';// 可选:用于桌面端去边框voidmain()async{WidgetsFlutterBinding.ensureInitialized();// 【可选】桌面端配置透明无边框if(isDesktop()){awaitwindowManager.ensureInitialized();WindowOptionswindowOptions=constWindowOptions(size:Size(400,400),center:true,transparent:true,frameless:true,);windowManager.waitUntilReadyToShow(windowOptions,()async{awaitwindowManager.show();});}runApp(constMoodLampApp());}boolisDesktop()=>['windows','macos','linux'].contains(Theme.of(null).platform?.toString());classMoodLampAppextendsStatelessWidget{constMoodLampApp({super.key});@overrideWidgetbuild(BuildContextcontext){returnMaterialApp(debugShowCheckedModeBanner:false,home:Scaffold(backgroundColor:Colors.transparent,// 关键:让背景透下去body:constLampBody(),),);}}💡 提示:安卓/iOS 上也能实现类似效果,只需将
Scaffold背景设为透明并使用沉浸式状态栏。
🖌️ 第二步:自定义绘制“光晕”——使用CustomPainter
我们将创建一个动态模糊光圈,靠多重圆环叠加模拟真实灯光扩散。
painters/lamp_painter.dart
// painters/lamp_painter.dartimport'dart:math';import'package:flutter/material.dart';classLampPainterextendsCustomPainter{finalColorcolor;finaldouble intensity;// 呼吸强度 [0.0 - 1.0]LampPainter(this.color,this.intensity);@overridevoidpaint(Canvascanvas,Sizesize){finalcenter=Offset(size.width/2,size.height/2);finalradius=min(size.width,size.height)*0.3;finalpaint=Paint()..blendMode=BlendMode.srcOver;// 绘制多层扩散光圈(从内到外)for(int i=0;i<8;i++){finalalpha=(1.0-i/10)*intensity;finalspread=1.0+i*0.3;paint.shader=RadialGradient(colors:[color.withOpacity(alpha*0.8),Colors.transparent,],).createShader(Rect.fromCircle(center:center,radius:radius*spread),);canvas.drawCircle(center,radius*spread,paint);}}@overrideboolshouldRepaint(covariantLampPainteroldDelegate){returnoldDelegate.color!=color||oldDelegate.intensity!=intensity;}}💓 第三步:呼吸动画控制器
我们希望灯光像呼吸一样起伏,周期约 4 秒一次。
widgets/breathing_lamp.dart
// widgets/breathing_lamp.dartimport'package:flutter/material.dart';import'../painters/lamp_painter.dart';classBreathingLampextendsStatefulWidget{constBreathingLamp({super.key});@overrideState<BreathingLamp>createState()=>_BreathingLampState();}class_BreathingLampStateextendsState<BreathingLamp>withSingleTickerProviderStateMixin{lateAnimationController_controller;List<Color>_colors=[Colors.white70,Colors.deepOrange.shade300,Colors.indigo.shade200,Colors.tealAccent.withOpacity(0.6),];int _colorIndex=0;ColorgetcurrentColor=>_colors[_colorIndex];@overridevoidinitState(){super.initState();_controller=AnimationController(vsync:this,duration:constDuration(seconds:4),)..repeat(reverse:true);// 自动往返重复}@overridevoiddispose(){_controller.dispose();super.dispose();}voidchangeColor(){setState((){_colorIndex=(_colorIndex+1)%_colors.length;});}@overrideWidgetbuild(BuildContextcontext){returnGestureDetector(onTap:changeColor,child:AnimatedBuilder(animation:_controller,builder:(context,child){// 将动画值映射为呼吸强度:sin 波形更自然finalintensity=sin(_controller.value*pi)*0.6+0.7;returnCustomPaint(painter:LampPainter(currentColor,intensity),child:Container(width:double.infinity,height:double.infinity,alignment:Alignment.center,child:Opacity(opacity:intensity*0.4,child:Text('tap\nto\nchange',textAlign:TextAlign.center,style:TextStyle(fontSize:12,color:currentColor,fontWeight:FontWeight.bold,shadows:[BoxShadow(blurRadius:4,color:Colors.black.withOpacity(0.3),)],),),),),);},),);}}🔗 最后一步:连接所有部分
更新main.dart中的LampBody
classLampBodyextendsStatelessWidget{constLampBody({super.key});@overrideWidgetbuild(BuildContextcontext){returnContainer(decoration:constBoxDecoration(gradient:LinearGradient(begin:Alignment.topCenter,end:Alignment.bottomCenter,colors:[Color(0xFF111122),Color(0xFF0A0A0F)],// 深色背景衬托光),),child:constBreathingLamp(),);}}🎥 运行效果
flutter run-dmacos# 或 windows, android, ios你会看到:
- 屏幕中央有一团柔和的发光体
- 光芒缓慢明暗变化,如同呼吸
- 点击任意位置,灯光切换色调
- 文字提示轻微闪烁,几乎融入光影
就像房间里的一盏小灯,安静陪伴你深夜 coding。
🧩 技术亮点解析
| 特性 | 作用 |
|---|---|
RadialGradient + Shader | 实现真实光晕扩散 |
BlendMode.srcOver | 让多层光叠加更自然 |
sin(_controller.value * π) | 模拟生理级呼吸节奏(非线性) |
GestureDetector | 全区域响应点击换色 |
CustomPainter.shouldRepaint | 性能优化,避免不必要的重绘 |
🌈 可扩展方向(让你的灯更聪明)
- 添加陀螺仪感应:设备倾斜时光晕偏移
- 接入音乐频谱:灯光随节奏脉动(使用
just_audio分析音频) - 定时熄灭:30 分钟后自动渐暗(助眠模式)
- 多灯协作:局域网内多个设备同步呼吸(WebSocket)
- 打包成屏保或桌面装饰工具
结语:Flutter 是光的画布
我们习惯把 Flutter 当作“做 App 的工具”,
但它本质上是一个像素级可控的视觉引擎。
它可以是列表、表单、购物车,
也可以是一束光、一阵风、一声低语。
下次当你打开编辑器,不妨问自己:
“我能用代码,创造出一点美的东西吗?”
这盏小小的“情绪灯”不会帮你赚钱,也不会上架商店,
但它会在某个加班的夜里,轻轻告诉你:
“嘿,你还好吗?”
📦 完整项目结构建议
mood_lamp/ ├── lib/ │ ├── main.dart │ ├── painters/lamp_painter.dart │ └── widgets/breathing_lamp.dart ├── assets/ # 可留空 ├── pubspec.yaml # 无需额外依赖(除 window_manager 可选) └── README.mdGitHub 标题建议写:「A breathing mood lamp built with Flutter — because code can be poetic.」
“Light is not just physics. It’s feeling.”
— 用 Flutter 写动画的人,都是追光者。
欢迎大家加入开源鸿蒙跨平台开发者社区,一起共建开源鸿蒙跨平台生态。