动画是提升用户体验的关键因素,能够使应用界面更加生动、直观,增强用户交互感。Flutter 提供了强大的动画系统,支持各种复杂的动画效果实现。本节课将详细介绍 Flutter 中的动画类型和实现方式,从基础的隐式动画到复杂的自定义显式动画,帮助你掌握为应用添加流畅动画效果的技能。
一、Flutter 动画基础
Flutter 动画系统基于以下核心概念:
- Animation:一个生成介于 0.0 和 1.0 之间数值的对象,它本身不包含渲染内容,只提供动画数值
- Curve:定义动画进度的速度变化,如加速、减速等
- Controller:控制动画的播放、暂停、反向等,管理动画生命周期
- Tween:定义动画的起始值和结束值,将 Animation 提供的 0-1 数值映射到实际需要的数值范围
- Animatable:可以生成 Tween 的对象,支持更复杂的数值映射
Flutter 动画主要分为两类:
- 隐式动画:由 Flutter 自动管理的动画,只需定义起始和结束状态
- 显式动画:需要手动控制的动画,提供更多自定义选项和控制能力
二、隐式动画
隐式动画(Implicit Animations)是最简单的动画实现方式,Flutter 提供了一系列封装好的隐式动画组件,当组件的某些属性发生变化时,会自动从旧值平滑过渡到新值。
1. AnimatedContainer
AnimatedContainer
是最常用的隐式动画组件,当它的属性(如大小、颜色、边距等)发生变化时,会自动产生过渡动画。
class AnimatedContainerDemo extends StatefulWidget {
const AnimatedContainerDemo({super.key});
@override
State<AnimatedContainerDemo> createState() => _AnimatedContainerDemoState();
}
class _AnimatedContainerDemoState extends State<AnimatedContainerDemo> {
// 控制容器属性的状态变量
bool _isExpanded = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('AnimatedContainer Demo')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedContainer(
// 动画持续时间
duration: const Duration(seconds: 1),
// 动画曲线
curve: Curves.easeInOut,
// 动态变化的属性
width: _isExpanded ? 300 : 100,
height: _isExpanded ? 300 : 100,
color: _isExpanded ? Colors.blue : Colors.red,
padding: _isExpanded
? const EdgeInsets.all(20)
: const EdgeInsets.all(10),
// 容器内的内容
child: const Center(
child: Text(
'Animate me!',
style: TextStyle(color: Colors.white, fontSize: 20),
),
),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
// 点击按钮切换状态,触发动画
setState(() {
_isExpanded = !_isExpanded;
});
},
child: const Text('Toggle Animation'),
),
],
),
),
);
}
}
2. 其他常用隐式动画组件
Flutter 还提供了其他专门用途的隐式动画组件:
AnimatedOpacity
控制组件透明度变化的动画:
class AnimatedOpacityDemo extends StatefulWidget {
const AnimatedOpacityDemo({super.key});
@override
State<AnimatedOpacityDemo> createState() => _AnimatedOpacityDemoState();
}
class _AnimatedOpacityDemoState extends State<AnimatedOpacityDemo> {
double _opacity = 1.0;
void _toggleOpacity() {
setState(() {
_opacity = _opacity == 1.0 ? 0.0 : 1.0;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('AnimatedOpacity Demo')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedOpacity(
opacity: _opacity,
duration: const Duration(seconds: 1),
curve: Curves.fastOutSlowIn,
child: Container(
width: 200,
height: 200,
color: Colors.green,
child: const Center(
child: Text(
'Fade me!',
style: TextStyle(color: Colors.white, fontSize: 24),
),
),
),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: _toggleOpacity,
child: const Text('Toggle Opacity'),
),
],
),
),
);
}
}
AnimatedPositioned
用于 Stack
中,控制子组件位置变化的动画:
class AnimatedPositionedDemo extends StatefulWidget {
const AnimatedPositionedDemo({super.key});
@override
State<AnimatedPositionedDemo> createState() => _AnimatedPositionedDemoState();
}
class _AnimatedPositionedDemoState extends State<AnimatedPositionedDemo> {
bool _isMoved = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('AnimatedPositioned Demo')),
body: Stack(
children: [
AnimatedPositioned(
duration: const Duration(seconds: 1),
curve: Curves.bounceOut,
left: _isMoved ? 200 : 50,
top: _isMoved ? 300 : 100,
width: 100,
height: 100,
child: Container(
color: Colors.purple,
child: const Center(
child: Text(
'Move me!',
style: TextStyle(color: Colors.white),
),
),
),
),
Positioned(
bottom: 50,
left: 0,
right: 0,
child: Center(
child: ElevatedButton(
onPressed: () {
setState(() {
_isMoved = !_isMoved;
});
},
child: const Text('Move the Box'),
),
),
)
],
),
);
}
}
AnimatedPadding、AnimatedSize 等
还有其他类似的隐式动画组件,使用方式大同小异:
AnimatedPadding
:控制内边距变化AnimatedSize
:根据子组件大小自动调整并产生动画AnimatedTransform
:控制变换效果的动画AnimatedDefaultTextStyle
:控制文本样式变化的动画
3. 隐式动画的优缺点
优点:
- 使用简单,只需修改状态即可触发动画
- 无需手动管理动画控制器
- 适合实现简单的过渡效果
缺点:
- 定制化程度低,无法实现复杂动画
- 缺乏精细的控制能力(如暂停、反向播放)
- 多个属性同时动画时难以协调
三、显式动画
显式动画(Explicit Animations)需要手动创建和管理动画控制器,提供了更精细的控制和更大的灵活性,适合实现复杂的动画效果。
1. AnimationController 与 Animation
AnimationController
是显式动画的核心,负责控制动画的时间和状态:
class BasicExplicitAnimation extends StatefulWidget {
const BasicExplicitAnimation({super.key});
@override
State<BasicExplicitAnimation> createState() => _BasicExplicitAnimationState();
}
class _BasicExplicitAnimationState extends State<BasicExplicitAnimation>
with SingleTickerProviderStateMixin {
// 动画控制器
late AnimationController _controller;
// 动画对象
late Animation<double> _animation;
@override
void initState() {
super.initState();
// 初始化动画控制器
_controller = AnimationController(
vsync: this, // 与当前组件生命周期关联
duration: const Duration(seconds: 2), // 动画持续时间
);
// 创建从 0 到 300 的动画
_animation = Tween<double>(begin: 0, end: 300).animate(_controller)
// 监听动画值变化,触发重建
..addListener(() {
setState(() {});
})
// 监听动画状态变化
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
// 动画完成后反向播放
_controller.reverse();
} else if (status == AnimationStatus.dismissed) {
// 动画回到起点后正向播放
_controller.forward();
}
});
// 开始动画
_controller.forward();
}
@override
void dispose() {
// 释放动画控制器资源
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Basic Explicit Animation')),
body: Center(
child: Container(
width: _animation.value, // 使用动画值
height: _animation.value,
color: Colors.orange,
child: const Center(
child: Text(
'Growing Box',
style: TextStyle(color: Colors.white, fontSize: 18),
),
),
),
),
);
}
}
注意:
SingleTickerProviderStateMixin
提供了一个 vsync 回调,用于防止动画在组件不可见时继续消耗资源。如果需要多个动画控制器,应使用TickerProviderStateMixin
。
2. CurvedAnimation 曲线动画
使用 CurvedAnimation
可以为动画添加非线性的速度变化:
class CurvedAnimationDemo extends StatefulWidget {
const CurvedAnimationDemo({super.key});
@override
State<CurvedAnimationDemo> createState() => _CurvedAnimationDemoState();
}
class _CurvedAnimationDemoState extends State<CurvedAnimationDemo>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 2),
);
// 创建曲线动画
final curve = CurvedAnimation(
parent: _controller,
curve: Curves.bounceOut, // 弹跳效果
reverseCurve: Curves.bounceIn, // 反向时的曲线
);
// 使用曲线动画
_animation = Tween<double>(begin: 50, end: 300).animate(curve)
..addListener(() {
setState(() {});
})
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
_controller.reverse();
} else if (status == AnimationStatus.dismissed) {
_controller.forward();
}
});
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Curved Animation')),
body: Center(
child: Container(
width: _animation.value,
height: 100,
color: Colors.pink,
child: const Center(
child: Text(
'Bouncy!',
style: TextStyle(color: Colors.white, fontSize: 20),
),
),
),
),
);
}
}
Flutter 提供了多种预定义的曲线,如:
Curves.linear
:线性动画Curves.easeIn
、Curves.easeOut
、Curves.easeInOut
:缓入、缓出、缓入缓出Curves.bounceIn
、Curves.bounceOut
:弹跳效果Curves.elasticIn
、Curves.elasticOut
:弹性效果
3. Tween 与多属性动画
Tween
定义了动画的取值范围,可以是任意类型。多个 Tween 可以组合实现多属性动画:
class MultiPropertyAnimation extends StatefulWidget {
const MultiPropertyAnimation({super.key});
@override
State<MultiPropertyAnimation> createState() => _MultiPropertyAnimationState();
}
class _MultiPropertyAnimationState extends State<MultiPropertyAnimation>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _sizeAnimation;
late Animation<double> _opacityAnimation;
late Animation<Color?> _colorAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 3),
);
// 大小动画
_sizeAnimation = Tween<double>(
begin: 50,
end: 250,
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));
// 透明度动画
_opacityAnimation = Tween<double>(begin: 0.2, end: 1.0).animate(
CurvedAnimation(
parent: _controller,
curve: const Interval(0.3, 1.0),
), // 延迟开始
);
// 颜色动画
_colorAnimation = ColorTween(begin: Colors.blue, end: Colors.green).animate(
CurvedAnimation(
parent: _controller,
curve: const Interval(0.0, 0.7),
), // 提前结束
);
// 监听动画值变化
_controller.addListener(() {
setState(() {});
});
// 循环播放
_controller.repeat(reverse: true);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Multi-property Animation')),
body: Center(
child: Opacity(
opacity: _opacityAnimation.value,
child: Container(
width: _sizeAnimation.value,
height: _sizeAnimation.value,
decoration: BoxDecoration(
color: _colorAnimation.value,
// 使用 borderRadius 属性(已修复)
borderRadius: BorderRadius.circular(_sizeAnimation.value / 10),
),
child: const Center(
child: Text(
'Fancy!',
style: TextStyle(color: Colors.white, fontSize: 24),
),
),
),
),
),
);
}
}
Interval
可以为不同的动画属性设置不同的时间区间,实现更复杂的动画编排。
4. AnimatedBuilder 优化重建性能
使用 addListener
配合 setState
会导致整个组件重建,使用 AnimatedBuilder
可以只重建需要动画的部分,提高性能:
class AnimatedBuilderDemo extends StatefulWidget {
const AnimatedBuilderDemo({super.key});
@override
State<AnimatedBuilderDemo> createState() => _AnimatedBuilderDemoState();
}
class _AnimatedBuilderDemoState extends State<AnimatedBuilderDemo>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _rotationAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 5),
);
_rotationAnimation = Tween<double>(
begin: 0,
end: 2 * 3.14159,
).animate(CurvedAnimation(parent: _controller, curve: Curves.linear));
_controller.repeat();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('AnimatedBuilder Demo')),
body: Center(
// 使用 AnimatedBuilder 包裹需要动画的部分
child: AnimatedBuilder(
animation: _rotationAnimation,
// builder 方法只重建动画相关部分
builder: (context, child) {
return Transform.rotate(
angle: _rotationAnimation.value,
child: child, // 传入静态子组件,避免重复构建
);
},
// 静态子组件,只会构建一次
child: Container(
width: 200,
height: 200,
decoration: BoxDecoration(
color: Colors.amber,
// 使用 borderRadius 属性(已修复)
borderRadius: BorderRadius.circular(20),
),
child: const Center(
child: Text(
'Spinning!',
style: TextStyle(color: Colors.black, fontSize: 24),
),
),
),
),
),
);
}
}
四、页面过渡动画
Flutter 允许自定义页面之间的切换动画,通过 PageRouteBuilder
可以实现各种过渡效果。
1. 淡入淡出过渡
class FadeTransitionDemo extends StatelessWidget {
const FadeTransitionDemo({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Fade Transition Demo')),
body: Center(
child: ElevatedButton(
child: const Text('Go to Second Screen'),
onPressed: () {
// 导航到第二个页面,使用自定义过渡动画
Navigator.push(
context,
PageRouteBuilder(
transitionDuration: const Duration(seconds: 1), // 过渡时间
// 构建页面内容
pageBuilder: (context, animation, secondaryAnimation) {
return const SecondScreen();
},
// 构建过渡效果
transitionsBuilder:
(context, animation, secondaryAnimation, child) {
// 淡入淡出过渡
return FadeTransition(opacity: animation, child: child);
},
),
);
},
),
),
);
}
}
class SecondScreen extends StatelessWidget {
const SecondScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Second Screen'),
backgroundColor: Colors.green,
),
body: Center(
child: ElevatedButton(
child: const Text('Go Back'),
onPressed: () {
Navigator.pop(context);
},
),
),
);
}
}
2. 滑动过渡
// 在导航时使用这个过渡
PageRouteBuilder(
transitionDuration: const Duration(milliseconds: 500),
pageBuilder: (context, animation, secondaryAnimation) {
return const SecondScreen();
},
transitionsBuilder: (context, animation, secondaryAnimation, child) {
// 滑动过渡 - 从右侧滑入
const begin = Offset(1.0, 0.0); // 起始位置(右侧)
const end = Offset.zero; // 结束位置(屏幕内)
const curve = Curves.easeInOut;
// 创建从 begin 到 end 的动画
var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve));
var offsetAnimation = animation.drive(tween);
return SlideTransition(
position: offsetAnimation,
child: child,
);
},
)
3. 缩放过渡
PageRouteBuilder(
transitionDuration: const Duration(milliseconds: 500),
pageBuilder: (context, animation, secondaryAnimation) {
return const SecondScreen();
},
transitionsBuilder: (context, animation, secondaryAnimation, child) {
// 缩放过渡
return ScaleTransition(
scale: animation.drive(
Tween(begin: 0.0, end: 1.0).chain(
CurveTween(curve: Curves.bounceOut)
)
),
child: child,
);
},
)
4. 组合过渡效果
可以将多种过渡效果组合使用:
PageRouteBuilder(
transitionDuration: const Duration(milliseconds: 700),
pageBuilder: (context, animation, secondaryAnimation) {
return const SecondScreen();
},
transitionsBuilder: (context, animation, secondaryAnimation, child) {
// 组合缩放和淡入效果
return ScaleTransition(
scale: animation.drive(
Tween(begin: 0.8, end: 1.0).chain(
CurveTween(curve: Curves.easeInOut)
)
),
child: FadeTransition(
opacity: animation.drive(
Tween(begin: 0.0, end: 1.0).chain(
CurveTween(curve: const Interval(0.2, 1.0))
)
),
child: child,
),
);
},
)
五、实例:按钮点击缩放效果
实现一个带有点击反馈的按钮,点击时会产生缩放效果:
class BounceButton extends StatefulWidget {
final Widget child;
final VoidCallback onPressed;
final double scaleFactor;
final Duration duration;
const BounceButton({
super.key,
required this.child,
required this.onPressed,
this.scaleFactor = 0.8,
this.duration = const Duration(milliseconds: 300),
});
@override
State<BounceButton> createState() => _BounceButtonState();
}
class _BounceButtonState extends State<BounceButton>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _scaleAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: widget.duration);
_scaleAnimation = Tween<double>(
begin: 1.0,
end: widget.scaleFactor,
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
// 处理按下 - 不直接执行forward,由onTapUp统一控制
void _onTapDown(TapDownDetails details) {
print("按下:准备启动动画");
}
// 处理释放 - 统一控制动画流程,确保完整执行
void _onTapUp(TapUpDetails details) {
print("释放:开始动画流程");
// 先执行完整的正向动画,再执行反向动画,最后触发回调
_controller.forward().then((_) {
_controller.reverse().then((_) {
widget.onPressed();
});
});
}
// 处理取消
void _onTapCancel() {
print("取消:反向动画");
_controller.reverse();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _scaleAnimation,
builder: (context, child) {
return Transform.scale(scale: _scaleAnimation.value, child: child);
},
child: GestureDetector(
onTapDown: _onTapDown,
onTapUp: _onTapUp,
onTapCancel: _onTapCancel,
behavior: HitTestBehavior.opaque,
child: widget.child,
),
);
}
}
// 使用示例
class BounceButtonDemo extends StatelessWidget {
const BounceButtonDemo({super.key});
void _handlePress() {
print('Button pressed!');
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Bounce Button Demo')),
body: Center(
child: BounceButton(
onPressed: _handlePress,
scaleFactor: 0.7,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 20),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(12),
boxShadow: const [
BoxShadow(
color: Colors.black26,
blurRadius: 10,
offset: Offset(0, 4),
),
],
),
child: const Text(
'Click Me',
style: TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
),
),
),
);
}
}
说明:
- 在
_onTapDown
中不直接执行_controller.forward()
,只标记按钮被按下状态 - 在
_onTapUp
中统一控制动画流程,先执行完整的正向动画(按钮缩小),再执行反向动画(按钮恢复),最后才触发业务回调
六、动画性能优化
动画性能对用户体验至关重要,以下是一些优化建议:
- 使用 AnimatedBuilder 减少重建范围:只重建需要动画的部分,避免整个页面重建。
- 避免在动画过程中执行复杂计算:动画回调中应尽量简洁,复杂计算会导致卡顿。
- 使用硬件加速:大多数动画会自动使用硬件加速,但应避免使用会触发软件渲染的操作(如使用
saveLayer
)。 - 控制动画帧率:大多数情况下,60fps 已足够,更高的帧率会消耗更多资源。
- 合理设置动画时长:一般动画时长在 200-300ms 之间,过长会让用户感到延迟。
- 使用
RepaintBoundary
隔离重绘区域:对于频繁重绘的动画组件,使用RepaintBoundary
包裹,避免影响其他组件。
RepaintBoundary(
child: AnimatedWidget(...),
)
- 避免透明度动画与阴影同时使用:这两种效果同时使用会降低性能,可以在动画期间暂时移除阴影。