跳到主要内容

动画

精心设计的动画能够让用户界面更加直观,提升应用的精致感,并改善用户体验。Flutter 提供了强大的动画支持,让开发者可以轻松实现各种动画效果。许多 Widget(尤其是 Material 组件)都内置了标准的动效,同时 Flutter 也支持完全自定义的动画效果。

动画类型概述

Flutter 中的动画主要分为两大类:

类型特点适用场景
隐式动画框架自动管理,代码简单简单的属性变化动画
显式动画开发者完全控制,灵活性高复杂的、需要精确控制的动画

如何选择动画方式?

根据你的需求,可以按照以下思路选择合适的动画实现方式:

  1. 只需要简单动画? 使用预置的隐式动画 Widget(如 AnimatedContainerAnimatedOpacity
  2. 预置 Widget 不够用? 使用 TweenAnimationBuilder 创建自定义隐式动画
  3. 需要精确控制动画过程? 使用内置的显式动画 Widget(如 SlideTransitionFadeTransition
  4. 需要完全自定义? 使用 AnimatedBuilderAnimatedWidget 创建自定义显式动画

动画基础概念

Animation 对象

Animation 对象是 Flutter 动画系统的核心。它是一个抽象类,能够:

  • 知道当前的值(通过 .value 访问)
  • 知道当前的状态(完成、 dismissed 等)
  • 生成两个值之间的插值

重要理解Animation 对象本身不涉及屏幕渲染或 build() 函数。它只是一个值生成器,需要配合其他 Widget 使用才能产生视觉效果。

// Animation 对象的基本用法
Animation<double> animation; // 定义

// 访问当前值
double currentValue = animation.value;

// 监听值变化
animation.addListener(() {
print('当前值: ${animation.value}');
});

// 监听状态变化
animation.addStatusListener((status) {
print('状态: $status');
});

AnimationController

AnimationController 是一个特殊的 Animation 对象,它在每一帧生成一个新值。默认情况下,它在给定的持续时间内线性生成 0.0 到 1.0 之间的数值。

class MyAnimation extends StatefulWidget {
@override
_MyAnimationState createState() => _MyAnimationState();
}

class _MyAnimationState extends State<MyAnimation>
with SingleTickerProviderStateMixin { // 需要 mixin
late AnimationController _controller;

@override
void initState() {
super.initState();

_controller = AnimationController(
duration: const Duration(seconds: 2), // 持续时间
vsync: this, // 防止离屏动画消耗资源
);
}

@override
void dispose() {
_controller.dispose(); // 必须释放资源
super.dispose();
}
}

关于 vsync 参数vsync 参数用于防止离屏动画消耗不必要的资源。通过将 SingleTickerProviderStateMixin 添加到类定义中,可以将 Stateful 对象作为 vsync 使用。

Tween(补间)

默认情况下,AnimationController 的值范围是 0.0 到 1.0。如果需要不同的范围或数据类型,可以使用 Tween

// 数值范围
final tween = Tween<double>(begin: -200, end: 0);

// 颜色渐变
final colorTween = ColorTween(begin: Colors.red, end: Colors.blue);

// 尺寸变化
final sizeTween = SizeTween(
begin: Size(100, 100),
end: Size(200, 200),
);

// 使用 Tween
Animation<double> animation = Tween<double>(begin: 0, end: 300).animate(_controller);

Tween 的工作原理Tween 是一个无状态对象,它只定义了从输入范围到输出范围的映射。它的 evaluate() 方法将动画的当前值(通常是 0.0 到 1.0)转换为实际的动画值。

CurvedAnimation(动画曲线)

CurvedAnimation 定义动画的非线性进度,让动画更加自然。

final curvedAnimation = CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
);

// 常用曲线
Curves.linear // 线性(匀速)
Curves.ease // 默认缓动
Curves.easeIn // 开始慢,然后加速
Curves.easeOut // 开始快,然后减速
Curves.easeInOut // 两头慢,中间快
Curves.bounceIn // 弹跳进入
Curves.bounceOut // 弹跳出
Curves.elasticIn // 弹性进入
Curves.elasticOut // 弹性出
Curves.fastOutSlowIn // 快出慢进

// 自定义曲线
class ShakeCurve extends Curve {
@override
double transform(double t) => sin(t * pi * 2);
}

隐式动画

隐式动画是最简单的动画方式,使用带 Animated 前缀的 Widget。框架会自动处理动画的启动和停止,开发者只需指定目标值即可。

AnimatedContainer

class AnimatedContainerExample extends StatefulWidget {
@override
_AnimatedContainerExampleState createState() =>
_AnimatedContainerExampleState();
}

class _AnimatedContainerExampleState
extends State<AnimatedContainerExample> {
bool _isExpanded = false;

@override
Widget build(BuildContext context) {
return Column(
children: [
AnimatedContainer(
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
width: _isExpanded ? 200 : 100,
height: _isExpanded ? 200 : 100,
decoration: BoxDecoration(
color: _isExpanded ? Colors.blue : Colors.red,
borderRadius: BorderRadius.circular(_isExpanded ? 50 : 0),
),
child: Center(child: Text('点击改变')),
),
SizedBox(height: 20),
ElevatedButton(
onPressed: () {
setState(() => _isExpanded = !_isExpanded);
},
child: Text(_isExpanded ? '缩小' : '放大'),
),
],
);
}
}

AnimatedOpacity

AnimatedOpacity(
opacity: _visible ? 1.0 : 0.0,
duration: Duration(milliseconds: 500),
child: Container(
width: 100,
height: 100,
color: Colors.blue,
),
)

AnimatedPositioned

Stack(
children: [
AnimatedPositioned(
duration: Duration(milliseconds: 500),
left: _isMoved ? 200 : 50,
top: _isMoved ? 200 : 50,
child: Container(
width: 50,
height: 50,
color: Colors.blue,
),
),
],
)

AnimatedDefaultTextStyle

AnimatedDefaultTextStyle(
duration: Duration(milliseconds: 300),
style: TextStyle(
fontSize: _isLarge ? 32 : 16,
color: _isLarge ? Colors.blue : Colors.black,
fontWeight: _isLarge ? FontWeight.bold : FontWeight.normal,
),
child: Text('动画文本'),
)

TweenAnimationBuilder:自定义隐式动画

当预置的隐式动画 Widget 不能满足需求时,可以使用 TweenAnimationBuilder 创建自定义隐式动画:

class CustomImplicitAnimation extends StatefulWidget {
@override
_CustomImplicitAnimationState createState() => _CustomImplicitAnimationState();
}

class _CustomImplicitAnimationState extends State<CustomImplicitAnimation> {
double _value = 0;

@override
Widget build(BuildContext context) {
return Column(
children: [
TweenAnimationBuilder<double>(
tween: Tween<double>(begin: 0, end: _value),
duration: Duration(milliseconds: 500),
curve: Curves.easeOutCubic,
builder: (context, value, child) {
// value 从 begin 动画过渡到 end
return Transform.rotate(
angle: value * 2 * pi, // 旋转
child: Container(
width: 100,
height: 100,
color: Colors.blue,
child: Center(
child: Text(
'${(value * 100).toStringAsFixed(0)}%',
style: TextStyle(color: Colors.white),
),
),
),
);
},
),
SizedBox(height: 20),
Slider(
value: _value,
onChanged: (newValue) {
setState(() => _value = newValue);
},
),
],
);
}
}

TweenAnimationBuilder 的优势

  • 不需要管理 AnimationController
  • 代码更简洁
  • 适合一次性的属性变化动画

内置过渡动画(显式动画 Widget)

Flutter 提供了一系列内置的过渡动画 Widget,它们结合了隐式动画的简单性和显式动画的灵活性。这些 Widget 接受 Animation 对象作为参数,但不需要手动管理监听器。

SlideTransition(滑动过渡)

class SlideTransitionExample extends StatefulWidget {
@override
_SlideTransitionExampleState createState() => _SlideTransitionExampleState();
}

class _SlideTransitionExampleState extends State<SlideTransitionExample>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<Offset> _animation;

@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(milliseconds: 500),
vsync: this,
);
_animation = Tween<Offset>(
begin: Offset(-1, 0), // 从左侧
end: Offset.zero, // 到中间
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.easeOut,
));
}

