Add comprehensive Flutter cross-platform development guide covering: - Widget patterns and const optimization - Riverpod/Bloc state management - GoRouter navigation - Performance optimization - Testing strategies - Platform-specific implementations Made-with: Cursor
13 KiB
13 KiB
Animations
Flutter animation patterns covering implicit animations, explicit animations, Hero transitions, and page transitions.
Implicit Animations
Use implicit animations for simple property changes:
class ImplicitAnimationExample extends StatefulWidget {
const ImplicitAnimationExample({super.key});
@override
State<ImplicitAnimationExample> createState() => _ImplicitAnimationExampleState();
}
class _ImplicitAnimationExampleState extends State<ImplicitAnimationExample> {
bool _expanded = false;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => setState(() => _expanded = !_expanded),
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
width: _expanded ? 200 : 100,
height: _expanded ? 200 : 100,
decoration: BoxDecoration(
color: _expanded ? Colors.blue : Colors.red,
borderRadius: BorderRadius.circular(_expanded ? 16 : 8),
),
child: const Center(child: Text('Tap me')),
),
);
}
}
Common Implicit Widgets
| Widget | Animates |
|---|---|
AnimatedContainer |
Size, color, padding, decoration |
AnimatedOpacity |
Opacity |
AnimatedPadding |
Padding |
AnimatedPositioned |
Position in Stack |
AnimatedAlign |
Alignment |
AnimatedCrossFade |
Cross-fade between two widgets |
AnimatedSwitcher |
Transition between child widgets |
AnimatedDefaultTextStyle |
Text style |
AnimatedScale |
Scale transform |
AnimatedRotation |
Rotation transform |
AnimatedSlide |
Slide offset |
AnimatedSwitcher
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
transitionBuilder: (child, animation) {
return FadeTransition(
opacity: animation,
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 0.1),
end: Offset.zero,
).animate(animation),
child: child,
),
);
},
child: _showFirst
? const Icon(Icons.check, key: ValueKey('check'))
: const Icon(Icons.close, key: ValueKey('close')),
)
Explicit Animations
Use explicit animations for complex, custom, or controlled animations:
class ExplicitAnimationExample extends StatefulWidget {
const ExplicitAnimationExample({super.key});
@override
State<ExplicitAnimationExample> createState() => _ExplicitAnimationExampleState();
}
class _ExplicitAnimationExampleState extends State<ExplicitAnimationExample>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
late final Animation<double> _scaleAnimation;
late final Animation<double> _rotationAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 500),
vsync: this,
);
_scaleAnimation = Tween<double>(begin: 1.0, end: 1.2).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeOut),
);
_rotationAnimation = Tween<double>(begin: 0, end: 0.1).animate(
CurvedAnimation(parent: _controller, curve: Curves.elasticOut),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: (_) => _controller.forward(),
onTapUp: (_) => _controller.reverse(),
onTapCancel: () => _controller.reverse(),
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value,
child: Transform.rotate(
angle: _rotationAnimation.value,
child: child,
),
);
},
child: const Card(child: Padding(padding: EdgeInsets.all(24), child: Text('Press me'))),
),
);
}
}
Animation with Hooks
import 'package:flutter_hooks/flutter_hooks.dart';
class AnimatedButtonHook extends HookWidget {
const AnimatedButtonHook({super.key});
@override
Widget build(BuildContext context) {
final controller = useAnimationController(
duration: const Duration(milliseconds: 300),
);
final scale = useAnimation(
Tween<double>(begin: 1.0, end: 0.95).animate(
CurvedAnimation(parent: controller, curve: Curves.easeInOut),
),
);
return GestureDetector(
onTapDown: (_) => controller.forward(),
onTapUp: (_) => controller.reverse(),
onTapCancel: () => controller.reverse(),
child: Transform.scale(
scale: scale,
child: const Card(child: Text('Animated Button')),
),
);
}
}
Staggered Animations
class StaggeredAnimation extends StatefulWidget {
const StaggeredAnimation({super.key});
@override
State<StaggeredAnimation> createState() => _StaggeredAnimationState();
}
class _StaggeredAnimationState extends State<StaggeredAnimation>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
late final List<Animation<double>> _itemAnimations;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 1500),
vsync: this,
);
_itemAnimations = List.generate(5, (index) {
final start = index * 0.1;
final end = start + 0.4;
return Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(start, end.clamp(0, 1), curve: Curves.easeOut),
),
);
});
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: List.generate(5, (index) {
return AnimatedBuilder(
animation: _itemAnimations[index],
builder: (context, child) {
return Opacity(
opacity: _itemAnimations[index].value,
child: Transform.translate(
offset: Offset(0, 20 * (1 - _itemAnimations[index].value)),
child: child,
),
);
},
child: ListTile(title: Text('Item $index')),
);
}),
);
}
}
Hero Animations
class HeroSourcePage extends StatelessWidget {
const HeroSourcePage({super.key});
@override
Widget build(BuildContext context) {
return GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
itemCount: items.length,
itemBuilder: (context, index) {
final item = items[index];
return GestureDetector(
onTap: () => context.push('/detail/${item.id}'),
child: Hero(
tag: 'hero-${item.id}',
child: Image.network(item.imageUrl, fit: BoxFit.cover),
),
);
},
);
}
}
class HeroDetailPage extends StatelessWidget {
final String itemId;
const HeroDetailPage({super.key, required this.itemId});
@override
Widget build(BuildContext context) {
final item = getItem(itemId);
return Scaffold(
body: Column(
children: [
Hero(
tag: 'hero-${item.id}',
child: Image.network(item.imageUrl, fit: BoxFit.cover),
),
Padding(
padding: const EdgeInsets.all(16),
child: Text(item.title, style: Theme.of(context).textTheme.headlineMedium),
),
],
),
);
}
}
Hero with Custom Flight
Hero(
tag: 'avatar-$userId',
flightShuttleBuilder: (
flightContext,
animation,
flightDirection,
fromHeroContext,
toHeroContext,
) {
return AnimatedBuilder(
animation: animation,
builder: (context, child) {
return Material(
color: Colors.transparent,
child: CircleAvatar(
radius: lerpDouble(24, 48, animation.value),
backgroundImage: NetworkImage(avatarUrl),
),
);
},
);
},
child: CircleAvatar(radius: 24, backgroundImage: NetworkImage(avatarUrl)),
)
Page Transitions
GoRouter Custom Transitions
GoRoute(
path: '/detail/:id',
pageBuilder: (context, state) {
return CustomTransitionPage(
key: state.pageKey,
child: DetailPage(id: state.pathParameters['id']!),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(
opacity: animation,
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 0.05),
end: Offset.zero,
).animate(CurvedAnimation(
parent: animation,
curve: Curves.easeOut,
)),
child: child,
),
);
},
);
},
)
Common Transition Patterns
extension PageTransitions on CustomTransitionPage {
static CustomTransitionPage<T> fade<T>({
required LocalKey key,
required Widget child,
}) {
return CustomTransitionPage<T>(
key: key,
child: child,
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(opacity: animation, child: child);
},
);
}
static CustomTransitionPage<T> slideUp<T>({
required LocalKey key,
required Widget child,
}) {
return CustomTransitionPage<T>(
key: key,
child: child,
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 1),
end: Offset.zero,
).animate(CurvedAnimation(
parent: animation,
curve: Curves.easeOutCubic,
)),
child: child,
);
},
);
}
static CustomTransitionPage<T> scale<T>({
required LocalKey key,
required Widget child,
}) {
return CustomTransitionPage<T>(
key: key,
child: child,
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return ScaleTransition(
scale: Tween<double>(begin: 0.9, end: 1).animate(
CurvedAnimation(parent: animation, curve: Curves.easeOut),
),
child: FadeTransition(opacity: animation, child: child),
);
},
);
}
}
Shared Axis Transition
import 'package:animations/animations.dart';
GoRoute(
path: '/settings',
pageBuilder: (context, state) {
return CustomTransitionPage(
key: state.pageKey,
child: const SettingsPage(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return SharedAxisTransition(
animation: animation,
secondaryAnimation: secondaryAnimation,
transitionType: SharedAxisTransitionType.horizontal,
child: child,
);
},
);
},
)
Common Curves
| Curve | Usage |
|---|---|
Curves.easeInOut |
General purpose (default) |
Curves.easeOut |
Deceleration (entering) |
Curves.easeIn |
Acceleration (exiting) |
Curves.elasticOut |
Bouncy effect |
Curves.bounceOut |
Bounce at end |
Curves.fastOutSlowIn |
Material standard |
Curves.easeOutCubic |
Smooth deceleration |
Animation Performance
class PerformantAnimation extends StatelessWidget {
const PerformantAnimation({super.key});
@override
Widget build(BuildContext context) {
return RepaintBoundary(
child: AnimatedBuilder(
animation: animation,
builder: (context, child) {
return Transform.translate(
offset: Offset(animation.value * 100, 0),
child: child,
);
},
child: const ExpensiveWidget(),
),
);
}
}
Performance Tips
| Tip | Implementation |
|---|---|
Use child parameter |
Pass static content to child in AnimatedBuilder |
RepaintBoundary |
Isolate animated widgets |
Avoid Opacity widget |
Use FadeTransition instead |
| Prefer transforms | Transform is cheaper than layout changes |
| Pre-compute values | Calculate in initState, not build |
Animation Checklist
| Item | Implementation |
|---|---|
| Simple animations | Use implicit widgets |
| Complex sequences | Use AnimationController |
| Widget transitions | AnimatedSwitcher with key |
| Cross-page elements | Hero with unique tags |
| Page transitions | CustomTransitionPage |
| Performance | RepaintBoundary + child parameter |
Flutter and Material Design are trademarks of Google LLC.