这个Flutter动画项目实现了一个设备搜索的动态效果,核心视觉元素是四个同心圆环,它们以相位差的方式交替变化大小和透明度,营造出波纹扩散的视觉效果。在实际展示中,任何时候都只有三个圆可见,另一个圆处于透明或最小尺寸状态。这种设计常见于蓝牙搜索、WiFi扫描等需要表达"正在搜索中"的场景。
动画效果的核心在于:
从技术实现角度看,这个效果完美展示了Flutter动画系统的几个关键能力:
整个动画系统建立在Flutter的动画框架之上,主要组件构成如下:
code复制AnimationController(驱动核心)
↓
TweenSequence(定义动画路径)
↓
CurvedAnimation(应用缓动曲线)
↓
AnimatedBuilder(监听变化)
↓
Widget树(重新构建)
这种架构的优势在于:
观察动画的时间线特征:
| 时间(秒) | 0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|---|
| 圆1大小 | 0 | 100 | 200 | 300 | 0 |
| 圆2大小 | 100 | 200 | 300 | 0 | 100 |
| 圆3大小 | 200 | 300 | 0 | 100 | 200 |
| 圆4大小 | 300 | 0 | 100 | 200 | 300 |
关键发现:
动画系统的数据流动分为两个阶段:
初始化阶段:
dart复制initState()
↓
AnimationController创建(4秒周期)
↓
TweenSequence定义动画路径
↓
动画对象绑定
运行阶段:
dart复制_controller.repeat()
↓
值随时间变化 (0.0 → 1.0)
↓
TweenSequence计算当前值
↓
AnimatedBuilder重建UI
AnimationController是整个动画系统的引擎:
dart复制_controller = AnimationController(
vsync: this,
duration: Duration(seconds: 4)
);
关键参数说明:
vsync: 绑定到当前Widget,防止屏幕外动画消耗资源duration: 4秒完整周期,对应四个圆的相位差repeat()让动画无限循环提示:在State类中混入SingleTickerProviderStateMixin是使用vsync的标准做法
每个圆的动画路径通过TweenSequence定义:
dart复制_sizeAnimation = TweenSequence<double>([
TweenSequenceItem(tween: Tween(begin: 0, end: 100), weight: 1.0),
TweenSequenceItem(tween: Tween(begin: 100, end: 200), weight: 1.0),
TweenSequenceItem(tween: Tween(begin: 200, end: 300), weight: 1.0),
TweenSequenceItem(tween: Tween(begin: 300, end: 0), weight: 1.0),
]).animate(CurvedAnimation(parent: _controller, curve: Curves.linear));
设计要点:
大小变化的同时,颜色透明度也在同步变化:
dart复制_colorAnimation = TweenSequence<Color?>([
TweenSequenceItem(
tween: ColorTween(
begin: Color(0xFF248EFF),
end: Color(0xFF248EFF).withOpacity(0.7)),
weight: 1.0),
// 其他阶段类似...
]).animate(CurvedAnimation(parent: _controller, curve: Curves.linear));
透明度变化规律:
四个圆的相位差是通过错开TweenSequence的起始点实现的:
dart复制// 圆1:0→100→200→300→0
_sizeAnimation = TweenSequence([0→100, 100→200, 200→300, 300→0])
// 圆2:100→200→300→0→100
_size1Animation = TweenSequence([100→200, 200→300, 300→0, 0→100])
// 圆3:200→300→0→100→200
_size2Animation = TweenSequence([200→300, 300→0, 0→100, 100→200])
// 圆4:300→0→100→200→300
_size3Animation = TweenSequence([300→0, 0→100, 100→200, 200→300])
这种设计确保:
这是动画设计的精妙之处:
数学验证:
当前实现存在明显的代码重复问题,优化方案:
方案1:使用循环创建动画
dart复制final animations = List.generate(4, (index) {
final offset = index * 0.25;
return _createCircleAnimation(offset);
});
方案2:封装动画创建逻辑
dart复制Animation<double> _createSizeAnimation(double offset) {
return TweenSequence([
// 根据offset计算各阶段的begin/end值
]).animate(CurvedAnimation(
parent: _controller,
curve: Interval(offset, 1.0, curve: Curves.linear)
));
}
方案3:使用AnimatedWidget
dart复制class PulsingCircle extends AnimatedWidget {
PulsingCircle({Key? key, required Animation<double> animation})
: super(key: key, listenable: animation);
@override
Widget build(BuildContext context) {
final animation = listenable as Animation<double>;
// 根据animation.value计算大小和颜色
}
}
要在中心圆添加双色渐变,可以修改_buildAnimatedCircle方法:
dart复制Widget _buildAnimatedCircle({
required double size,
required Color? color,
bool useGradient = false,
}) {
return Container(
width: size,
height: size,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: useGradient ? null : color,
gradient: useGradient
? LinearGradient(
colors: [Colors.blue, Colors.green],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
)
: null,
),
);
}
使用注意事项:
使用const构造函数:
dart复制const Icon(Icons.search, color: Colors.white, size: 90)
限制重建范围:
dart复制return AnimatedBuilder(
animation: _controller,
child: const Icon(...), // 静态部分提前创建
builder: (context, child) {
return Stack(
children: [
// 动画部分
child, // 直接使用预构建的静态部分
],
);
},
);
考虑使用CustomPaint:
对于复杂动画,CustomPaint比多层Stack更高效
添加点击响应:
dart复制GestureDetector(
onTap: () {
_controller.stop();
// 执行搜索逻辑
},
child: AnimatedBuilder(...),
)
成功/失败状态:
dart复制void _onSearchComplete(bool success) {
_controller.stop();
setState(() {
_statusColor = success ? Colors.green : Colors.red;
});
}
动态调整速度:
dart复制Slider(
value: _duration,
min: 1,
max: 10,
onChanged: (v) {
setState(() {
_duration = v;
_controller.duration = Duration(seconds: v.toInt());
});
},
)
iOS/Android风格差异:
dart复制Icon(
Theme.of(context).platform == TargetPlatform.iOS
? CupertinoIcons.search
: Icons.search,
)
暗黑模式支持:
dart复制color: Theme.of(context).brightness == Brightness.dark
? Colors.blue[200]
: Color(0xFF248EFF),
响应式尺寸:
dart复制final size = MediaQuery.of(context).size.width * 0.7;
_controller = AnimationController(
duration: Duration(seconds: (size / 100).toInt()),
);
以下是整合了所有优化建议的改进版本:
dart复制import 'package:flutter/material.dart';
class OptimizedSearchAnimation extends StatefulWidget {
const OptimizedSearchAnimation({super.key});
@override
State<OptimizedSearchAnimation> createState() => _OptimizedSearchAnimationState();
}
class _OptimizedSearchAnimationState extends State<OptimizedSearchAnimation>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
final List<Animation<double>> _sizeAnimations = [];
final List<Animation<Color?>> _colorAnimations = [];
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 4),
);
// 创建4个圆的动画,每个相位差25%
for (var i = 0; i < 4; i++) {
final offset = i * 0.25;
_sizeAnimations.add(
TweenSequence<double>([
_createTweenItem(0 + offset, 100 + offset),
_createTweenItem(100 + offset, 200 + offset),
_createTweenItem(200 + offset, 300 + offset),
_createTweenItem(300 + offset, 0 + offset),
]).animate(
CurvedAnimation(
parent: _controller,
curve: Curves.linear,
),
),
);
_colorAnimations.add(
TweenSequence<Color?>([
_createColorItem(1.0, 0.7, offset),
_createColorItem(0.7, 0.4, offset),
_createColorItem(0.4, 0.1, offset),
_createColorItem(0.1, 0.0, offset),
]).animate(
CurvedAnimation(
parent: _controller,
curve: Curves.linear,
),
),
);
}
_controller.repeat();
}
TweenSequenceItem<double> _createTweenItem(double begin, double end) {
return TweenSequenceItem<double>(
tween: Tween<double>(begin: begin, end: end),
weight: 1.0,
);
}
TweenSequenceItem<Color?> _createColorItem(double beginOpacity, double endOpacity, double offset) {
return TweenSequenceItem<Color?>(
tween: ColorTween(
begin: Color(0xFF248EFF).withOpacity(beginOpacity),
end: Color(0xFF248EFF).withOpacity(endOpacity),
),
weight: 1.0,
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: Center(
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Stack(
alignment: Alignment.center,
children: [
// 从大到小绘制,确保正确的叠加顺序
for (var i = 3; i >= 0; i--)
_buildAnimatedCircle(
size: _sizeAnimations[i].value,
color: _colorAnimations[i].value,
isCenter: i == 0,
),
const Icon(Icons.search, color: Colors.white, size: 90),
],
);
},
),
),
);
}
Widget _buildAnimatedCircle({
required double size,
required Color? color,
bool isCenter = false,
}) {
return Container(
width: size,
height: size,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
gradient: isCenter
? LinearGradient(
colors: [Colors.blue, Colors.green],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
)
: null,
),
);
}
}
关键改进点:
检查vsync:
性能分析:
dart复制void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_controller.repeat();
});
}
简化动画:
忘记启动控制器:
颜色值为null:
Widget尺寸为0:
全局动画控制器:
dart复制class AppState extends InheritedWidget {
final AnimationController searchController;
@override
bool updateShouldNotify(covariant AppState oldWidget) {
return searchController != oldWidget.searchController;
}
}
页面切换处理:
dart复制Navigator.push(
context,
PageRouteBuilder(
transitionDuration: Duration.zero,
pageBuilder: (_, __, ___) => NextPage(),
),
);
动画状态保持:
dart复制@override
void didChangeDependencies() {
super.didChangeDependencies();
final appState = AppState.of(context);
_controller.value = appState.searchController.value;
}
方形脉冲:
dart复制BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: color,
)
不规则形状:
dart复制CustomPaint(
painter: WavePainter(animationValue: _controller.value),
)
3D效果:
dart复制Transform(
transform: Matrix4.identity()
..setEntry(3, 2, 0.001)
..rotateX(animationValue * math.pi),
child: Container(...),
)
旋转+缩放:
dart复制Transform.rotate(
angle: _rotationAnimation.value,
child: Transform.scale(
scale: _scaleAnimation.value,
child: Icon(...),
),
)
路径动画:
dart复制Positioned(
left: _pathAnimation.value.dx,
top: _pathAnimation.value.dy,
child: Icon(...),
)
粒子效果:
dart复制LayoutBuilder(
builder: (_, constraints) {
return Stack(
children: List.generate(50, (i) {
return Positioned(
left: _particleAnimations[i].dx.value,
top: _particleAnimations[i].dy.value,
child: Particle(size: _particleAnimations[i].size.value),
);
}),
);
},
)
搜索状态反馈:
dart复制void _startSearch() async {
_controller.repeat();
final result = await searchService.search();
_controller.stop();
if (result) {
_showSuccessAnimation();
} else {
_showFailureAnimation();
}
}
进度指示:
dart复制ValueListenableBuilder<double>(
valueListenable: _progress,
builder: (_, value, __) {
return CircularProgressIndicator(value: value);
},
)
动态数量圆环:
dart复制void updateDeviceCount(int count) {
setState(() {
_circleCount = count;
_resetAnimations();
});
}
这个Flutter动画实现展示了如何通过精心设计的相位差动画创建流畅的视觉效果。在实际项目中,可以根据具体需求调整动画参数、添加交互逻辑或集成业务状态,打造更加丰富多样的用户体验。