@override
void dispose() {
_controller.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return Column(
children: [
SlideTransition(
position: _animation,
child: Container(
width: 100,
height: 100,
color: Colors.blue,
child: Center(child: Text('滑动')),
),
),
ElevatedButton(
onPressed: () => _controller.forward(),
child: Text('滑入'),
),
],
);
}
}

FadeTransition(淡入淡出)

FadeTransition(
opacity: _animation, // Animation<double> 类型
child: Container(
width: 100,
height: 100,
color: Colors.blue,
),
)

ScaleTransition(缩放过渡)

ScaleTransition(
scale: _animation, // Animation<double> 类型
child: Container(
width: 100,
height: 100,
color: Colors.blue,
),
)

RotationTransition(旋转过渡)

RotationTransition(
turns: _animation, // Animation<double> 类型,单位是圈
child: Container(
width: 100,
height: 100,
color: Colors.blue,
),
)

SizeTransition(尺寸过渡)

SizeTransition(
sizeFactor: _animation, // Animation<double> 类型
axis: Axis.vertical, // 垂直或水平
child: Container(
width: 100,
height: 100,
color: Colors.blue,
),
)

PositionedTransition(位置过渡)

Stack(
children: [
PositionedTransition(
rect: RelativeRectTween(
begin: RelativeRect.fromLTRB(0, 0, 200, 200),
end: RelativeRect.fromLTRB(100, 100, 0, 0),
).animate(_animation),
child: Container(
color: Colors.blue,
),
),
],
)

