
// 简报
动画是好应用与卓越应用之间的分水岭。它提供视觉反馈、引导用户注意力,并创造让用户难忘的愉悦体验。Flutter 的动画系统极为强大,但要真正掌握,就必须理解背后更深层的概念。
本指南将带你越过基础动画,进入高级领域。你会学到如何制作自定义物理模拟、复杂过渡,以及自然流畅、引人入胜的动画。这些正是顶尖应用用来打造难忘体验的技巧。
自定义动画控制器
动画控制器(AnimationController)是 Flutter 动画的核心。理解如何创建并编排多个控制器,会为你带来无限的创作可能。
advanced_animation_widget.dart
class AdvancedAnimationWidget extends StatefulWidget {
@override
_AdvancedAnimationWidgetState createState() => _AdvancedAnimationWidgetState();
}
class _AdvancedAnimationWidgetState extends State<AdvancedAnimationWidget>
with TickerProviderStateMixin {
late AnimationController _slideController;
late AnimationController _fadeController;
late AnimationController _scaleController;
late Animation<Offset> _slideAnimation;
late Animation<double> _fadeAnimation;
late Animation<double> _scaleAnimation;
@override
void initState() {
super.initState();
// Create multiple controllers with different durations
_slideController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_fadeController = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
);
_scaleController = AnimationController(
duration: const Duration(milliseconds: 1000),
vsync: this,
);
// Create curved animations
_slideAnimation = Tween<Offset>(
begin: const Offset(0, 1),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _slideController,
curve: Curves.elasticOut,
));
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _fadeController,
curve: Curves.easeInOut,
));
_scaleAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _scaleController,
curve: Curves.bounceOut,
));
}
void _startSequentialAnimation() async {
// Start animations in sequence with delays
_slideController.forward();
await Future.delayed(const Duration(milliseconds: 200));
_fadeController.forward();
await Future.delayed(const Duration(milliseconds: 200));
_scaleController.forward();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: Listenable.merge([_slideController, _fadeController, _scaleController]),
builder: (context, child) {
return SlideTransition(
position: _slideAnimation,
child: FadeTransition(
opacity: _fadeAnimation,
child: ScaleTransition(
scale: _scaleAnimation,
child: Container(
width: 200,
height: 200,
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(20),
),
child: const Center(
child: Text('Animated!', style: TextStyle(color: Colors.white, fontSize: 24)),
),
),
),
),
);
},
);
}
}核心概念
- 使用 TickerProviderStateMixin 管理多个控制器
- 以延迟执行串联多个动画
- 组合不同类型的动画,构成复杂效果
- 务必 dispose 控制器,防止内存泄漏
物理动画
物理模拟能营造最自然的动画手感。Flutter 提供弹簧、重力与摩擦力模拟,让你的界面反应灵敏、充满生气。
spring_animation_widget.dart
class SpringAnimationWidget extends StatefulWidget {
@override
_SpringAnimationWidgetState createState() => _SpringAnimationWidgetState();
}
class _SpringAnimationWidgetState extends State<SpringAnimationWidget>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
);
// Create a spring simulation
final spring = SpringDescription(
mass: 1.0, // Mass of the object
stiffness: 500.0, // Spring stiffness
damping: 20.0, // Damping coefficient
);
final springSimulation = SpringSimulation(spring, 0.0, 1.0, 0.0);
_animation = _controller.drive(
CurveTween(curve: Curve.from(springSimulation)),
);
}
void _startSpringAnimation() {
_controller.reset();
_controller.forward();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Transform.translate(
offset: Offset(0, -100 * _animation.value),
child: Container(
width: 100,
height: 100,
decoration: const BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
),
);
},
);
}
}
// Custom physics simulation
class BouncingBallSimulation extends Simulation {
final double gravity;
final double bounciness;
final double initialVelocity;
BouncingBallSimulation({
required this.gravity,
required this.bounciness,
required this.initialVelocity,
});
@override
double x(double time) {
// Calculate position based on physics
return initialVelocity * time + 0.5 * gravity * time * time;
}
@override
double dx(double time) {
// Calculate velocity
return initialVelocity + gravity * time;
}
@override
bool isDone(double time) {
return false; // Continue indefinitely
}
}物理模拟技巧
- 阻尼(damping)越低越有弹性,越高则越受控
- 调整质量(mass)可改变动画的“重量感”
- 使用重力模拟营造逼真的下坠效果
- 叠加多种物理模拟,构成复杂行为
HERO 动画与过渡
Hero 动画能在页面之间建立无缝过渡。掌握自定义 Hero 动画,打造独特的导航体验,引导用户穿梭于应用的流程之中。
custom_hero_animation.dart
// Custom Hero Animation
class CustomHeroAnimation extends StatelessWidget {
final String heroTag;
final Widget child;
final Duration duration;
const CustomHeroAnimation({
required this.heroTag,
required this.child,
this.duration = const Duration(milliseconds: 500),
});
@override
Widget build(BuildContext context) {
return Hero(
tag: heroTag,
flightShuttleBuilder: (
BuildContext flightContext,
Animation<double> animation,
HeroFlightDirection flightDirection,
BuildContext fromHeroContext,
BuildContext toHeroContext,
) {
return AnimatedBuilder(
animation: animation,
builder: (context, child) {
return Transform.scale(
scale: 1.0 + (animation.value * 0.2),
child: Transform.rotate(
angle: animation.value * 2 * math.pi,
child: Material(
type: MaterialType.transparency,
child: toHeroContext.widget,
),
),
);
},
);
},
child: child,
);
}
}
// Advanced Page Transition
class SlideUpPageRoute<T> extends PageRoute<T> {
final Widget child;
final Duration duration;
SlideUpPageRoute({
required this.child,
this.duration = const Duration(milliseconds: 300),
});
@override
Color? get barrierColor => null;
@override
String? get barrierLabel => null;
@override
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
return child;
}
@override
Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
final slideAnimation = Tween<Offset>(
begin: const Offset(0, 1),
end: Offset.zero,
).animate(CurvedAnimation(
parent: animation,
curve: Curves.easeOutCubic,
));
final fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: animation,
curve: const Interval(0.3, 1.0),
));
return SlideTransition(
position: slideAnimation,
child: FadeTransition(
opacity: fadeAnimation,
child: child,
),
);
}
@override
Duration get transitionDuration => duration;
@override
bool get maintainState => true;
}HERO 最佳实践
- 使用唯一的 hero tag,避免冲突
- 自定义 flightShuttleBuilder,做出独一无二的过渡
- 留意复杂 Hero 动画对性能的影响
- 在不同屏幕尺寸上测试 Hero 动画
CUSTOM PAINT 动画
CustomPainter 让你创作任何想象得到的动画。从形状渐变到粒子系统,自定义绘制赋予你对动画像素级的掌控。
animated_wave_painter.dart
class AnimatedWavePainter extends CustomPainter {
final double animationValue;
final Color waveColor;
AnimatedWavePainter({
required this.animationValue,
required this.waveColor,
});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = waveColor
..style = PaintingStyle.fill;
final path = Path();
final waveHeight = 20.0;
final waveLength = size.width / 2;
path.moveTo(0, size.height / 2);
for (double x = 0; x <= size.width; x += 1) {
final y = size.height / 2 +
waveHeight * math.sin((x / waveLength) * 2 * math.pi + animationValue * 2 * math.pi);
path.lineTo(x, y);
}
path.lineTo(size.width, size.height);
path.lineTo(0, size.height);
path.close();
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return oldDelegate is AnimatedWavePainter &&
oldDelegate.animationValue != animationValue;
}
}
class WaveAnimationWidget extends StatefulWidget {
@override
_WaveAnimationWidgetState createState() => _WaveAnimationWidgetState();
}
class _WaveAnimationWidgetState extends State<WaveAnimationWidget>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 3),
vsync: this,
)..repeat();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return CustomPaint(
painter: AnimatedWavePainter(
animationValue: _controller.value,
waveColor: Colors.blue,
),
size: const Size(300, 200),
);
},
);
}
}CUSTOM PAINT 技巧
- 优化 shouldRepaint 以提升性能
- 使用 Path.combine 处理复杂形状运算
- 把昂贵的计算缓存在 paint 方法之外
- 高级特效可考虑使用 shader
动画性能秘诀
应该这样做
- 为子 widget 使用 const 构造函数
- 以 RepaintBoundary 优化重绘范围
- 使用 AnimatedBuilder 精准重建
- 妥善 dispose 控制器
- 使用 Transform,避免改动布局
避免这样做
- 直接对布局属性做动画
- 在 build 方法内创建控制器
- 在 paint 方法内进行复杂计算
- 同时运行过多动画
- 忘记释放(dispose)资源


