
// 簡報
動畫是好應用程式與卓越應用程式之間的分野。它提供視覺回饋、引導用戶注意力,並創造讓用戶難忘的愉悅體驗。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)資源