DecoratedBoxTransition(装饰过渡)

DecoratedBoxTransition(
decoration: DecorationTween(
begin: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(0),
),
end: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(50),
),
).animate(_animation),
child: Container(
width: 100,
height: 100,
),
)

显式动画

显式动画提供更精细的控制,使用 AnimationController 和 Animation。

基本结构

class ExplicitAnimationExample extends StatefulWidget {
@override
_ExplicitAnimationExampleState createState() =>
_ExplicitAnimationExampleState();
}

class _ExplicitAnimationExampleState extends State<ExplicitAnimationExample>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;

@override
void initState() {
super.initState();

// 创建动画控制器
_controller = AnimationController(
duration: Duration(seconds: 1),
vsync: this,
);

// 创建动画
_animation = Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
);
}

@override
void dispose() {
_controller.dispose();
super.dispose();
}

void _startAnimation() {
if (_controller.isCompleted) {
_controller.reverse();
} else {
_controller.forward();
}
}

@override
Widget build(BuildContext context) {
return Column(
children: [
AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Opacity(
opacity: _animation.value,
child: Transform.scale(
scale: _animation.value,
child: Container(
width: 100,
height: 100,
color: Colors.blue,
),
),
);
},
),
SizedBox(height: 20),
ElevatedButton(
onPressed: _startAnimation,
child: Text('播放动画'),
),
],
);
}
}

AnimationController 方法

_controller.forward()    // 从头播放到尾
_controller.reverse() // 从尾播放到头
_controller.repeat() // 重复播放
_controller.stop() // 停止
_controller.reset() // 重置到初始状态
_controller.animateTo(0.5) // 动画到指定值

Tween 类型

// 数值
Tween<double>(begin: 0, end: 100)

// 颜色
ColorTween(begin: Colors.red, end: Colors.blue)

// 尺寸
SizeTween(begin: Size(100, 100), end: Size(200, 200))

// 位置
OffsetTween(begin: Offset.zero, end: Offset(100, 100))

// 边界
EdgeInsetsTween(begin: EdgeInsets.all(10), end: EdgeInsets.all(20))

// 对齐
AlignmentTween(begin: Alignment.topLeft, end: Alignment.bottomRight)

动画曲线

Curves.linear       // 线性
Curves.ease // 缓入缓出
Curves.easeIn // 缓入
Curves.easeOut // 缓出
Curves.easeInOut // 缓入缓出
Curves.bounceIn // 弹跳进入
Curves.bounceOut // 弹跳出
Curves.elasticIn // 弹性进入
Curves.elasticOut // 弹性出

交错动画

交错动画让多个动画按顺序或重叠执行:

class StaggeredAnimationExample extends StatefulWidget {
@override
_StaggeredAnimationExampleState createState() =>
_StaggeredAnimationExampleState();
}

class _StaggeredAnimationExampleState extends State<StaggeredAnimationExample>
with SingleTickerProviderStateMixin {
late AnimationController _controller;

@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(milliseconds: 1500),
vsync: this,
);
}

@override
void dispose() {
_controller.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return Column(
children: [
AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: List.generate(4, (index) {
final delay = index * 0.2;
final animation = Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(
delay,
delay + 0.5,
curve: Curves.easeOut,
),
),
);

return Transform.translate(
offset: Offset(0, -50 * animation.value),
child: Container(
width: 40,
height: 40,
color: Colors.primaries[index * 2],
),
);
}),
);
},
),
SizedBox(height: 20),
ElevatedButton(
onPressed: () {
if (_controller.isCompleted) {
_controller.reverse();
} else {
_controller.forward();
}
},
child: Text('播放'),
),
],
);
}
}

