导语:动画是提升 Flutter 应用交互体验的核心手段,流畅的动画能让界面过渡更自然、操作反馈更直观。Flutter 提供了分层的动画 API 体系,从无需手动管理控制器的基础组件(如 AnimatedContainer),到跨页面的共享元素动画(Hero),再到灵活可控的自定义动画(AnimationController),满足不同场景需求。本文通过实战案例手把手教你实现各类动画效果,包含完整可运行代码、核心原理解析及性能优化技巧,让你快速掌握 Flutter 动画开发精髓!
一、核心概念铺垫
在动手实现前,先明确 Flutter 动画的核心基础,避免踩坑:
| 概念 | 作用 | 适用场景 |
|---|---|---|
| 动画组件(AnimatedXXX) | 封装好的高阶组件,修改属性自动触发动画 | 简单属性过渡(尺寸、颜色、透明度等) |
| Hero 动画 | 跨页面共享元素的过渡动画 | 列表页→详情页(图片、卡片等共享元素) |
| AnimationController | 动画核心控制器,管理播放、暂停、时长等 | 复杂自定义动画(多效果组合、进度控制) |
| Animation | 存储动画值(如 0→1、0→2π),提供插值计算 | 所有自定义动画场景 |
| AnimatedBuilder | 构建动画 UI,仅重建动画相关部分 | 性能优化,避免整树重建 |
| Curve | 动画曲线,控制动画速度变化 | 调整动画节奏(匀速、加速减速等) |
💡 关键原则:优先使用内置动画组件(开发效率高、性能有保障),复杂场景再用自定义控制器。
二、基础动画:AnimatedContainer(零控制器上手)
核心特性
- 无需手动管理 AnimationController,修改组件属性自动触发过渡动画
- 支持多属性同时动画:width、height、color、padding、borderRadius、transform 等
- 核心配置:
duration(动画时长)、curve(动画曲线)、onEnd(动画结束回调)
实战案例:点击切换卡片状态(尺寸 + 颜色 + 圆角)
实现点击卡片后,同时发生尺寸放大、颜色切换、圆角变化的组合动画:
dart
import 'package:flutter/material.dart';
void main() {
runApp(const AnimatedContainerApp());
}
class AnimatedContainerApp extends StatefulWidget {
const AnimatedContainerApp({super.key});
@override
State<AnimatedContainerApp> createState() => _AnimatedContainerAppState();
}
class _AnimatedContainerAppState extends State<AnimatedContainerApp> {
bool _isExpanded = false; // 控制动画状态的开关
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(primarySwatch: Colors.blue),
home: Scaffold(
appBar: AppBar(title: const Text('AnimatedContainer 实战')),
body: Center(
child: GestureDetector(
// 点击触发状态切换
onTap: () {
setState(() {
_isExpanded = !_isExpanded;
});
},
// 核心动画组件
child: AnimatedContainer(
// 动画目标属性:尺寸
width: _isExpanded ? 300 : 150,
height: _isExpanded ? 300 : 150,
// 动画目标属性:颜色
color: _isExpanded ? Colors.blueAccent : Colors.redAccent,
// 动画目标属性:圆角
borderRadius: _isExpanded
? BorderRadius.circular(50)
: BorderRadius.circular(10),
// 动画目标属性:内边距(新增效果)
padding: _isExpanded
? const EdgeInsets.all(30)
: const EdgeInsets.all(10),
// 动画时长(必须配置)
duration: const Duration(seconds: 1),
// 动画曲线(控制速度变化)
curve: Curves.easeInOut,
// 动画结束回调(可选)
onEnd: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(_isExpanded ? '已展开' : '已收缩')),
);
},
child: const Center(
child: Text(
'点击切换',
style: TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold),
),
),
),
),
),
),
);
}
}
关键说明
-
动画触发逻辑:通过
setState修改_isExpanded状态,AnimatedContainer 自动检测属性变化并执行过渡 -
多属性同步:支持同时对多个属性设置动画,无需额外处理同步问题
-
扩展技巧:可通过
transform属性添加平移、旋转效果,例如:dart
transform: _isExpanded ? Matrix4.translationValues(0, -50, 0) // 上移50 : Matrix4.identity(),
三、页面过渡动画:Hero(共享元素无缝跳转)
核心特性
- 跨页面共享同一个元素的过渡动画,实现「列表页→详情页」的无缝衔接
- 核心要求:两个页面的共享元素必须设置相同的
tag(唯一标识) - 自动处理:Flutter 自动计算元素在两个页面的位置、尺寸变化,生成过渡动画
实战案例:图片列表→详情页过渡
1. 列表页(共享元素源头)
dart
class HeroListPage extends StatelessWidget {
const HeroListPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Hero 动画列表')),
body: GridView.count(
crossAxisCount: 2,
padding: const EdgeInsets.all(10),
crossAxisSpacing: 10,
mainAxisSpacing: 10,
children: List.generate(6, (index) {
// 每个图片项都是 Hero 动画元素
return GestureDetector(
onTap: () {
// 跳转到详情页,携带图片URL参数
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => HeroDetailPage(
imageUrl: 'https://picsum.photos/200/200?random=$index',
heroTag: 'image_$index', // 唯一tag,与详情页一致
),
),
);
},
child: Hero(
tag: 'image_$index', // 唯一标识,必须全局唯一
// 优化:添加过渡动画的形状裁剪
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
'https://picsum.photos/200/200?random=$index',
fit: BoxFit.cover,
width: double.infinity,
height: double.infinity,
// 加载占位
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return const Center(child: CircularProgressIndicator(strokeWidth: 2));
},
),
),
),
);
}),
),
);
}
}
2. 详情页(共享元素目标)
dart
class HeroDetailPage extends StatelessWidget {
final String imageUrl;
final String heroTag; // 接收列表页传递的tag
const HeroDetailPage({
super.key,
required this.imageUrl,
required this.heroTag,
});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
// 点击空白处返回
body: GestureDetector(
onTap: () => Navigator.pop(context),
child: Center(
child: Hero(
tag: heroTag, // 与列表页完全一致
// 优化:添加过渡动画的淡入效果
child: FadeInImage(
placeholder: const AssetImage('assets/loading.png'), // 本地占位图(需提前配置)
image: NetworkImage(imageUrl.replaceAll('200/200', '800/800')), // 高清图
fit: BoxFit.contain,
width: double.infinity,
height: double.infinity,
),
),
),
),
);
}
}
避坑指南
tag必须全局唯一:如果列表有多个元素,不能使用固定字符串(如示例中用image_$index区分)- 避免嵌套 Hero:共享元素内部不能再包含 Hero 组件,否则会导致动画异常
- 占位图优化:网络图片需添加加载占位,避免动画过程中出现空白
四、自定义动画:AnimationController(灵活控制动画进度)
核心概念
- AnimationController:动画的「总开关」,控制播放、暂停、反向、重复,提供 0.0→1.0 的线性值
- Animation :通过
Tween(插值器)将 0.0→1.0 映射为实际需要的值(如 0→2π、0.5→1.0) - AnimatedBuilder:高效构建动画 UI,仅重建动画相关部分,避免整树重绘
- SingleTickerProviderStateMixin:提供动画帧回调,用于驱动 AnimationController
实战案例:旋转 + 缩放 + 透明度组合动画
实现一个无限循环的组合动画:旋转 360° + 缩放(0.5→1.0)+ 透明度(0.3→1.0)
dart
import 'package:flutter/material.dart';
void main() {
runApp(const CustomAnimationApp());
}
class CustomAnimationApp extends StatefulWidget {
const CustomAnimationApp({super.key});
@override
State<CustomAnimationApp> createState() => _CustomAnimationAppState();
}
class _CustomAnimationAppState extends State<CustomAnimationApp>
with SingleTickerProviderStateMixin {
late AnimationController _controller; // 动画控制器
late Animation<double> _rotationAnim; // 旋转动画
late Animation<double> _scaleAnim; // 缩放动画
late Animation<double> _opacityAnim; // 透明度动画
@override
void initState() {
super.initState();
// 1. 初始化控制器:时长2秒,绑定当前页面的帧回调
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 2),
lowerBound: 0.0, // 最小值
upperBound: 1.0, // 最大值
)..repeat(reverse: true); // 循环播放,反向重复(1.0→0.0→1.0)
// 2. 配置旋转动画:0→2π(360°)
_rotationAnim = Tween<double>(begin: 0, end: 2 * 3.1415)
.animate(CurvedAnimation(parent: _controller, curve: Curves.linear));
// 3. 配置缩放动画:0.5→1.0
_scaleAnim = Tween<double>(begin: 0.5, end: 1.0)
.animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));
// 4. 配置透明度动画:0.3→1.0
_opacityAnim = Tween<double>(begin: 0.3, end: 1.0)
.animate(CurvedAnimation(parent: _controller, curve: Curves.easeIn));
}
@override
void dispose() {
// 必须释放控制器,避免内存泄漏
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('自定义组合动画实战')),
body: Center(
// AnimatedBuilder:仅重建builder内部的Widget
child: AnimatedBuilder(
animation: _controller, // 绑定控制器
builder: (context, child) {
// 组合变换:旋转 + 缩放 + 透明度
return Opacity(
opacity: _opacityAnim.value,
child: Transform(
transform: Matrix4.identity()
..rotateZ(_rotationAnim.value) // 旋转
..scale(_scaleAnim.value), // 缩放
alignment: Alignment.center, // 变换中心点
child: child, // 复用子Widget,减少重建
),
);
},
// 静态子Widget:不会随动画重建
child: Container(
width: 200,
height: 200,
decoration: BoxDecoration(
color: Colors.purpleAccent,
borderRadius: BorderRadius.circular(20),
boxShadow: const [
BoxShadow(
color: Colors.purple.withOpacity(0.3),
blurRadius: 10,
offset: Offset(0, 5),
)
],
),
child: const Center(
child: Text(
'旋转缩放\n透明度变化',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
),
),
),
),
),
);
}
}
进阶技巧
- 动画控制:通过
_controller.play()(播放)、_controller.pause()(暂停)、_controller.reverse()(反向)控制动画状态 - 进度监听:通过
_controller.addListener(() {})监听动画进度,实现自定义逻辑(如进度条同步) - 多曲线组合:不同动画可配置不同的
Curve,实现更丰富的节奏变化
五、动画性能优化指南
| 优化点 | 实现方式 | 效果 |
|---|---|---|
| 减少重建范围 | 使用 AnimatedBuilder 包裹动画部分,将静态 Widget 作为 child 参数传入 | 仅动画相关部分重建,避免整树重绘 |
| 隔离重绘区域 | 用 RepaintBoundary 包裹动画 Widget,创建独立的重绘层 | 避免动画重绘影响其他区域 |
| 优先使用内置组件 | 优先选择 AnimatedContainer、AnimatedOpacity 等内置组件,而非自定义 AnimationController | 内置组件已做性能优化,开发效率更高 |
| 避免复杂计算 | 不在 AnimatedBuilder 的 builder 方法中执行复杂逻辑(如循环、网络请求) | 减少每帧耗时,避免动画卡顿 |
| 控制动画帧率 | 复杂动画可通过 AnimationController 的 frameRate 参数限制帧率(如 30fps) |
降低性能消耗,适配低端设备 |
| 复用动画对象 | 将 Animation 对象缓存到成员变量,而非在 builder 中重复创建 | 减少对象创建销毁开销 |
六、常见问题排查
- 动画不生效 :检查是否忘记设置
duration(基础动画)或未调用_controller.forward()(自定义动画) - 动画卡顿:排查是否在 builder 中做了复杂计算,或未使用 AnimatedBuilder/RepaintBoundary
- Hero 动画异常 :确认两个页面的
tag完全一致,且共享元素的父布局没有影响位置的动态变化 - 内存泄漏 :自定义动画必须在
dispose中调用_controller.dispose()
七、结语
Flutter 动画系统的设计理念是「分层抽象」,让开发者可以根据需求选择合适的 API :简单场景用内置动画组件快速实现,复杂场景用 AnimationController 灵活定制。本文通过三个核心实战案例,覆盖了大部分日常开发场景,结合性能优化技巧和避坑指南,帮助你在项目中快速落地高质量动画。建议在实际开发中多尝试组合不同动画效果,同时关注动画的性能表现,让应用既美观又流畅!