1. 项目概述
今天要分享的是一个在Flutter中实现设备搜索动画效果的实战案例。这个动画效果常见于智能家居、蓝牙设备连接等场景,当App需要搜索周边设备时,通常会展示一个动态的雷达扫描效果,让用户直观感知搜索过程。
我在开发智能家居App时,就遇到过需要实现这种动画效果的需求。当时市面上现成的组件要么功能太重,要么效果不够流畅,于是决定自己动手实现一个轻量级解决方案。下面就把这个过程中积累的经验和踩过的坑分享给大家。
2. 核心需求解析
2.1 动画效果拆解
典型的设备搜索动画通常包含以下几个视觉元素:
- 雷达扫描效果:一个旋转的扇形区域,模拟雷达波扫描
- 设备发现提示:当"发现"设备时,在雷达范围内显示脉冲圆点
- 背景网格:增强科技感的辅助元素
2.2 技术选型考量
在Flutter中实现这类动画,主要有三种方案:
- 使用Rive等专业动画工具制作后导入
- 基于CustomPainter自定义绘制
- 组合使用内置动画组件
经过对比测试,我选择了CustomPainter方案,原因如下:
- 完全可控:可以精确控制每一帧的绘制细节
- 性能优异:避免了外部依赖和资源加载
- 灵活扩展:方便后续添加更多交互效果
3. 实现细节与核心代码
3.1 基础雷达扫描实现
dart复制class RadarPainter extends CustomPainter {
final double sweepAngle;
RadarPainter({required this.sweepAngle});
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width/2, size.height/2);
final radius = size.width/2 * 0.8;
// 绘制背景圆
canvas.drawCircle(
center,
radius,
Paint()..color = Colors.blue.withOpacity(0.1),
);
// 绘制扫描扇形
final sweepPaint = Paint()
..shader = SweepGradient(
colors: [Colors.transparent, Colors.blue.withOpacity(0.5)],
stops: [0.7, 1.0],
).createShader(Rect.fromCircle(center: center, radius: radius));
canvas.drawArc(
Rect.fromCircle(center: center, radius: radius),
-pi/2,
sweepAngle,
true,
sweepPaint,
);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}
3.2 添加动画控制器
dart复制class RadarAnimation extends StatefulWidget {
const RadarAnimation({super.key});
@override
State<RadarAnimation> createState() => _RadarAnimationState();
}
class _RadarAnimationState extends State<RadarAnimation>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 2),
)..repeat();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return CustomPaint(
painter: RadarPainter(
sweepAngle: _controller.value * 2 * pi,
),
);
},
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
4. 高级效果实现
4.1 添加设备发现动画
dart复制// 在RadarPainter中添加设备点绘制
void paint(Canvas canvas, Size size) {
// ...原有代码...
// 绘制设备点
final devices = [
Offset(0.5, 0.3),
Offset(-0.2, 0.6),
Offset(0.3, -0.4),
];
for (final device in devices) {
final position = center + Offset(
device.dx * radius,
device.dy * radius,
);
// 脉冲动画效果
final pulseRadius = radius * 0.1 * (1 + sin(_controller.value * 2 * pi)/4);
canvas.drawCircle(
position,
pulseRadius,
Paint()
..color = Colors.green.withOpacity(0.7)
..style = PaintingStyle.fill,
);
canvas.drawCircle(
position,
pulseRadius * 1.5,
Paint()
..color = Colors.green.withOpacity(0.3)
..style = PaintingStyle.stroke
..strokeWidth = 2,
);
}
}
4.2 性能优化技巧
- 重绘范围控制:
dart复制@override
bool shouldRepaint(covariant RadarPainter oldDelegate) {
return oldDelegate.sweepAngle != sweepAngle;
}
- 使用RepaintBoundary:
dart复制Widget build(BuildContext context) {
return RepaintBoundary(
child: AnimatedBuilder(
// ...
),
);
}
5. 常见问题与解决方案
5.1 动画卡顿问题
现象:在低端设备上动画不流畅
解决方案:
- 降低动画精度:减少不必要的绘制操作
- 使用硬件加速:确保使用
RepaintBoundary - 简化Shader:使用简单的线性渐变替代复杂效果
5.2 设备点闪烁问题
现象:设备点出现时闪烁明显
解决方案:
- 添加动画插值器:
dart复制_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 2),
)..repeat();
_animation = CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
);
- 优化透明度变化:
dart复制..color = Colors.green.withOpacity(0.5 * (1 + sin(_controller.value * pi))/2)
6. 完整实现与扩展建议
6.1 完整组件代码
dart复制class DeviceRadar extends StatefulWidget {
final List<Offset> devices;
const DeviceRadar({
super.key,
required this.devices,
});
@override
State<DeviceRadar> createState() => _DeviceRadarState();
}
class _DeviceRadarState extends State<DeviceRadar>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 2),
)..repeat();
_animation = CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
);
}
@override
Widget build(BuildContext context) {
return AspectRatio(
aspectRatio: 1,
child: RepaintBoundary(
child: AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return CustomPaint(
painter: RadarPainter(
sweepAngle: _animation.value * 2 * pi,
devices: widget.devices,
progress: _animation.value,
),
);
},
),
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
6.2 扩展功能建议
- 交互增强:
- 点击设备点显示详情
- 长按设备点触发连接操作
- 视觉效果增强:
- 添加扫描线尾迹效果
- 实现3D透视变换
- 添加环境光效
- 状态管理:
- 与BLoC或Riverpod集成
- 实现搜索状态可视化
在实际项目中,这个基础版本经过扩展后,可以支持多达20种不同的设备状态显示,包括信号强度指示、连接状态变化等复杂交互。关键是要保持绘制逻辑的简洁,避免过度设计。