Hero 动画

Hero 动画在页面切换时实现共享元素过渡:

// 源页面
GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => DetailPage()),
);
},
child: Hero(
tag: 'hero-image',
child: Image.network(
'https://picsum.photos/100',
width: 100,
height: 100,
),
),
)

// 目标页面
Hero(
tag: 'hero-image', // 相同的 tag
child: Image.network(
'https://picsum.photos/400',
width: 300,
height: 300,
),
)

物理动画

使用物理模拟实现更自然的动画效果。物理动画模拟真实世界的行为,例如弹簧、重力、摩擦等,让动画更加逼真。

弹簧动画

class SpringAnimationExample extends StatefulWidget {
@override
_SpringAnimationExampleState createState() => _SpringAnimationExampleState();
}

class _SpringAnimationExampleState extends State<SpringAnimationExample>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;

@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: Duration(seconds: 1),
);

// 使用 SpringSimulation 创建弹簧动画
_animation = Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(
parent: _controller,
curve: Curves.elasticOut, // 使用内置的弹性曲线
),
);
}

@override
void dispose() {
_controller.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return Column(
children: [
AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Transform.scale(
scale: _animation.value,
child: Container(
width: 100,
height: 100,
color: Colors.blue,
child: Center(child: Text('弹跳')),
),
);
},
),
SizedBox(height: 20),
ElevatedButton(
onPressed: () {
_controller.forward(from: 0);
},
child: Text('弹跳'),
),
],
);
}
}

自定义物理模拟

使用 Simulation 类创建自定义物理效果:

void _animateWithPhysics() {
final simulation = SpringSimulation(
SpringDescription.withDampingRatio(
mass: 1.0, // 质量
stiffness: 100.0, // 刚度
ratio: 0.7, // 阻尼比
),
0.0, // 起始位置
1.0, // 结束位置
0.0, // 初始速度
);

_controller.animateWith(simulation);
}

物理模拟参数说明

参数说明影响
mass质量质量越大,惯性越大,动画越慢
stiffness刚度刚度越大,弹簧越硬,振荡越快
ratio阻尼比1.0 为临界阻尼(不振荡),小于 1.0 会振荡

AnimatedBuilder 详解

AnimatedBuilder 是创建自定义显式动画的核心工具,它可以高效地重建 Widget 树的一部分。

基本用法

AnimatedBuilder(
animation: _animation,
builder: (context, child) {
// 每帧都会调用
return Transform.scale(
scale: _animation.value,
child: child, // 使用缓存的 child
);
},
// child 只构建一次,提高性能
child: Container(
width: 100,
height: 100,
color: Colors.blue,
),
)

性能优化关键child 参数只构建一次,然后在每次动画帧中重用。如果将所有内容放在 builder 中,每帧都会重建整个子树。

组合多个动画

class MultiAnimationExample extends StatefulWidget {
@override
_MultiAnimationExampleState createState() => _MultiAnimationExampleState();
}

class _MultiAnimationExampleState extends State<MultiAnimationExample>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _widthAnimation;
late Animation<double> _heightAnimation;
late Animation<Color?> _colorAnimation;

@override
void initState() {
super.initState();

_controller = AnimationController(
duration: Duration(milliseconds: 1500),
vsync: this,
);

// 使用 Interval 创建交错动画
_widthAnimation = Tween<double>(begin: 100, end: 200).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(0.0, 0.5, curve: Curves.easeOut), // 前半段
),
);

_heightAnimation = Tween<double>(begin: 100, end: 150).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(0.3, 0.8, curve: Curves.easeOut), // 中间段
),
);

_colorAnimation = ColorTween(begin: Colors.blue, end: Colors.red).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(0.5, 1.0, curve: Curves.easeOut), // 后半段
),
);
}

@override
void dispose() {
_controller.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return Column(
children: [
AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Container(
width: _widthAnimation.value,
height: _heightAnimation.value,
color: _colorAnimation.value,
);
},
),
ElevatedButton(
onPressed: () => _controller.forward(from: 0),
child: Text('播放组合动画'),
),
],
);
}
}

AnimatedBuilder vs AnimatedWidget

除了 AnimatedBuilder,还可以使用 AnimatedWidget 创建自定义动画:

