✨ Animation cơ bản
1. Implicit Animations
Phần tiêu đề “1. Implicit Animations”Flutter cung cấp các Animated widgets dễ sử dụng:
AnimatedContainer( duration: Duration(milliseconds: 300), width: _isExpanded ? 200 : 100, height: _isExpanded ? 200 : 100, color: _isExpanded ? Colors.blue : Colors.red, child: Text('Animated'),)Chỉ cần thay đổi giá trị và widget tự animate!
2. Các Animated Widgets phổ biến
Phần tiêu đề “2. Các Animated Widgets phổ biến”| Widget | Animate |
|---|---|
AnimatedContainer |
Size, color, padding, margin, decoration |
AnimatedOpacity |
Opacity |
AnimatedPadding |
Padding |
AnimatedPositioned |
Position trong Stack |
AnimatedAlign |
Alignment |
AnimatedDefaultTextStyle |
Text style |
AnimatedSwitcher |
Widget switching |
AnimatedCrossFade |
Fade giữa 2 widgets |
3. AnimatedOpacity
Phần tiêu đề “3. AnimatedOpacity”AnimatedOpacity( duration: Duration(milliseconds: 500), opacity: _isVisible ? 1.0 : 0.0, child: Container( width: 100, height: 100, color: Colors.blue, ),)4. AnimatedPositioned
Phần tiêu đề “4. AnimatedPositioned”Trong Stack:
Stack( children: [ AnimatedPositioned( duration: Duration(milliseconds: 300), left: _isLeft ? 0 : 200, top: _isTop ? 0 : 200, child: Container(width: 50, height: 50, color: Colors.red), ), ],)5. AnimatedSwitcher
Phần tiêu đề “5. AnimatedSwitcher”Animate khi thay đổi widget:
AnimatedSwitcher( duration: Duration(milliseconds: 300), child: Text( '$_count', key: ValueKey<int>(_count), // Key quan trọng! ),)Custom transition
Phần tiêu đề “Custom transition”AnimatedSwitcher( duration: Duration(milliseconds: 300), transitionBuilder: (child, animation) { return ScaleTransition(scale: animation, child: child); }, child: _currentWidget,)6. AnimatedCrossFade
Phần tiêu đề “6. AnimatedCrossFade”Fade giữa 2 widgets:
AnimatedCrossFade( duration: Duration(milliseconds: 300), crossFadeState: _showFirst ? CrossFadeState.showFirst : CrossFadeState.showSecond, firstChild: Icon(Icons.play_arrow, size: 50), secondChild: Icon(Icons.pause, size: 50),)7. Curves
Phần tiêu đề “7. Curves”Điều chỉnh acceleration của animation:
AnimatedContainer( duration: Duration(milliseconds: 500), curve: Curves.easeInOut, // Smooth start and end // curve: Curves.bounceOut, // Bounce effect // curve: Curves.elasticOut, // Elastic effect width: _isExpanded ? 200 : 100,)| Curve | Description |
|---|---|
linear |
Đều |
easeIn |
Chậm → nhanh |
easeOut |
Nhanh → chậm |
easeInOut |
Chậm → nhanh → chậm |
bounceOut |
Nảy ở cuối |
elasticOut |
Đàn hồi |
8. TweenAnimationBuilder
Phần tiêu đề “8. TweenAnimationBuilder”Animation tùy chỉnh với giá trị bất kỳ:
TweenAnimationBuilder<double>( tween: Tween<double>(begin: 0, end: _targetValue), duration: Duration(milliseconds: 500), builder: (context, value, child) { return Transform.rotate( angle: value * 2 * 3.14159, // Rotate based on value child: child, ); }, child: Icon(Icons.refresh),)9. Hero Animation
Phần tiêu đề “9. Hero Animation”Animate widget khi chuyển screen:
Screen 1
Phần tiêu đề “Screen 1”GestureDetector( onTap: () => Navigator.push(context, MaterialPageRoute( builder: (_) => DetailScreen(), )), child: Hero( tag: 'avatar', // Same tag on both screens child: CircleAvatar(radius: 30), ),)Screen 2
Phần tiêu đề “Screen 2”Hero( tag: 'avatar', // Same tag child: CircleAvatar(radius: 100),)10. Ví dụ thực tế
Phần tiêu đề “10. Ví dụ thực tế”Loading Button
Phần tiêu đề “Loading Button”class LoadingButton extends StatefulWidget { @override State<LoadingButton> createState() => _LoadingButtonState();}
class _LoadingButtonState extends State<LoadingButton> { bool _isLoading = false;
Future<void> _handlePress() async { setState(() => _isLoading = true); await Future.delayed(Duration(seconds: 2)); setState(() => _isLoading = false); }
@override Widget build(BuildContext context) { return AnimatedContainer( duration: Duration(milliseconds: 300), width: _isLoading ? 50 : 150, height: 50, child: ElevatedButton( onPressed: _isLoading ? null : _handlePress, child: AnimatedSwitcher( duration: Duration(milliseconds: 200), child: _isLoading ? SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2), ) : Text('Submit'), ), ), ); }}Expandable Card
Phần tiêu đề “Expandable Card”class ExpandableCard extends StatefulWidget { @override State<ExpandableCard> createState() => _ExpandableCardState();}
class _ExpandableCardState extends State<ExpandableCard> { bool _isExpanded = false;
@override Widget build(BuildContext context) { return GestureDetector( onTap: () => setState(() => _isExpanded = !_isExpanded), child: AnimatedContainer( duration: Duration(milliseconds: 300), curve: Curves.easeInOut, width: double.infinity, padding: EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(_isExpanded ? 0.2 : 0.1), blurRadius: _isExpanded ? 20 : 10, ), ], ), child: Column( children: [ Text('Card Title'), AnimatedCrossFade( duration: Duration(milliseconds: 300), crossFadeState: _isExpanded ? CrossFadeState.showSecond : CrossFadeState.showFirst, firstChild: SizedBox.shrink(), secondChild: Padding( padding: EdgeInsets.only(top: 16), child: Text('Expanded content here...'), ), ), ], ), ), ); }}📝 Tóm tắt
Phần tiêu đề “📝 Tóm tắt”| Type | Khi nào dùng |
|---|---|
Implicit (AnimatedX) |
Đơn giản, chỉ cần thay đổi value |
| TweenAnimationBuilder | Custom values |
| Hero | Transition giữa screens |
| Widget | Animate |
|---|---|
AnimatedContainer |
Nhiều properties |
AnimatedOpacity |
Fade in/out |
AnimatedSwitcher |
Thay đổi widget |
AnimatedCrossFade |
Fade giữa 2 widgets |