Flutter flutter_animate 第三方库 动画的鸿蒙化适配与实战指南
本文介绍了如何在Flutter应用中利用flutter_animate库实现流畅动画效果。主要内容包括: 动画在应用中的重要性:提升用户体验、引导注意力、传达状态和增加趣味性 flutter_animate相比原生动画API的优势:代码简洁、学习成本低、动画类型丰富 环境配置:通过pubspec.yaml添加依赖 具体实现: 基础动画效果(淡入、滑动、缩放) 二维码页面的复合动画(淡入+缩放+滑动
Flutter flutter_animate 动画的鸿蒙化适配与实战指南
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
各位小伙伴们好呀!👋 我是那个上海某高校的大一计算机学生,继续来给大家分享 Flutter for OpenHarmony 开发的学习心得!
今天要聊的是 flutter_animate 动画!🎬
一个 App 如果没有动画,就像一盘菜没有调料,总觉得少了点什么。用户滑动、点击、页面切换… 有了动画才让这些交互变得生动有趣!
之前用 Flutter 原生的动画 API,写起来特别复杂,要用 AnimationController、Tween、Curve… 一堆概念 😵。后来发现了 flutter_animate 这个库,我的天,简直是救星!代码简洁到爆,今天必须分享给大家!
一、功能引入介绍 🎨
1.1 为什么需要动画?
动画不是花架子,它有实际作用:
- 🎯 引导注意力:新用户看到动画引导,能更快理解功能
- 😊 提升体验:流畅的动画让 App 看起来更专业
- 📚 传达状态:加载中、成功、失败… 动画比文字更直观
- 🎮 增加趣味:好的动画让用户更愿意使用 App
1.2 flutter_animate 的优势
| 对比项 | 原生动画 API | flutter_animate |
|---|---|---|
| 代码量 | 多 | 少 ✅ |
| 学习成本 | 高 | 低 ✅ |
| 动画类型 | 基础 | 丰富 ✅ |
| 组合能力 | 一般 | 强大 ✅ |
二、环境与依赖配置 🔧
2.1 pubspec.yaml 依赖
dependencies:
flutter:
sdk: flutter
# ========== 动画 ==========
flutter_animate: ^4.5.0
三、分步实现完整代码 🚀
3.1 基础动画效果
flutter_animate 的使用非常简单,只需要在 Widget 后面链式调用 .animate():
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
class AnimationDemo extends StatelessWidget {
const AnimationDemo({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('动画演示')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 淡入动画
Container(
width: 100,
height: 100,
color: Colors.red,
).animate().fadeIn(duration: 500.ms),
const SizedBox(height: 20),
// 滑动进入 + 淡入
Container(
width: 100,
height: 100,
color: Colors.blue,
).animate().slideX(begin: -0.5, end: 0).fadeIn(),
const SizedBox(height: 20),
// 缩放动画
Container(
width: 100,
height: 100,
color: Colors.green,
).animate().scale(begin: const Offset(0, 0), end: const Offset(1, 1)),
],
),
),
);
}
}
3.2 二维码页面的动画效果
这是项目中实际使用的动画实现:
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:share_plus/share_plus.dart';
import 'package:path_provider/path_provider.dart';
import 'dart:io';
class QRCodePage extends StatefulWidget {
final String? initialData;
final String? title;
const QRCodePage({
super.key,
this.initialData,
this.title,
});
State<QRCodePage> createState() => _QRCodePageState();
}
class _QRCodePageState extends State<QRCodePage> {
final TextEditingController _dataController = TextEditingController();
String _currentData = '';
Color _selectedColor = const Color(0xFF6366F1);
final List<Color> _availableColors = [
const Color(0xFF6366F1),
const Color(0xFF8B5CF6),
const Color(0xFFEC4899),
const Color(0xFFEF4444),
const Color(0xFFF97316),
const Color(0xFF10B981),
const Color(0xFF06B6D4),
const Color(0xFF3B82F6),
];
void initState() {
super.initState();
if (widget.initialData != null) {
_currentData = widget.initialData!;
_dataController.text = widget.initialData!;
} else {
_currentData = 'https://my-ohos-app.com';
_dataController.text = _currentData;
}
}
void _updateQRCode() {
setState(() {
_currentData = _dataController.text.trim();
if (_currentData.isEmpty) {
_currentData = 'https://my-ohos-app.com';
}
});
}
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF8FAFC),
appBar: AppBar(
title: Text(widget.title ?? '生成二维码'),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 二维码预览 - 带动画效果
_buildQRPreview()
.animate()
.fadeIn(duration: 300.ms) // 淡入
.scale(
begin: const Offset(0.9, 0.9), // 从 90% 开始
end: const Offset(1, 1), // 到 100%
),
const SizedBox(height: 24),
// 内容输入 - 带滑动动画
_buildDataInput()
.animate()
.fadeIn(delay: 100.ms, duration: 300.ms) // 延迟 100ms 淡入
.slideY(begin: 0.2, end: 0), // 从下往上滑入
const SizedBox(height: 24),
// 颜色选择器
_buildColorPicker()
.animate()
.fadeIn(delay: 200.ms, duration: 300.ms)
.slideY(begin: 0.2, end: 0),
const SizedBox(height: 24),
// 快捷操作
_buildQuickActions()
.animate()
.fadeIn(delay: 300.ms, duration: 300.ms)
.slideY(begin: 0.2, end: 0),
],
),
),
);
}
/// 二维码预览组件
Widget _buildQRPreview() {
return Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 20,
offset: const Offset(0, 4),
),
],
),
child: Column(
children: [
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.grey.shade200),
),
child: QrImageView(
data: _currentData,
version: QrVersions.auto,
size: 200,
eyeStyle: QrEyeStyle(
eyeShape: QrEyeShape.square,
color: _selectedColor,
),
dataModuleStyle: QrDataModuleStyle(
dataModuleShape: QrDataModuleShape.square,
color: _selectedColor,
),
),
),
const SizedBox(height: 16),
Text(
_currentData.length > 30
? '${_currentData.substring(0, 30)}...'
: _currentData,
style: TextStyle(color: Colors.grey[600], fontSize: 12),
textAlign: TextAlign.center,
),
],
),
);
}
/// 数据输入组件
Widget _buildDataInput() {
return Container(
padding: const EdgeInsets.all(20),
decoration: _cardDecoration(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
controller: _dataController,
maxLines: 3,
decoration: InputDecoration(
hintText: '输入文本、链接或任意数据',
filled: true,
fillColor: const Color(0xFFF8FAFC),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
),
onChanged: (_) => _updateQRCode(),
),
],
),
);
}
/// 颜色选择器
Widget _buildColorPicker() {
return Container(
padding: const EdgeInsets.all(20),
decoration: _cardDecoration(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('颜色', style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 16),
Wrap(
spacing: 12,
children: _availableColors.map((color) {
final isSelected = _selectedColor == color;
return GestureDetector(
onTap: () => setState(() => _selectedColor = color),
child: Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
border: Border.all(
color: isSelected ? Colors.white : Colors.transparent,
width: 3,
),
boxShadow: isSelected
? [
BoxShadow(
color: color.withValues(alpha: 0.5),
blurRadius: 8,
spreadRadius: 2,
),
]
: null,
),
child: isSelected
? const Icon(Icons.check, color: Colors.white, size: 20)
: null,
),
);
}).toList(),
),
],
),
);
}
/// 快捷操作
Widget _buildQuickActions() {
return Container(
padding: const EdgeInsets.all(20),
decoration: _cardDecoration(),
child: Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('已复制')),
);
},
icon: const Icon(Icons.copy),
label: const Text('复制'),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton.icon(
onPressed: () async {
// 分享逻辑
await Share.share(_currentData);
},
icon: const Icon(Icons.share),
label: const Text('分享'),
),
),
],
),
);
}
BoxDecoration _cardDecoration() {
return BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
);
}
}
3.3 高级动画效果
import 'package:flutter_animate/flutter_animate.dart';
class AdvancedAnimations extends StatelessWidget {
const AdvancedAnimations({super.key});
Widget build(BuildContext context) {
return Scaffold(
body: ListView(
padding: const EdgeInsets.all(20),
children: [
// ============ 1. 组合动画 ============
_buildTitle('组合动画'),
Container(
width: 100,
height: 100,
color: Colors.blue,
)
.animate()
.fadeIn(duration: 500.ms) // 先淡入
.slideX(begin: -0.3, end: 0) // 再左滑
.scale(begin: const Offset(0.8, 0.8), end: const Offset(1, 1)), // 最后放大
const SizedBox(height: 30),
// ============ 2. 延迟动画 ============
_buildTitle('延迟动画(依次出现)'),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: List.generate(5, (index) {
return Container(
width: 50,
height: 50,
color: Colors.primaries[index],
)
.animate()
.fadeIn(delay: (index * 100).ms) // 每个延迟 100ms
.scale();
}),
),
const SizedBox(height: 30),
// ============ 3. 摇晃效果 ============
_buildTitle('摇晃效果'),
Container(
width: 100,
height: 100,
color: Colors.orange,
)
.animate(onPlay: (controller) => controller.repeat())
.shake(hz: 2, duration: 1.seconds),
const SizedBox(height: 30),
// ============ 4. 脉冲效果 ============
_buildTitle('脉冲效果'),
Container(
width: 100,
height: 100,
color: Colors.red,
)
.animate(onPlay: (controller) => controller.repeat(reverse: true))
.scale(
begin: const Offset(1, 1),
end: const Offset(1.1, 1.1),
duration: 500.ms,
),
const SizedBox(height: 30),
// ============ 5. 滑动效果 ============
_buildTitle('各种滑动效果'),
Row(
children: [
Expanded(child: _buildSlideCard(Icons.arrow_upward, '上', Colors.blue)),
const SizedBox(width: 8),
Expanded(child: _buildSlideCard(Icons.arrow_downward, '下', Colors.green)),
const SizedBox(width: 8),
Expanded(child: _buildSlideCard(Icons.arrow_back, '左', Colors.orange)),
const SizedBox(width: 8),
Expanded(child: _buildSlideCard(Icons.arrow_forward, '右', Colors.purple)),
],
),
const SizedBox(height: 30),
// ============ 6. 翻转效果 ============
_buildTitle('翻转效果'),
Container(
width: 100,
height: 100,
color: Colors.teal,
)
.animate()
.flip(duration: 800.ms),
const SizedBox(height: 30),
// ============ 7. 颜色渐变 ============
_buildTitle('颜色动画'),
Container(
width: 100,
height: 100,
color: Colors.red,
)
.animate(onPlay: (controller) => controller.repeat(reverse: true))
.tint(
color: Colors.blue.withValues(alpha: 0.5),
duration: 2.seconds,
),
],
),
);
}
Widget _buildTitle(String title) {
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Text(
title,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
);
}
Widget _buildSlideCard(IconData icon, String label, Color color) {
return Container(
height: 80,
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(12),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, color: Colors.white, size: 30),
Text(label, style: const TextStyle(color: Colors.white)),
],
),
)
.animate()
.fadeIn()
.slideY(begin: 0.5, end: 0, curve: Curves.easeOutBack);
}
}
3.4 列表动画
让列表的每一项都有入场动画:
import 'package:flutter_animate/flutter_animate.dart';
class AnimatedListPage extends StatelessWidget {
const AnimatedListPage({super.key});
Widget build(BuildContext context) {
final items = List.generate(20, (index) => '商品 ${index + 1}');
return Scaffold(
appBar: AppBar(title: const Text('列表动画')),
body: ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ListTile(
leading: CircleAvatar(
backgroundColor: Colors.primaries[index % Colors.primaries.length],
child: Text('${index + 1}'),
),
title: Text(items[index]),
subtitle: Text('这是第 ${index + 1} 个商品'),
)
// 每个 item 依次延迟 50ms 淡入并从左滑入
.animate()
.fadeIn(delay: (index * 50).ms)
.slideX(begin: 0.3, end: 0, delay: (index * 50).ms);
},
),
);
}
}
3.5 骨架屏动画
加载数据时显示骨架屏动画:
import 'package:shimmer/shimmer.dart';
import 'package:flutter_animate/flutter_animate.dart';
class SkeletonLoader extends StatelessWidget {
const SkeletonLoader({super.key});
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: 5,
itemBuilder: (context, index) {
return Card(
margin: const EdgeInsets.only(bottom: 16),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 图片骨架
Container(
height: 150,
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(8),
),
)
.animate(onPlay: (c) => c.repeat())
.shimmer(duration: 1.seconds, color: Colors.grey.shade100),
const SizedBox(height: 12),
// 标题骨架
Container(
height: 20,
width: 200,
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(4),
),
)
.animate(onPlay: (c) => c.repeat())
.shimmer(duration: 1.2.seconds, color: Colors.grey.shade100),
const SizedBox(height: 8),
// 描述骨架
Container(
height: 16,
width: double.infinity,
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(4),
),
)
.animate(onPlay: (c) => c.repeat())
.shimmer(duration: 1.4.seconds, color: Colors.grey.shade100),
],
),
),
);
},
),
);
}
}
四,开发踩坑与挫折 😤
4.1 踩坑一:动画不执行
问题描述:
动画没有任何反应。
排查过程:
- 检查是否在 StatelessWidget 中使用
- 检查
.animate()是否调用了
解决方案:
// ❌ 错误
Container(color: Colors.red).fadeIn();
// ✅ 正确
Container(color: Colors.red).animate().fadeIn();
4.2 踩坑二:动画只执行一次
问题描述:
动画播放一次后就不再播放了。
解决方案:
使用 onPlay 回调中的 controller.repeat():
Container(color: Colors.red)
.animate(onPlay: (controller) => controller.repeat())
.shake();
4.3 踩坑三:动画延迟太久
问题描述:
设置了 delay,但动画看起来卡顿。
解决方案:
动画延迟总和不宜过长,建议每个 item 延迟 30-50ms:
// ✅ 推荐:每个 item 延迟 50ms
item.animate()
.fadeIn(delay: (index * 50).ms)
.slideY(begin: 0.3, end: 0);
五、最终实现效果 📸
(此处附鸿蒙设备上成功运行的截图)
—
六、个人学习总结 📝
通过 flutter_animate 的学习,我收获了很多:
- ✅ 动画不需要复杂的 AnimationController
- ✅ 链式调用让代码更优雅
- ✅ 各种预设动画效果让 App 更生动
flutter_animate 真的是 Flutter 动画的最佳选择!强烈推荐给大家!
作者:上海某高校大一学生,Flutter 爱好者
发布时间:2026年4月
更多推荐



所有评论(0)