// 使用 AnimatedWidget
class AnimatedBox extends AnimatedWidget {
const AnimatedBox({Key? key, required Animation<double> animation})
: super(key: key, listenable: animation);

@override
Widget build(BuildContext context) {
final animation = listenable as Animation<double>;
return Container(
width: animation.value * 100,
height: animation.value * 100,
color: Colors.blue,
);
}
}

// 使用
AnimatedBox(animation: _animation)

两种方式的对比

特性AnimatedBuilderAnimatedWidget
代码组织内联在 build 中独立类
复用性较低较高
适用场景简单动画可复用的动画组件

AnimatedList 动画列表

AnimatedList 为列表项的添加和删除提供内置动画支持:

class AnimatedListExample extends StatefulWidget {
@override
_AnimatedListExampleState createState() => _AnimatedListExampleState();
}

class _AnimatedListExampleState extends State<AnimatedListExample> {
final GlobalKey<AnimatedListState> _listKey = GlobalKey<AnimatedListState>();
final List<String> _items = ['项目 1', '项目 2', '项目 3'];
int _counter = 4;

void _addItem() {
final newIndex = _items.length;
final newItem = '项目 $_counter';
_items.add(newItem);
_counter++;

_listKey.currentState?.insertItem(newIndex);
}

void _removeItem(int index) {
final removedItem = _items[index];
_items.removeAt(index);

_listKey.currentState?.removeItem(
index,
(context, animation) => _buildItem(removedItem, animation),
);
}

Widget _buildItem(String item, Animation<double> animation) {
return SizeTransition(
sizeFactor: animation,
child: ListTile(
title: Text(item),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () => _removeItem(_items.indexOf(item)),
),
),
);
}

@override
Widget build(BuildContext context) {
return Column(
children: [
Expanded(
child: AnimatedList(
key: _listKey,
initialItemCount: _items.length,
itemBuilder: (context, index, animation) {
return _buildItem(_items[index], animation);
},
),
),
ElevatedButton(
onPressed: _addItem,
child: Text('添加项目'),
),
],
);
}
}

性能优化

1. 使用 const 构造函数

// 好:const Widget 只构建一次
const Text('不变的文本')

// 差:每次重建都创建新实例
Text('不变的文本')

2. 正确使用 AnimatedBuilder 的 child 参数

// 好:child 只构建一次
AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Transform.scale(
scale: _animation.value,
child: child,
);
},
child: ExpensiveWidget(), // 复杂的 Widget
)

// 差:每帧都重建整个子树
AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Transform.scale(
scale: _animation.value,
child: ExpensiveWidget(), // 每帧都重建!
);
},
)

3. 避免在动画中重建整个 Widget 树

// 差:整个页面每帧都重建
setState(() {
_animationValue = animation.value;
});

// 好:只重建动画部分
AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Transform.translate(
offset: Offset(_animation.value * 100, 0),
child: child,
);
},
child: RestOfTheUI(),
)

4. 使用 RepaintBoundary

对于复杂动画,使用 RepaintBoundary 减少重绘范围:

RepaintBoundary(
child: AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return CustomPaint(
painter: MyPainter(_animation.value),
);
},
),
)

5. 合理选择动画类型

场景推荐方案
简单属性变化隐式动画(AnimatedXxx)
复杂但一次性的动画TweenAnimationBuilder
需要精确控制显式动画 + AnimatedBuilder
可复用的动画组件AnimatedWidget
列表项动画AnimatedList

小结

本章我们详细学习了 Flutter 动画系统:

  1. 动画类型:隐式动画和显式动画的选择
  2. 核心概念:Animation、AnimationController、Tween、CurvedAnimation
  3. 隐式动画:AnimatedContainer、AnimatedOpacity、TweenAnimationBuilder
  4. 内置过渡动画:SlideTransition、FadeTransition、ScaleTransition 等
  5. 显式动画:AnimationController 的完整控制
  6. AnimatedBuilder:自定义动画的高效方式
  7. 交错动画:Interval 实现多动画协调
  8. Hero 动画:页面间共享元素过渡
  9. 物理动画:弹簧模拟等真实物理效果
  10. 性能优化:正确使用 child 参数、RepaintBoundary 等

练习

  1. 创建一个带动画的按钮,按下时有缩放和颜色变化效果
  2. 实现一个卡片翻转动画(3D 旋转效果)
  3. 创建一个带动画的列表项删除效果
  4. 实现一个波浪效果的加载动画
  5. 使用 AnimatedList 实现一个待办事项列表

参考资源