第16章 - 动画入门
嗨,朋友!我是长安。
一个好的 App 不仅要功能完善,还要有流畅的动画效果来提升用户体验。这一章,我们来学习 Flutter 中的动画,从简单的隐式动画到复杂的显式动画。
🎬 动画基础概念
Flutter 动画分类
| 类型 | 特点 | 适用场景 |
|---|---|---|
| 隐式动画 | 简单易用,自动处理 | 简单的属性变化 |
| 显式动画 | 完全控制,更灵活 | 复杂的动画效果 |
| Hero 动画 | 页面间共享元素 | 图片放大、列表详情 |
| 物理动画 | 模拟真实物理效果 | 弹簧、摩擦等效果 |
✨ 隐式动画(最简单)
隐式动画是 Flutter 中最简单的动画方式,只需要改变属性值,Flutter 自动处理过渡动画。
AnimatedContainer
最常用的隐式动画 Widget,可以动画化几乎所有 Container 属性。
class AnimatedContainerDemo extends StatefulWidget {
const AnimatedContainerDemo({super.key});
State<AnimatedContainerDemo> createState() => _AnimatedContainerDemoState();
}
class _AnimatedContainerDemoState extends State<AnimatedContainerDemo> {
bool _expanded = false;
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('AnimatedContainer')),
body: Center(
child: GestureDetector(
onTap: () => setState(() => _expanded = !_expanded),
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
width: _expanded ? 200 : 100,
height: _expanded ? 200 : 100,
decoration: BoxDecoration(
color: _expanded ? Colors.blue : Colors.red,
borderRadius: BorderRadius.circular(_expanded ? 100 : 16),
boxShadow: [
BoxShadow(
color: Colors.black26,
blurRadius: _expanded ? 20 : 5,
offset: Offset(0, _expanded ? 10 : 2),
),
],
),
child: Center(
child: Text(
_expanded ? '收缩' : '展开',
style: const TextStyle(color: Colors.white),
),
),
),
),
),
);
}
}
AnimatedOpacity
透明度渐变动画。
class AnimatedOpacityDemo extends StatefulWidget {
const AnimatedOpacityDemo({super.key});
State<AnimatedOpacityDemo> createState() => _AnimatedOpacityDemoState();
}
class _AnimatedOpacityDemoState extends State<AnimatedOpacityDemo> {
bool _visible = true;
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('AnimatedOpacity')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedOpacity(
duration: const Duration(milliseconds: 500),
opacity: _visible ? 1.0 : 0.0,
child: Container(
width: 150,
height: 150,
color: Colors.blue,
child: const Center(
child: Text('我会消失', style: TextStyle(color: Colors.white)),
),
),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () => setState(() => _visible = !_visible),
child: Text(_visible ? '隐藏' : '显示'),
),
],
),
),
);
}
}
AnimatedPositioned
位置动画,必须在 Stack 中使用。
class AnimatedPositionedDemo extends StatefulWidget {
const AnimatedPositionedDemo({super.key});
State<AnimatedPositionedDemo> createState() => _AnimatedPositionedDemoState();
}
class _AnimatedPositionedDemoState extends State<AnimatedPositionedDemo> {
bool _moved = false;
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('AnimatedPositioned')),
body: Stack(
children: [
AnimatedPositioned(
duration: const Duration(milliseconds: 500),
curve: Curves.elasticOut,
left: _moved ? 200 : 50,
top: _moved ? 300 : 100,
child: GestureDetector(
onTap: () => setState(() => _moved = !_moved),
child: Container(
width: 100,
height: 100,
decoration: BoxDecoration(
color: Colors.purple,
borderRadius: BorderRadius.circular(16),
),
child: const Center(
child: Text('点我移动', style: TextStyle(color: Colors.white)),
),
),
),
),
],
),
);
}
}
AnimatedCrossFade
两个 Widget 之间的淡入淡出切换。
class AnimatedCrossFadeDemo extends StatefulWidget {
const AnimatedCrossFadeDemo({super.key});
State<AnimatedCrossFadeDemo> createState() => _AnimatedCrossFadeDemoState();
}
class _AnimatedCrossFadeDemoState extends State<AnimatedCrossFadeDemo> {
bool _showFirst = true;
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('AnimatedCrossFade')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedCrossFade(
duration: const Duration(milliseconds: 300),
crossFadeState: _showFirst
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
firstChild: Container(
width: 200,
height: 200,
color: Colors.blue,
child: const Center(
child: Icon(Icons.favorite, color: Colors.white, size: 60),
),
),
secondChild: Container(
width: 200,
height: 200,
color: Colors.red,
child: const Center(
child: Icon(Icons.star, color: Colors.white, size: 60),
),
),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () => setState(() => _showFirst = !_showFirst),
child: const Text('切换'),
),
],
),
),
);
}
}
AnimatedSwitcher
更灵活的 Widget 切换动画。
class AnimatedSwitcherDemo extends StatefulWidget {
const AnimatedSwitcherDemo({super.key});
State<AnimatedSwitcherDemo> createState() => _AnimatedSwitcherDemoState();
}
class _AnimatedSwitcherDemoState extends State<AnimatedSwitcherDemo> {
int _count = 0;
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('AnimatedSwitcher')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
transitionBuilder: (child, animation) {
return ScaleTransition(
scale: animation,
child: child,
);
},
child: Text(
'$_count',
// 关键:必须设置不同的 key
key: ValueKey<int>(_count),
style: const TextStyle(fontSize: 60),
),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () => setState(() => _count++),
child: const Text('增加'),
),
],
),
),
);
}
}
更多隐式动画 Widget
// 尺寸动画
AnimatedSize(
duration: const Duration(milliseconds: 300),
child: Container(
width: _expanded ? 200 : 100,
height: _expanded ? 200 : 100,
color: Colors.blue,
),
)
// 内边距动画
AnimatedPadding(
duration: const Duration(milliseconds: 300),
padding: EdgeInsets.all(_expanded ? 50 : 10),
child: Container(color: Colors.blue),
)
// 对齐动画
AnimatedAlign(
duration: const Duration(milliseconds: 300),
alignment: _alignLeft ? Alignment.centerLeft : Alignment.centerRight,
child: Container(width: 50, height: 50, color: Colors.blue),
)
// 默认文字样式动画
AnimatedDefaultTextStyle(
duration: const Duration(milliseconds: 300),
style: TextStyle(
fontSize: _large ? 30 : 16,
color: _large ? Colors.red : Colors.black,
),
child: const Text('Hello'),
)
// 主题动画
AnimatedTheme(
duration: const Duration(milliseconds: 300),
data: _dark ? ThemeData.dark() : ThemeData.light(),
child: ...,
)
🎮 显式动画(完全控制)
显式动画提供更精细的控制,需要使用 AnimationController。
AnimationController 基础
class ExplicitAnimationDemo extends StatefulWidget {
const ExplicitAnimationDemo({super.key});
State<ExplicitAnimationDemo> createState() => _ExplicitAnimationDemoState();
}
class _ExplicitAnimationDemoState extends State<ExplicitAnimationDemo>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
void initState() {
super.initState();
// 创建控制器
_controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this, // 需要 TickerProviderStateMixin
);
// 创建动画(使用 Tween)
_animation = Tween<double>(begin: 0, end: 300).animate(_controller);
// 监听动画值变化
_animation.addListener(() {
setState(() {}); // 触发重建
});
// 监听动画状态
_animation.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_controller.reverse(); // 完成后反向
} else if (status == AnimationStatus.dismissed) {
_controller.forward(); // 反向完成后正向
}
});
}
void dispose() {
_controller.dispose(); // 必须释放
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('显式动画')),
body: Center(
child: Container(
width: _animation.value,
height: _animation.value,
color: Colors.blue,
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
if (_controller.isAnimating) {
_controller.stop();
} else {
_controller.forward();
}
},
child: Icon(_controller.isAnimating ? Icons.pause : Icons.play_arrow),
),
);
}
}
使用 AnimatedBuilder(推荐)
避免整个 Widget 重建,性能更好。
class AnimatedBuilderDemo extends StatefulWidget {
const AnimatedBuilderDemo({super.key});
State<AnimatedBuilderDemo> createState() => _AnimatedBuilderDemoState();
}
class _AnimatedBuilderDemoState extends State<AnimatedBuilderDemo>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _sizeAnimation;
late Animation<Color?> _colorAnimation;
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
);
_sizeAnimation = Tween<double>(begin: 50, end: 200).animate(
CurvedAnimation(parent: _controller, curve: Curves.elasticOut),
);
_colorAnimation = ColorTween(begin: Colors.blue, end: Colors.red).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
);
}
void dispose() {
_controller.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('AnimatedBuilder')),
body: Center(
// AnimatedBuilder 只重建 builder 返回的部分
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Container(
width: _sizeAnimation.value,
height: _sizeAnimation.value,
decoration: BoxDecoration(
color: _colorAnimation.value,
borderRadius: BorderRadius.circular(_sizeAnimation.value / 4),
),
child: child, // child 不会重建
);
},
child: const Center(
child: Text('不会重建', style: TextStyle(color: Colors.white)),
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
if (_controller.status == AnimationStatus.completed) {
_controller.reverse();
} else {
_controller.forward();
}
},
child: const Icon(Icons.play_arrow),
),
);
}
}
Tween 动画
Tween 定义动画的起始值和结束值。
// 数值
Tween<double>(begin: 0, end: 100)
// 颜色
ColorTween(begin: Colors.blue, end: Colors.red)
// 偏移
Tween<Offset>(begin: const Offset(0, 0), end: const Offset(1, 1))
// 边框
BorderRadiusTween(
begin: BorderRadius.circular(0),
end: BorderRadius.circular(50),
)
// 装饰
DecorationTween(
begin: BoxDecoration(color: Colors.blue),
end: BoxDecoration(color: Colors.red),
)
// 自定义(使用 chain)
Tween<double>(begin: 0, end: 1)
.chain(CurveTween(curve: Curves.easeInOut))
.animate(_controller)
CurvedAnimation(曲线动画)
使用不同的曲线控制动画节奏。
// 使用曲线
final curvedAnimation = CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut, // 正向曲线
reverseCurve: Curves.easeOut, // 反向曲线
);
final animation = Tween<double>(begin: 0, end: 100).animate(curvedAnimation);
// 常用曲线
Curves.linear // 线性
Curves.easeIn // 慢入
Curves.easeOut // 慢出
Curves.easeInOut // 慢入慢出
Curves.bounceIn // 弹跳入
Curves.bounceOut // 弹跳出
Curves.elasticIn // 弹性入
Curves.elasticOut // 弹性出
Curves.fastOutSlowIn // 快出慢入(Material 推荐)
内置 Transition Widgets
Flutter 提供了一些内置的过渡动画 Widget:
// 位移动画
SlideTransition(
position: Tween<Offset>(
begin: const Offset(-1, 0), // 从左边滑入
end: Offset.zero,
).animate(_controller),
child: Container(...),
)
// 缩放动画
ScaleTransition(
scale: Tween<double>(begin: 0, end: 1).animate(_controller),
child: Container(...),
)
// 旋转动画
RotationTransition(
turns: Tween<double>(begin: 0, end: 2).animate(_controller), // 旋转2圈
child: Container(...),
)
// 透明度动画
FadeTransition(
opacity: Tween<double>(begin: 0, end: 1).animate(_controller),
child: Container(...),
)
// 大小动画
SizeTransition(
sizeFactor: Tween<double>(begin: 0, end: 1).animate(_controller),
child: Container(...),
)
// 装饰动画
DecoratedBoxTransition(
decoration: DecorationTween(
begin: const BoxDecoration(color: Colors.blue),
end: const BoxDecoration(color: Colors.red),
).animate(_controller),
child: Container(width: 100, height: 100),
)
组合多个动画
class MultiAnimationDemo extends StatefulWidget {
const MultiAnimationDemo({super.key});
State<MultiAnimationDemo> createState() => _MultiAnimationDemoState();
}
class _MultiAnimationDemoState extends State<MultiAnimationDemo>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _scaleAnimation;
late Animation<double> _rotationAnimation;
late Animation<Offset> _slideAnimation;
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
);
// 0-50% 时间:缩放
_scaleAnimation = Tween<double>(begin: 0.5, end: 1).animate(
CurvedAnimation(
parent: _controller,
curve: const Interval(0, 0.5, curve: Curves.easeOut),
),
);
// 25-75% 时间:旋转
_rotationAnimation = Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(
parent: _controller,
curve: const Interval(0.25, 0.75, curve: Curves.easeInOut),
),
);
// 50-100% 时间:滑动
_slideAnimation = Tween<Offset>(
begin: Offset.zero,
end: const Offset(0.5, 0),
).animate(
CurvedAnimation(
parent: _controller,
curve: const Interval(0.5, 1, curve: Curves.easeIn),
),
);
}
void dispose() {
_controller.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('组合动画')),
body: Center(
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return SlideTransition(
position: _slideAnimation,
child: RotationTransition(
turns: _rotationAnimation,
child: ScaleTransition(
scale: _scaleAnimation,
child: Container(
width: 100,
height: 100,
color: Colors.blue,
),
),
),
);
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
if (_controller.status == AnimationStatus.completed) {
_controller.reverse();
} else {
_controller.forward();
}
},
child: const Icon(Icons.play_arrow),
),
);
}
}
🦸 Hero 动画
Hero 动画用于页面间的共享元素过渡,最常见的场景是图片从列表到详情页的放大效果。
// 列表页
class HeroListPage extends StatelessWidget {
const HeroListPage({super.key});
final List<String> images = const [
'https://picsum.photos/200/200?random=1',
'https://picsum.photos/200/200?random=2',
'https://picsum.photos/200/200?random=3',
];
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Hero 动画')),
body: GridView.builder(
padding: const EdgeInsets.all(16),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
),
itemCount: images.length,
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => HeroDetailPage(
imageUrl: images[index],
tag: 'hero-$index',
),
),
);
},
child: Hero(
tag: 'hero-$index', // 两个页面的 tag 必须相同
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.network(
images[index],
fit: BoxFit.cover,
),
),
),
);
},
),
);
}
}
// 详情页
class HeroDetailPage extends StatelessWidget {
final String imageUrl;
final String tag;
const HeroDetailPage({
super.key,
required this.imageUrl,
required this.tag,
});
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
),
body: Center(
child: Hero(
tag: tag, // 与列表页相同的 tag
child: Image.network(
imageUrl,
fit: BoxFit.contain,
),
),
),
);
}
}
自定义 Hero 动画
Hero(
tag: 'custom-hero',
// 自定义飞行过程中的 Widget
flightShuttleBuilder: (
flightContext,
animation,
flightDirection,
fromHeroContext,
toHeroContext,
) {
return AnimatedBuilder(
animation: animation,
builder: (context, child) {
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(
(1 - animation.value) * 12, // 圆角渐变
),
boxShadow: [
BoxShadow(
color: Colors.black26,
blurRadius: animation.value * 20, // 阴影渐变
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular((1 - animation.value) * 12),
child: Image.network(imageUrl, fit: BoxFit.cover),
),
);
},
);
},
child: Image.network(imageUrl),
)
🔄 页面转场动画
使用 PageRouteBuilder
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) {
return const DetailPage();
},
transitionsBuilder: (context, animation, secondaryAnimation, child) {
// 淡入淡出
return FadeTransition(
opacity: animation,
child: child,
);
},
transitionDuration: const Duration(milliseconds: 300),
),
);
// 滑动进入
transitionsBuilder: (context, animation, secondaryAnimation, child) {
final offsetAnimation = Tween<Offset>(
begin: const Offset(1, 0), // 从右边滑入
end: Offset.zero,
).animate(CurvedAnimation(
parent: animation,
curve: Curves.easeOutCubic,
));
return SlideTransition(
position: offsetAnimation,
child: child,
);
}
// 缩放进入
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return ScaleTransition(
scale: Tween<double>(begin: 0.8, end: 1).animate(
CurvedAnimation(parent: animation, curve: Curves.easeOutCubic),
),
child: FadeTransition(
opacity: animation,
child: child,
),
);
}
封装通用转场动画
class FadePageRoute<T> extends PageRouteBuilder<T> {
final Widget page;
FadePageRoute({required this.page})
: super(
pageBuilder: (context, animation, secondaryAnimation) => page,
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(opacity: animation, child: child);
},
transitionDuration: const Duration(milliseconds: 300),
);
}
class SlidePageRoute<T> extends PageRouteBuilder<T> {
final Widget page;
final AxisDirection direction;
SlidePageRoute({
required this.page,
this.direction = AxisDirection.right,
}) : super(
pageBuilder: (context, animation, secondaryAnimation) => page,
transitionsBuilder: (context, animation, secondaryAnimation, child) {
Offset begin;
switch (direction) {
case AxisDirection.up:
begin = const Offset(0, 1);
break;
case AxisDirection.down:
begin = const Offset(0, -1);
break;
case AxisDirection.left:
begin = const Offset(1, 0);
break;
case AxisDirection.right:
begin = const Offset(-1, 0);
break;
}
return SlideTransition(
position: Tween<Offset>(begin: begin, end: Offset.zero).animate(
CurvedAnimation(parent: animation, curve: Curves.easeOutCubic),
),
child: child,
);
},
transitionDuration: const Duration(milliseconds: 300),
);
}
// 使用
Navigator.push(context, FadePageRoute(page: const DetailPage()));
Navigator.push(context, SlidePageRoute(page: const DetailPage(), direction: AxisDirection.up));
🎨 Lottie 动画
Lottie 可以播放 AE 导出的 JSON 动画文件,效果精美。
添加依赖
dependencies:
lottie: ^3.1.0
基本用法
import 'package:lottie/lottie.dart';
// 从 assets 加载
Lottie.asset(
'assets/animations/loading.json',
width: 200,
height: 200,
)
// 从网络加载
Lottie.network(
'https://assets.lottiefiles.com/packages/xxx.json',
)
// 循环播放
Lottie.asset(
'assets/animations/loading.json',
repeat: true,
)
// 单次播放
Lottie.asset(
'assets/animations/success.json',
repeat: false,
)
控制 Lottie 动画
class LottieControlDemo extends StatefulWidget {
const LottieControlDemo({super.key});
State<LottieControlDemo> createState() => _LottieControlDemoState();
}
class _LottieControlDemoState extends State<LottieControlDemo>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
void initState() {
super.initState();
_controller = AnimationController(vsync: this);
}
void dispose() {
_controller.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Lottie 控制')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Lottie.asset(
'assets/animations/heart.json',
controller: _controller,
onLoaded: (composition) {
_controller.duration = composition.duration;
},
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: const Icon(Icons.play_arrow),
onPressed: () => _controller.forward(),
),
IconButton(
icon: const Icon(Icons.pause),
onPressed: () => _controller.stop(),
),
IconButton(
icon: const Icon(Icons.replay),
onPressed: () {
_controller.reset();
_controller.forward();
},
),
IconButton(
icon: const Icon(Icons.repeat),
onPressed: () => _controller.repeat(),
),
],
),
],
),
),
);
}
}
获取 Lottie 动画资源:
- LottieFiles - 免费动画库
- Icons8 Animated Icons
🎯 实战:加载动画组件
// lib/widgets/loading_widget.dart
import 'package:flutter/material.dart';
class LoadingWidget extends StatefulWidget {
final double size;
final Color color;
const LoadingWidget({
super.key,
this.size = 50,
this.color = Colors.blue,
});
State<LoadingWidget> createState() => _LoadingWidgetState();
}
class _LoadingWidgetState extends State<LoadingWidget>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 1500),
vsync: this,
)..repeat();
}
void dispose() {
_controller.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return SizedBox(
width: widget.size,
height: widget.size,
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Stack(
alignment: Alignment.center,
children: List.generate(3, (index) {
final delay = index * 0.2;
final animation = Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(delay, delay + 0.6, curve: Curves.easeInOut),
),
);
return Transform.scale(
scale: animation.value,
child: Container(
width: widget.size,
height: widget.size,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: widget.color.withOpacity(1 - animation.value),
),
),
);
}),
);
},
),
);
}
}
// 使用
const LoadingWidget()
LoadingWidget(size: 80, color: Colors.red)
📝 小结
这一章我们学习了 Flutter 动画:
隐式动画(简单场景)
AnimatedContainer- 容器属性动画AnimatedOpacity- 透明度动画AnimatedPositioned- 位置动画AnimatedCrossFade- 切换动画
显式动画(复杂场景)
AnimationController- 动画控制器Tween- 定义动画值范围CurvedAnimation- 曲线动画AnimatedBuilder- 高效重建
特殊动画
Hero- 页面间共享元素PageRouteBuilder- 页面转场Lottie- AE 动画
动画使用建议
- 简单场景用隐式动画 - 代码少,易维护
- 复杂场景用显式动画 - 完全控制
- 记得释放 Controller - 在 dispose 中调用
- 使用 AnimatedBuilder - 避免不必要的重建
- 选择合适的曲线 - 让动画更自然
💪 练习题
- 使用隐式动画实现一个点赞按钮(点击放大变色)
- 使用显式动画实现一个旋转的加载指示器
- 实现一个带 Hero 动画的图片列表
🚀 下一步
学会了动画后,下一章我们来学习 调试与性能优化!
由 编程指南 提供
