- Phân biệt implicit vs explicit animation.
- Implicit family: AnimatedContainer, AnimatedOpacity, AnimatedAlign, AnimatedSwitcher.
- Explicit:
AnimationController,Tween<T>,Curve. TweenAnimationBuilder— flexible implicit.vsyncvớiSingleTickerProviderStateMixin.Hero— shared element transition.- Lottie / Rive cho animation phức tạp.
- Custom animation với CustomPainter.
- Staggered animation.
- Performance:
RepaintBoundary.
1. Implicit vs Explicit — spectrum
| Implicit | Explicit | |
|---|---|---|
| Cách dùng | Declare target state, Flutter tween từ state hiện tại | Control AnimationController + value mỗi frame |
| Boilerplate | Min | Cao |
| Flexibility | Limited | Full |
| Use case | 80% — UI state transition đơn giản | Complex sequence, custom curve, multi-property |
| Vd | AnimatedContainer color change | Loading spinner custom, hero spring |
2. Implicit — Animated* widget family
| Widget | Anim |
|---|---|
| AnimatedContainer | color, size, padding, decoration, alignment |
| AnimatedOpacity | fade in/out |
| AnimatedAlign | vị trí align |
| AnimatedDefaultTextStyle | text style transition |
| AnimatedCrossFade | fade giữa 2 child |
| AnimatedSwitcher | swap child có transition |
| AnimatedSize | size change |
| AnimatedPadding | padding change |
| AnimatedPositioned | position trong Stack |
class _ToggleBoxState extends State<ToggleBox> {
bool _on = false;
@override
Widget build(BuildContext context) => GestureDetector(
onTap: () => setState(() => _on = !_on),
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
width: _on ? 200 : 100,
height: _on ? 100 : 50,
decoration: BoxDecoration(
color: _on ? Colors.amber : Colors.blue,
borderRadius: BorderRadius.circular(_on ? 24 : 8),
),
),
);
}
// 1 setState đổi flag → AnimatedContainer tween từ size+color cũ → mới.
3. TweenAnimationBuilder — flexible implicit
TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: targetValue),
duration: const Duration(seconds: 1),
curve: Curves.easeOutCubic,
builder: (context, value, child) {
return Transform.rotate(angle: value * 6.28, child: child);
},
child: const Icon(Icons.refresh, size: 64),
)
// Khi targetValue đổi → animate từ value hiện tại → target mới.
4. Explicit — AnimationController + Tween
class _BouncingState extends State<Bouncing> with SingleTickerProviderStateMixin {
late final AnimationController _ctrl;
late final Animation<double> _scale;
@override
void initState() {
super.initState();
_ctrl = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 800),
);
_scale = Tween<double>(begin: 1, end: 1.4).animate(
CurvedAnimation(parent: _ctrl, curve: Curves.elasticOut),
);
_ctrl.repeat(reverse: true); // loop forward + reverse
}
@override
void dispose() {
_ctrl.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) => ScaleTransition(
scale: _scale,
child: const Icon(Icons.favorite, color: Colors.red, size: 100),
);
}
Không dispose = leak + animation tiếp tục chạy sau khi widget unmount. super.dispose() không tự dispose controller.
vsync và TickerProviderMixin
vsync: this— sync animation với screen refresh (60Hz/120Hz). Tránh waste CPU khi screen off.SingleTickerProviderStateMixin— 1 controller. Đa số case.TickerProviderStateMixin— nhiều controller.
5. Curve — easing function
Curves.linear // đều, robot-like
Curves.easeIn // chậm đầu
Curves.easeOut // chậm cuối
Curves.easeInOut // chậm 2 đầu, "natural" nhất
Curves.easeOutCubic // recommend cho UI transition
Curves.bounceIn // nảy
Curves.bounceOut
Curves.elasticIn // đàn hồi
Curves.elasticOut
Curves.fastOutSlowIn // Material default
Cùng duration 300ms, easeOutCubic feel premium, linear feel mechanical.
Default cho hầu hết UI: easeOutCubic hoặc fastOutSlowIn.
6. AnimatedBuilder — perf pattern
// ❌ Sai — heavy widget rebuild mỗi frame
return RotationTransition(
turns: _ctrl,
child: ExpensiveWidget(), // rebuild mỗi tick
);
// ✅ Đúng — child ngoài builder, không rebuild
return AnimatedBuilder(
animation: _ctrl,
builder: (context, child) => Transform.rotate(
angle: _ctrl.value * 6.28,
child: child,
),
child: const ExpensiveWidget(), // build 1 lần, pass vào builder
);
7. Staggered animation
// 1 controller, nhiều animation interval
late final AnimationController _ctrl = AnimationController(
vsync: this, duration: const Duration(seconds: 2),
);
late final Animation<double> _fade = CurvedAnimation(
parent: _ctrl, curve: const Interval(0, 0.4, curve: Curves.easeOut),
);
late final Animation<Offset> _slide = Tween(
begin: const Offset(0, 0.3), end: Offset.zero,
).animate(CurvedAnimation(
parent: _ctrl, curve: const Interval(0.3, 0.7, curve: Curves.easeOut),
));
late final Animation<double> _scale = Tween(begin: 0.8, end: 1.0).animate(
CurvedAnimation(parent: _ctrl, curve: const Interval(0.6, 1.0, curve: Curves.easeOutBack)),
);
// 3 anim chạy theo timeline cùng 1 controller — staggered.
8. Hero — shared element transition
// Trong list page
GestureDetector(
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => DetailPage(item: item))),
child: Hero(
tag: 'item-${item.id}',
child: Image.network(item.thumbUrl, width: 100, height: 100),
),
)
// Trong detail page
Hero(
tag: 'item-${item.id}', // CÙNG tag
child: Image.network(item.fullUrl, width: screenWidth, height: 300),
)
// Flutter tự animate giữa 2 vị trí + size.
9. Lottie / Rive
flutter pub add lottie
flutter pub add rive
// Lottie — JSON từ Adobe After Effects
Lottie.asset('assets/loading.json', width: 200, height: 200)
Lottie.network('https://lottiefiles.com/some.json')
// Rive — vector animation với state machine interactive
RiveAnimation.asset('assets/character.riv')
- Lottie: one-shot animation export từ AE. Đẹp cho loading, splash, illustration.
- Rive: interactive — state machine với input/event. Game-like, character animation.
10. Custom page route transition
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (_, __, ___) => const NextPage(),
transitionDuration: const Duration(milliseconds: 300),
transitionsBuilder: (_, anim, __, child) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 1), // slide từ dưới
end: Offset.zero,
).animate(CurvedAnimation(parent: anim, curve: Curves.easeOutCubic)),
child: child,
);
},
),
)
11. AnimatedList
final _listKey = GlobalKey<AnimatedListState>();
final _items = <Item>[];
AnimatedList(
key: _listKey,
initialItemCount: _items.length,
itemBuilder: (context, index, animation) => SizeTransition(
sizeFactor: animation,
child: ListTile(title: Text(_items[index].title)),
),
)
// Add với animation
void _addItem(Item item) {
_items.insert(0, item);
_listKey.currentState?.insertItem(0, duration: const Duration(milliseconds: 300));
}
// Remove với animation
void _removeItem(int index) {
final removed = _items.removeAt(index);
_listKey.currentState?.removeItem(index,
(context, animation) => SizeTransition(
sizeFactor: animation,
child: ListTile(title: Text(removed.title)),
),
);
}
12. Physics-based animation (intro)
// SpringSimulation — physics
final spring = SpringSimulation(
SpringDescription(mass: 1, stiffness: 100, damping: 10),
0, // start
1, // end
0, // initial velocity
);
_ctrl.animateWith(spring);
// BouncingScrollPhysics — đã quen từ ListView iOS-style
ListView(
physics: const BouncingScrollPhysics(),
children: [/*...*/],
)
13. RepaintBoundary — performance
// Isolate paint layer — parent rebuild không trigger child repaint
RepaintBoundary(
child: ExpensiveAnimation(),
)
// Khi animation trong RepaintBoundary chạy, chỉ paint phần đó.
// Parent không liên quan animation → không bị repaint.
14. Performance Overlay
MaterialApp(
showPerformanceOverlay: true, // 2 bar đo UI + Raster thread
)
// 2 thanh:
// - UI thread (Dart): build/layout time
// - Raster thread: GPU paint time
// Đỏ = vượt 16ms (60fps) → jank.
15. Bài tập
Theme toggle animated
Toggle theme button với AnimatedContainer đổi background + icon rotate 180°. Mỗi click swap.
💡 Gợi ý đáp án
AnimatedContainer width/color, RotationTransition cho icon. Hoặc AnimatedRotation.
Like animation
Tap heart icon → scale up + color change. Dùng explicit (AnimationController + Tween). Bonus: thêm particle burst nhỏ.
💡 Gợi ý đáp án
AnimationController duration 400ms. Tween scale 1 → 1.4 → 1 (use TweenSequence). Color animation từ grey → red. Particle: stack với nhiều dot animate radial out.
Hero detail page
Grid product list → tap → detail page. Hero transition thumbnail → full image.
💡 Gợi ý đáp án
Tag unique per item: 'product-${item.id}'. Cùng tag ở 2 page, Flutter tự animate.
Lottie loading screen
Loading screen với Lottie animation tải free từ LottieFiles.
💡 Gợi ý đáp án
Download từ lottiefiles.com → put vào assets. Lottie.asset('assets/loading.json').
Bouncing notification badge
Notification badge: số đếm tăng → badge scale 0 → 1.2 → 1 với spring. Dùng SpringSimulation hoặc Curves.elasticOut.
💡 Gợi ý đáp án
TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: 1),
duration: const Duration(milliseconds: 500),
curve: Curves.elasticOut,
builder: (_, value, __) => Transform.scale(scale: value, child: badge),
)
16. Quiz
AnimatedContainer cần controller không?
Xem đáp án
Đáp án: Không. Implicit tự nội — chỉ cần đổi prop + duration + curve, Flutter tween.
vsync parameter — vai trò?
Xem đáp án
Đáp án: Sync animation với ticker (frame refresh). Pause khi screen off để save CPU/battery.
AnimationController không dispose — chuyện gì?
Xem đáp án
Đáp án: Memory leak + animation tiếp chạy nền sau dispose widget. super.dispose() không tự dispose controller.
Hero tag duplicate giữa 2 page — chuyện gì?
Xem đáp án
Đáp án: Throw error "There are multiple heroes that share the same tag within a subtree". Tag phải unique.
Curves.easeInOut vs Curves.linear — khác feel?
Xem đáp án
Đáp án: easeInOut "natural", linear "robot-like". Easing usually preferred trừ progress indicator (cần linear).
RepaintBoundary giúp gì?
Xem đáp án
Đáp án: Isolate paint layer. Repaint chỉ phần thay đổi, không repaint cả tree. Performance hint.
Lottie vs Rive — chọn?
Xem đáp án
Đáp án: Lottie cho one-shot animation export từ AE. Rive cho interactive với state machine.
AnimatedBuilder builder gọi mỗi frame — performance OK?
Xem đáp án
Đáp án: OK nếu widget trong builder rẻ. Đặt heavy widget ngoài (pass qua child param), chỉ widget animated trong builder.
17. Tổng kết
- ✅ Implicit (Animated*) cho 80% case — declare target, framework tween.
- ✅ Explicit (AnimationController + Tween) cho complex sequence.
- ✅
vsync+ SingleTickerProviderStateMixin. - ✅ Curve quan trọng hơn duration — easeOutCubic default.
- ✅ AnimatedBuilder pattern: heavy child ngoài, transform trong builder.
- ✅ Staggered animation — 1 controller, nhiều Interval.
- ✅ Hero — shared element transition giữa route.
- ✅ Lottie (one-shot) vs Rive (interactive).
- ✅ AnimatedList cho insert/remove animation.
- ✅ Physics — SpringSimulation.
- ✅ RepaintBoundary isolate paint zone.
- ✅ Performance Overlay đo UI + Raster thread time.
18. Kết nối
- Chương 13 — Custom Paint: animation pair với CustomPainter.
- Chương 15: Performance overlay đo jank animation.