assets/templates/explicit_animation.dart
import 'package:flutter/material.dart';
/// Explicit animation example using AnimationController
///
/// This example demonstrates:
/// - AnimationController setup and lifecycle
/// - Tween interpolation
/// - AnimatedWidget pattern for reusable animations
/// - AnimatedBuilder pattern for complex widgets
/// - Animation status monitoring
void main() => runApp(const ExplicitAnimationApp());
class ExplicitAnimationApp extends StatelessWidget {
const ExplicitAnimationApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Explicit Animation',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: const LogoAnimationDemo(),
);
}
}
/// Basic explicit animation with addListener and setState
class BasicExplicitAnimation extends StatefulWidget {
const BasicExplicitAnimation({super.key});
@override
State<BasicExplicitAnimation> createState() => _BasicExplicitAnimationState();
}
class _BasicExplicitAnimationState extends State<BasicExplicitAnimation>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
);
_animation = Tween<double>(begin: 0, end: 300).animate(_controller)
..addListener(() {
setState(() {});
});
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Basic Explicit')),
body: Center(
child: Container(
margin: const EdgeInsets.symmetric(vertical: 10),
height: _animation.value,
width: _animation.value,
child: const FlutterLogo(),
),
),
);
}
}
/// AnimatedWidget pattern - best for reusable animated widgets
class AnimatedLogo extends AnimatedWidget {
const AnimatedLogo({super.key, required Animation<double> animation})
: super(listenable: animation);
@override
Widget build(BuildContext context) {
final animation = listenable as Animation<double>;
return Center(
child: Container(
margin: const EdgeInsets.symmetric(vertical: 10),
height: animation.value,
width: animation.value,
child: const FlutterLogo(),
),
);
}
}
class AnimatedWidgetDemo extends StatefulWidget {
const AnimatedWidgetDemo({super.key});
@override
State<AnimatedWidgetDemo> createState() => _AnimatedWidgetDemoState();
}
class _AnimatedWidgetDemoState extends State<AnimatedWidgetDemo>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
);
_animation = Tween<double>(begin: 0, end: 300).animate(_controller);
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Animated Widget')),
body: AnimatedLogo(animation: _animation),
);
}
}
/// AnimatedBuilder pattern - best for complex widgets
class LogoWidget extends StatelessWidget {
const LogoWidget({super.key});
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(vertical: 10),
child: const FlutterLogo(),
);
}
}
class GrowTransition extends StatelessWidget {
const GrowTransition({
required this.child,
required this.animation,
super.key,
});
final Widget child;
final Animation<double> animation;
@override
Widget build(BuildContext context) {
return Center(
child: AnimatedBuilder(
animation: animation,
builder: (context, child) {
return SizedBox(
height: animation.value,
width: animation.value,
child: child,
);
},
child: child,
),
);
}
}
class AnimatedBuilderDemo extends StatefulWidget {
const AnimatedBuilderDemo({super.key});
@override
State<AnimatedBuilderDemo> createState() => _AnimatedBuilderDemoState();
}
class _AnimatedBuilderDemoState extends State<AnimatedBuilderDemo>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
);
_animation = Tween<double>(begin: 0, end: 300).animate(_controller);
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Animated Builder')),
body: GrowTransition(animation: _animation, child: const LogoWidget()),
);
}
}
/// Multiple simultaneous animations with status monitoring
class MultiPropertyAnimation extends AnimatedWidget {
const MultiPropertyAnimation({
super.key,
required Animation<double> animation,
}) : super(listenable: animation);
static final _opacityTween = Tween<double>(begin: 0.1, end: 1);
static final _sizeTween = Tween<double>(begin: 0, end: 300);
static final _colorTween = ColorTween(begin: Colors.red, end: Colors.blue);
@override
Widget build(BuildContext context) {
final animation = listenable as Animation<double>;
return Center(
child: Opacity(
opacity: _opacityTween.evaluate(animation),
child: Container(
height: _sizeTween.evaluate(animation),
width: _sizeTween.evaluate(animation),
decoration: BoxDecoration(
color: _colorTween.evaluate(animation),
borderRadius: BorderRadius.circular(12),
),
child: const FlutterLogo(),
),
),
);
}
}
class MultiPropertyDemo extends StatefulWidget {
const MultiPropertyDemo({super.key});
@override
State<MultiPropertyDemo> createState() => _MultiPropertyDemoState();
}
class _MultiPropertyDemoState extends State<MultiPropertyDemo>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
);
_animation = CurvedAnimation(parent: _controller, curve: Curves.easeIn)
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
_controller.reverse();
} else if (status == AnimationStatus.dismissed) {
_controller.forward();
}
});
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Multi Property')),
body: MultiPropertyAnimation(animation: _animation),
);
}
}
/// Main demo widget showing all examples
class LogoAnimationDemo extends StatelessWidget {
const LogoAnimationDemo({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Explicit Animations')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () => Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (context) => const BasicExplicitAnimation(),
),
),
child: const Text('Basic Explicit'),
),
ElevatedButton(
onPressed: () => Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (context) => const AnimatedWidgetDemo(),
),
),
child: const Text('Animated Widget'),
),
ElevatedButton(
onPressed: () => Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (context) => const AnimatedBuilderDemo(),
),
),
child: const Text('Animated Builder'),
),
ElevatedButton(
onPressed: () => Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (context) => const MultiPropertyDemo(),
),
),
child: const Text('Multi Property'),
),
],
),
),
);
}
}
assets/templates/hero_transition.dart
import 'package:flutter/material.dart';
/// Hero animation example for shared element transitions
///
/// This example demonstrates:
/// - Basic hero animation with matching tags
/// - Custom PhotoHero widget
/// - Navigation between screens with hero transition
/// - MaterialRectCenterArcTween for aspect ratio preservation
void main() => runApp(const HeroAnimationApp());
class HeroAnimationApp extends StatelessWidget {
const HeroAnimationApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Hero Animation',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: const GalleryScreen(),
);
}
}
/// Custom hero widget for images
class PhotoHero extends StatelessWidget {
const PhotoHero({
super.key,
required this.photo,
this.onTap,
required this.width,
});
final String photo;
final VoidCallback? onTap;
final double width;
@override
Widget build(BuildContext context) {
return SizedBox(
width: width,
child: Hero(
tag: photo,
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
child: Image.asset(
photo,
fit: BoxFit.contain,
errorBuilder: (context, error, stackTrace) {
return Container(
width: width,
height: width,
color: Colors.grey,
child: const Icon(Icons.broken_image),
);
},
),
),
),
),
);
}
}
/// Gallery screen showing multiple hero images
class GalleryScreen extends StatelessWidget {
const GalleryScreen({super.key});
static const List<String> _photos = [
'images/photo1.png',
'images/photo2.png',
'images/photo3.png',
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Hero Gallery')),
body: GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
itemCount: _photos.length,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: PhotoHero(
photo: _photos[index],
width: 100,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (context) => DetailScreen(photo: _photos[index]),
),
);
},
),
);
},
),
);
}
}
/// Detail screen showing hero image in fullscreen
class DetailScreen extends StatelessWidget {
const DetailScreen({super.key, required this.photo});
final String photo;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Detail')),
body: Center(
child: PhotoHero(
photo: photo,
width: MediaQuery.of(context).size.width - 32,
onTap: () => Navigator.of(context).pop(),
),
),
);
}
}
/// Example with circular hero (using placeholder instead of asset)
class CircularHeroExample extends StatefulWidget {
const CircularHeroExample({super.key});
@override
State<CircularHeroExample> createState() => _CircularHeroExampleState();
}
class _CircularHeroExampleState extends State<CircularHeroExample> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Circular Hero')),
body: ListView(
children: List.generate(3, (index) {
final tag = 'circle-$index';
return Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Hero(
tag: tag,
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (context) =>
CircularDetailScreen(tag: tag, index: index),
),
);
},
child: Container(
width: 80,
height: 80,
decoration: const BoxDecoration(
color: Colors.blue,
shape: BoxShape.circle,
),
),
),
),
),
),
);
}),
),
);
}
}
class CircularDetailScreen extends StatelessWidget {
const CircularDetailScreen({
super.key,
required this.tag,
required this.index,
});
final String tag;
final int index;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Circular Detail')),
body: Center(
child: Hero(
tag: tag,
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () => Navigator.of(context).pop(),
child: Container(
width: 300,
height: 300,
decoration: BoxDecoration(
color: Colors.blue.withValues(alpha: 0.8),
borderRadius: BorderRadius.circular(150),
),
child: Center(
child: Text(
'Image $index',
style: const TextStyle(color: Colors.white, fontSize: 24),
),
),
),
),
),
),
),
);
}
}
assets/templates/implicit_animation.dart
import 'package:flutter/material.dart';
/// Implicit animation example showing basic fade animation
///
/// This example demonstrates:
/// - AnimatedOpacity for fade in/out
/// - State-driven animation triggers
/// - Simple, declarative animation code
void main() => runApp(const ImplicitAnimationApp());
class ImplicitAnimationApp extends StatelessWidget {
const ImplicitAnimationApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Implicit Animation',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: const FadeExample(),
);
}
}
class FadeExample extends StatefulWidget {
const FadeExample({super.key});
@override
State<FadeExample> createState() => _FadeExampleState();
}
class _FadeExampleState extends State<FadeExample> {
bool _visible = true;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Implicit Fade')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedOpacity(
opacity: _visible ? 1.0 : 0.0,
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut,
child: const FlutterLogo(size: 100),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () => setState(() => _visible = !_visible),
child: Text(_visible ? 'Fade Out' : 'Fade In'),
),
],
),
),
);
}
}
/// AnimatedContainer example - animating multiple properties
class AnimatedContainerExample extends StatefulWidget {
const AnimatedContainerExample({super.key});
@override
State<AnimatedContainerExample> createState() =>
_AnimatedContainerExampleState();
}
class _AnimatedContainerExampleState extends State<AnimatedContainerExample> {
bool _expanded = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Animated Container')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
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 ? 20 : 8),
),
child: const FlutterLogo(),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () => setState(() => _expanded = !_expanded),
child: Text(_expanded ? 'Shrink' : 'Expand'),
),
],
),
),
);
}
}
/// TweenAnimationBuilder example - custom tween without boilerplate
class TweenAnimationBuilderExample extends StatefulWidget {
const TweenAnimationBuilderExample({super.key});
@override
State<TweenAnimationBuilderExample> createState() =>
_TweenAnimationBuilderExampleState();
}
class _TweenAnimationBuilderExampleState
extends State<TweenAnimationBuilderExample> {
bool _animated = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Tween Animation Builder')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TweenAnimationBuilder<double>(
tween: Tween<double>(begin: 0, end: 1),
duration: const Duration(seconds: 1),
curve: Curves.easeInOut,
builder: (context, value, child) {
return Opacity(
opacity: value,
child: Transform.scale(scale: value, child: child),
);
},
child: const FlutterLogo(size: 100),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () => setState(() => _animated = !_animated),
child: const Text('Animate'),
),
],
),
),
);
}
}
assets/templates/staggered_animation.dart
import 'package:flutter/material.dart';
/// Staggered animation example with multiple property animations
///
/// This example demonstrates:
/// - Single AnimationController driving multiple animations
/// - Interval-based timing for sequential/overlapping animations
/// - Multiple tweens for different properties
/// - Status monitoring for animation loops
void main() => runApp(const StaggeredAnimationApp());
class StaggeredAnimationApp extends StatelessWidget {
const StaggeredAnimationApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Staggered Animation',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: const StaggeredDemo(),
);
}
}
/// Staggered animation with fade, scale, and color changes
class StaggeredAnimation extends StatelessWidget {
StaggeredAnimation({super.key, required this.animation});
late final Animation<double> opacity = Tween<double>(begin: 0.0, end: 1.0)
.animate(
CurvedAnimation(
parent: animation,
curve: const Interval(0.0, 0.1, curve: Curves.ease),
),
);
late final Animation<double> width = Tween<double>(begin: 50.0, end: 150.0)
.animate(
CurvedAnimation(
parent: animation,
curve: const Interval(0.125, 0.25, curve: Curves.ease),
),
);
late final Animation<double> height = Tween<double>(begin: 50.0, end: 150.0)
.animate(
CurvedAnimation(
parent: animation,
curve: const Interval(0.25, 0.375, curve: Curves.ease),
),
);
late final Animation<BorderRadius?> borderRadius =
BorderRadiusTween(
begin: BorderRadius.circular(4),
end: BorderRadius.circular(75),
).animate(
CurvedAnimation(
parent: animation,
curve: const Interval(0.375, 0.5, curve: Curves.ease),
),
);
late final Animation<Color?> color =
ColorTween(begin: Colors.red, end: Colors.orange).animate(
CurvedAnimation(
parent: animation,
curve: const Interval(0.5, 0.625, curve: Curves.ease),
),
);
final Animation<double> animation;
Widget _buildAnimation(BuildContext context, Widget? child) {
return Center(
child: Opacity(
opacity: opacity.value,
child: Container(
width: width.value,
height: height.value,
decoration: BoxDecoration(
color: color.value,
border: Border.all(color: Colors.indigo[300]!, width: 3),
borderRadius: borderRadius.value,
),
),
),
);
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(animation: animation, builder: _buildAnimation);
}
}
class StaggeredDemo extends StatefulWidget {
const StaggeredDemo({super.key});
@override
State<StaggeredDemo> createState() => _StaggeredDemoState();
}
class _StaggeredDemoState extends State<StaggeredDemo>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 2000),
vsync: this,
);
}
Future<void> _playAnimation() async {
try {
await _controller.forward().orCancel;
await _controller.reverse().orCancel;
} on TickerCanceled {
// Animation was canceled
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Staggered Animation')),
body: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: _playAnimation,
child: Center(
child: Container(
width: 300,
height: 300,
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.1),
border: Border.all(color: Colors.black.withValues(alpha: 0.5)),
),
child: StaggeredAnimation(animation: _controller.view),
),
),
),
);
}
}
/// Staggered menu animation example
class StaggeredMenuExample extends StatefulWidget {
const StaggeredMenuExample({super.key});
@override
State<StaggeredMenuExample> createState() => _StaggeredMenuExampleState();
}
class _StaggeredMenuExampleState extends State<StaggeredMenuExample>
with SingleTickerProviderStateMixin {
static const _menuTitles = [
'Declarative Style',
'Premade Widgets',
'Stateful Hot Reload',
'Native Performance',
'Great Community',
];
static const _initialDelayTime = Duration(milliseconds: 50);
static const _itemSlideTime = Duration(milliseconds: 250);
static const _staggerTime = Duration(milliseconds: 50);
late AnimationController _controller;
final _animationDuration =
_initialDelayTime + (_staggerTime * _menuTitles.length) + _itemSlideTime;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: _animationDuration,
vsync: this,
);
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Staggered Menu')),
body: ListView.builder(
itemCount: _menuTitles.length,
itemBuilder: (context, index) {
final start =
_initialDelayTime.inMilliseconds.toDouble() +
(_staggerTime.inMilliseconds.toDouble() * index);
final end = start + _itemSlideTime.inMilliseconds.toDouble();
final intervalStart =
start / _animationDuration.inMilliseconds.toDouble();
final intervalEnd =
end / _animationDuration.inMilliseconds.toDouble();
final animation = CurvedAnimation(
parent: _controller,
curve: Interval(intervalStart, intervalEnd, curve: Curves.easeOut),
);
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(1, 0),
end: Offset.zero,
).animate(animation),
child: FadeTransition(opacity: animation, child: child),
);
},
child: ListTile(title: Text(_menuTitles[index])),
);
},
),
);
}
}
references/curves.md
# Curves Reference
Curves define the easing function for animations, controlling how values interpolate from start to end.
## Core Concept
A curve takes an input `t` (0.0 to 1.0, representing time) and outputs a value (typically 0.0 to 1.0, representing progress).
## Built-in Curves
### Linear
```dart
Curves.linear
```
No easing - straight line from 0 to 1. Use when you want constant speed.
### Ease (Sigmoid)
```dart
Curves.ease
Curves.easeIn
Curves.easeOut
Curves.easeInOut
```
**Curves.ease** - Slow start and end, fast middle (most common)
**Curves.easeIn** - Slow start, fast end (accelerating)
**Curves.easeOut** - Fast start, slow end (decelerating)
**Curves.easeInOut** - Slow start and end, fast middle (combination)
### Cubic (Stronger Ease)
```dart
Curves.easeInCubic
Curves.easeOutCubic
Curves.easeInOutCubic
```
Stronger easing than basic ease. More dramatic slow/fast zones.
### Elastic (Bouncy)
```dart
Curves.elasticIn
Curves.elasticOut
Curves.elasticInOut
```
**Curves.elasticIn** - Bounces in (starts behind, shoots forward)
**Curves.elasticOut** - Bounces out (goes past, comes back)
**Curves.elasticInOut** - Bounces in and out
Example: Good for playful UI, notifications
### Bounce
```dart
Curves.bounceIn
Curves.bounceOut
Curves.bounceInOut
```
**Curves.bounceIn** - Bounces in (less bouncy than elastic)
**Curves.bounceOut** - Bounces out (less bouncy than elastic)
**Curves.bounceInOut** - Bounces in and out
Example: Good for feedback animations
### Back (Overshoot)
```dart
Curves.backIn
Curves.backOut
Curves.backInOut
```
**Curves.backIn** - Backs up before going in
**Curves.backOut** - Backs up before coming out
**Curves.backInOut** - Backs up on both ends
Example: Good for revealing content
### Decelerate (Fast then Slow)
```dart
Curves.decelerate
```
Fast start, very slow end. Similar to easeOut but more dramatic.
### Fast Out Slow In
```dart
Curves.fastOutSlowIn
```
Very fast start, very slow middle, very fast end. Dramatic.
### Custom Curves in Flutter API
Flutter includes specialized curves:
```dart
// Material design curves
Curves.fastLinearToSlowEaseIn
Curves.slowMiddle
// Specific curves for certain transitions
Curves.ease
```
## Using Curves
### With Implicit Animation
```dart
AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
width: _expanded ? 200 : 100,
child: const FlutterLogo(),
)
```
### With Explicit Animation (CurvedAnimation)
```dart
animation = CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
);
```
### With Interval
```dart
animation = CurvedAnimation(
parent: _controller,
curve: const Interval(
0.0,
0.5,
curve: Curves.easeIn,
),
);
```
### With Curves.elasticOut
```dart
AnimatedContainer(
duration: const Duration(milliseconds: 1000),
curve: Curves.elasticOut,
transform: Matrix4.rotationZ(_rotated ? 0.5 : 0),
child: const FlutterLogo(),
)
```
## Custom Curves
### Creating a Custom Curve
```dart
import 'dart:math' as math;
class CustomCurve extends Curve {
@override
double transform(double t) {
// t is 0.0 to 1.0
// Return 0.0 to 1.0
return math.pow(t, 2); // Quadratic ease in
}
}
```
**Usage:**
```dart
AnimatedContainer(
curve: CustomCurve(),
duration: const Duration(milliseconds: 300),
width: _expanded ? 200 : 100,
child: const FlutterLogo(),
)
```
### Shake Curve
```dart
class ShakeCurve extends Curve {
@override
double transform(double t) {
return sin(t * pi * 2);
}
}
```
### Smooth Step Curve
```dart
class SmoothStep extends Curve {
final double steps;
const SmoothStep({this.steps = 5});
@override
double transform(double t) {
return (t * steps).floor() / steps;
}
}
```
### Exponential Curve
```dart
class ExponentialCurve extends Curve {
final double exponent;
const ExponentialCurve({this.exponent = 2});
@override
double transform(double t) {
return math.pow(t, exponent);
}
}
```
## Curve Composition
### Cubic Bezier
Flutter doesn't have built-in cubic bezier, but you can approximate:
```dart
class CubicBezier extends Curve {
final double p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y;
const CubicBezier({
required this.p0x,
required this.p0y,
required this.p1x,
required this.p1y,
required this.p2x,
required this.p2y,
required this.p3x,
required this.p3y,
});
@override
double transform(double t) {
// Simplified cubic bezier formula
final u = 1 - t;
return (u * u * u * p0y +
3 * u * u * t * p1y +
3 * u * t * t * p2y +
t * t * t * p3y);
}
}
```
### Combining Curves
```dart
class CombinedCurve extends Curve {
final Curve first;
final Curve second;
final double threshold;
const CombinedCurve({
required this.first,
required this.second,
required this.threshold,
});
@override
double transform(double t) {
if (t < threshold) {
return first.transform(t / threshold) * threshold;
}
return threshold +
second.transform((t - threshold) / (1 - threshold)) * (1 - threshold);
}
}
```
**Usage:**
```dart
AnimatedContainer(
curve: CombinedCurve(
first: Curves.easeIn,
second: Curves.easeOut,
threshold: 0.5,
),
duration: const Duration(milliseconds: 500),
width: _expanded ? 200 : 100,
child: const FlutterLogo(),
)
```
## Choosing the Right Curve
### Motion Design Guidelines
| Animation Type | Recommended Curve | Why |
|---------------|-------------------|-----|
| Fade in/out | easeInOut | Natural opacity change |
| Size change | easeOut | Decelerate feels natural for growth |
| Position slide | easeInOut | Smooth start and stop |
| Rotation | easeInOut | Natural angular acceleration |
| Color change | linear | Uniform color transition |
| Scale | elasticOut | Playful, bouncy feel |
| Reveal | backOut | Dramatic reveal effect |
| Loading | linear or ease | Consistent speed |
| Success | elasticOut | Celebratory feel |
### Platform Conventions
**iOS:**
- Prefers subtle curves (ease, easeInOut)
- Limited use of elastic/bounce
- Consistent with system animations
**Android:**
- Wider variety of curves
- More use of elastic/bounce for feedback
- Material design curves (easeIn, fastOutSlowIn)
**Web:**
- CSS-like curves (ease, ease-in, ease-out, ease-in-out)
- Limited use of complex curves
## Curve Combinations
### Multi-Stage Animation
```dart
// Stage 1: Ease in (0.0 - 0.5)
// Stage 2: Ease out (0.5 - 1.0)
animation1 = CurvedAnimation(
parent: _controller,
curve: const Interval(0.0, 0.5, curve: Curves.easeIn),
);
animation2 = CurvedAnimation(
parent: _controller,
curve: const Interval(0.5, 1.0, curve: Curves.easeOut),
);
```
### Staggered with Different Curves
```dart
// Item 1: Bouncy
item1Animation = CurvedAnimation(
parent: _controller,
curve: const Interval(0.0, 0.3, curve: Curves.elasticOut),
);
// Item 2: Smooth
item2Animation = CurvedAnimation(
parent: _controller,
curve: const Interval(0.1, 0.4, curve: Curves.easeInOut),
);
// Item 3: Linear
item3Animation = CurvedAnimation(
parent: _controller,
curve: const Interval(0.2, 0.5, curve: Curves.linear),
);
```
## Performance Considerations
### Curve Complexity
Simple curves (linear, ease) are faster:
- Fewer calculations
- Less GPU work
- Consistent timing
Complex curves (elastic, bounce) are slower:
- More calculations
- Potential for frame drops
- Variable timing
### Optimization Tips
- Use simpler curves on low-end devices
- Cache curve calculations if needed
- Test performance on target devices
- Profile with Flutter DevTools
## Debugging Curves
### Visualize Curve
```dart
class CurveVisualizer extends StatelessWidget {
final Curve curve;
const CurveVisualizer({super.key, required this.curve});
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: _CurvePainter(curve: curve),
);
}
}
class _CurvePainter extends CustomPainter {
final Curve curve;
const CurvePainter({required this.curve});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.blue
..strokeWidth = 2
..style = PaintingStyle.stroke;
final path = Path();
for (double t = 0; t <= 1; t += 0.01) {
final x = t * size.width;
final y = size.height - (curve.transform(t) * size.height);
if (t == 0) {
path.moveTo(x, y);
} else {
path.lineTo(x, y);
}
}
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(_CurvePainter oldDelegate) => false;
}
```
### Print Curve Values
```dart
void printCurveValues(Curve curve) {
print('Curve: $curve');
for (double t = 0; t <= 1; t += 0.1) {
print('t=$t, output=${curve.transform(t)}');
}
}
// Usage
printCurveValues(Curves.easeInOut);
```
## Accessibility
### Respecting User Preferences
```dart
Curve getAdaptiveCurve(BuildContext context) {
if (MediaQuery.of(context).disableAnimations) {
return Curves.linear; // No easing when animations disabled
}
return Curves.easeInOut;
}
```
### Reduced Motion
```dart
AnimatedContainer(
duration: MediaQuery.of(context).disableAnimations
? Duration.zero
: const Duration(milliseconds: 300),
curve: getAdaptiveCurve(context),
width: _expanded ? 200 : 100,
child: const FlutterLogo(),
)
```
## Common Patterns
### Loading Spinner
```dart
_animation = CurvedAnimation(
parent: _controller,
curve: Curves.linear, // Constant speed
);
```
### Success Checkmark
```dart
_animation = CurvedAnimation(
parent: _controller,
curve: Curves.elasticOut, // Bouncy celebration
);
```
### Page Transition
```dart
_enterAnimation = CurvedAnimation(
parent: _controller,
curve: Curves.easeIn, // Accelerate in
);
_exitAnimation = CurvedAnimation(
parent: _controller,
curve: Curves.easeOut, // Decelerate out
);
```
### Modal Popup
```dart
_animation = CurvedAnimation(
parent: _controller,
curve: Curves.easeOutBack, // Dramatic reveal
);
```
## Curve Comparison
| Curve | Start | Middle | End | Feel |
|-------|-------|--------|-----|------|
| linear | Fast | Fast | Fast | Mechanical |
| ease | Slow | Fast | Slow | Natural |
| easeIn | Slow | Fast | Fast | Accelerating |
| easeOut | Fast | Slow | Slow | Decelerating |
| easeInOut | Slow | Fast | Slow | Smooth |
| elasticIn | Back | Fast | Normal | Bouncy start |
| elasticOut | Normal | Fast | Back | Bouncy end |
| bounceIn | Bounce | Fast | Normal | Bouncy start |
| bounceOut | Normal | Fast | Bounce | Bouncy end |
| backIn | Back | Fast | Normal | Overshoot start |
| backOut | Normal | Fast | Back | Overshoot end |
## Best Practices
### DO
- Use appropriate curves for animation type
- Test on real devices
- Consider platform conventions
- Respect accessibility settings
- Profile performance with DevTools
### DON'T
- Use elastic/bounce for everything (distracting)
- Over-combine curves (confusing motion)
- Ignore device performance
- Use same curve for all animations (boring)
- Forget to handle edge cases (t < 0 or t > 1)
references/explicit.md
# Explicit Animations Reference
Explicit animations provide full control using AnimationController, Tween, and animation status listeners.
## Core Components
### AnimationController
The heart of explicit animations. Manages animation lifecycle, value, and timing.
```dart
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this, // Required TickerProvider
lowerBound: 0.0, // Optional: default 0.0
upperBound: 1.0, // Optional: default 1.0
);
}
@override
void dispose() {
_controller.dispose(); // ALWAYS dispose!
super.dispose();
}
```
**Lifecycle methods:**
```dart
// Start animation forward
_controller.forward();
// Start animation backward
_controller.reverse();
// Stop animation
_controller.stop();
// Repeat animation
_controller.repeat();
// Animate to specific value
await _controller.animateTo(0.5);
// Reset to beginning
_controller.reset();
// Animate with physics simulation
_controller.fling(velocity: 2.0);
// Animate with custom simulation
_controller.animateWith(SpringSimulation(...));
```
**Listening to changes:**
```dart
_controller.addListener(() {
// Value changed - rebuild needed
setState(() {});
});
_controller.addStatusListener((status) {
// Status changed
switch (status) {
case AnimationStatus.dismissed:
print('Animation dismissed (at 0)');
break;
case AnimationStatus.forward:
print('Animation moving forward');
break;
case AnimationStatus.reverse:
print('Animation moving backward');
break;
case AnimationStatus.completed:
print('Animation completed (at 1)');
break;
}
});
```
### Tween
Interpolates between begin and end values.
```dart
// Simple tween
animation = Tween<double>(begin: 0, end: 300).animate(_controller);
// Color tween
animation = ColorTween(
begin: Colors.red,
end: Colors.blue,
).animate(_controller);
// Border radius tween
animation = BorderRadiusTween(
begin: BorderRadius.circular(4),
end: BorderRadius.circular(75),
).animate(_controller);
// Rect tween
animation = RectTween(
begin: Rect.fromLTWH(0, 0, 100, 100),
end: Rect.fromLTWH(100, 100, 200, 200),
).animate(_controller);
```
**Common Tweens:**
- `Tween<T>` - Generic tween (use for most types)
- `ColorTween` - Color interpolation
- `RectTween` - Rectangle interpolation
- `IntTween` - Integer interpolation
- `SizeTween` - Size interpolation
- `OffsetTween` - Offset interpolation
- `BorderRadiusTween` - Border radius interpolation
### CurvedAnimation
Applies a curve to transform animation values.
```dart
animation = CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
reverseCurve: Curves.easeIn, // Optional: different curve for reverse
);
```
**With Interval:**
```dart
animation = CurvedAnimation(
parent: _controller,
curve: const Interval(
0.25, // Start at 25% of controller
0.75, // End at 75% of controller
curve: Curves.easeInOut,
),
);
```
### ReverseAnimation
Reverses the parent animation.
```dart
final forwardAnimation = Tween<double>(begin: 0, end: 1).animate(_controller);
final reverseAnimation = ReverseAnimation(forwardAnimation);
```
## Patterns
### AnimatedWidget Pattern
Best for reusable animated widgets. Automatically handles rebuilds.
```dart
class AnimatedLogo extends AnimatedWidget {
const AnimatedLogo({super.key, required Animation<double> animation})
: super(listenable: animation);
@override
Widget build(BuildContext context) {
final animation = listenable as Animation<double>;
return Center(
child: Container(
height: animation.value,
width: animation.value,
child: const FlutterLogo(),
),
);
}
}
// Usage
@override
Widget build(BuildContext context) {
return AnimatedLogo(animation: _animation);
}
```
### AnimatedBuilder Pattern
Best for complex widgets. Separates animation logic from widget logic.
```dart
class GrowTransition extends StatelessWidget {
const GrowTransition({
required this.child,
required this.animation,
super.key,
});
final Widget child;
final Animation<double> animation;
@override
Widget build(BuildContext context) {
return Center(
child: AnimatedBuilder(
animation: animation,
builder: (context, child) {
return SizedBox(
height: animation.value,
width: animation.value,
child: child,
);
},
child: child,
),
);
}
}
// Usage
@override
Widget build(BuildContext context) {
return GrowTransition(
animation: _animation,
child: const LogoWidget(),
);
}
```
**Why pass child twice?**
The `child` parameter to `AnimatedBuilder` is passed to the `builder` function, allowing the child to be built once and reused, rather than rebuilt every animation frame.
### Multiple Animations Pattern
Animate multiple properties from single controller.
```dart
class MultiPropertyAnimation extends AnimatedWidget {
const MultiPropertyAnimation({super.key, required Animation<double> animation})
: super(listenable: animation);
static final _opacityTween = Tween<double>(begin: 0.1, end: 1);
static final _sizeTween = Tween<double>(begin: 0, end: 300);
static final _colorTween = ColorTween(
begin: Colors.red,
end: Colors.blue,
);
@override
Widget build(BuildContext context) {
final animation = listenable as Animation<double>;
return Center(
child: Opacity(
opacity: _opacityTween.evaluate(animation),
child: Container(
height: _sizeTween.evaluate(animation),
width: _sizeTween.evaluate(animation),
decoration: BoxDecoration(
color: _colorTween.evaluate(animation),
borderRadius: BorderRadius.circular(12),
),
child: const FlutterLogo(),
),
),
);
}
}
```
## Built-in Transitions
Flutter provides pre-built transitions for common use cases.
### FadeTransition
```dart
FadeTransition(
opacity: _animation,
child: const FlutterLogo(),
)
```
### ScaleTransition
```dart
ScaleTransition(
scale: _animation,
child: const FlutterLogo(),
)
```
### SlideTransition
```dart
SlideTransition(
position: Tween<Offset>(
begin: Offset(0, 1),
end: Offset.zero,
).animate(_animation),
child: const FlutterLogo(),
)
```
### SizeTransition
```dart
SizeTransition(
sizeFactor: _animation,
axis: Axis.vertical,
child: const FlutterLogo(),
)
```
### RotationTransition
```dart
RotationTransition(
turns: _animation,
child: const FlutterLogo(),
)
```
### DecoratedBoxTransition
```dart
DecoratedBoxTransition(
decoration: DecorationTween(
begin: BoxDecoration(color: Colors.red),
end: BoxDecoration(color: Colors.blue),
).animate(_animation),
child: const FlutterLogo(),
)
```
### PositionedTransition
```dart
Stack(
children: [
PositionedTransition(
rect: RelativeRectTween(
begin: RelativeRect.fill(calculateRect(context, startPosition)),
end: RelativeRect.fill(calculateRect(context, endPosition)),
).animate(_animation),
child: const FlutterLogo(),
),
],
)
```
## Animation Status Handling
### Basic Status Loop
```dart
_animation.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_controller.reverse();
} else if (status == AnimationStatus.dismissed) {
_controller.forward();
}
});
```
### One-way Animation
```dart
_animation.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_controller.stop(); // Or navigate, show dialog, etc.
}
});
```
### Reset and Repeat
```dart
_animation.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_controller.reset();
_controller.forward();
}
});
```
## Performance Best Practices
### DO
- Always dispose AnimationController
- Use `AnimatedWidget` or `AnimatedBuilder` instead of `setState()` in listeners
- Minimize widget rebuilds during animation
- Use static Tweens when possible (avoid recreating)
- Profile with Flutter DevTools
- Use `timeDilation` for debugging
### DON'T
- Forget to dispose controllers (memory leak)
- Call `setState()` in animation listeners unnecessarily
- Create complex widget trees inside animation builds
- Animate too many widgets simultaneously
- Use animation values directly in expensive operations
### Performance Pattern: RepaintBoundary
```dart
RepaintBoundary(
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return ExpensiveWidget();
},
),
)
```
## Debugging
### Slow Animations
```dart
void main() {
timeDilation = 10.0; // 10x slower
runApp(MyApp());
}
```
### Print Animation Values
```dart
_controller.addListener(() {
print('Animation value: ${_controller.value}');
});
_animation.addStatusListener((status) {
print('Animation status: $status');
});
```
### Flutter DevTools Performance Overlay
1. Run app in debug mode
2. Press 'p' to toggle performance overlay
3. Look for "GPU UI" and "GPU Raster" graphs
4. Aim for 60fps (16.67ms per frame)
## Common Patterns
### Fade In Then Slide Up
```dart
_animation = Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(
parent: _controller,
curve: const Interval(0.0, 0.5, curve: Curves.easeIn),
),
);
_slideAnimation = Tween<Offset>(
begin: Offset(0, 0.5),
end: Offset.zero,
).animate(
CurvedAnimation(
parent: _controller,
curve: const Interval(0.3, 1.0, curve: Curves.easeOut),
),
);
```
### Bounce Effect
```dart
_animation = CurvedAnimation(
parent: _controller,
curve: Curves.elasticOut,
);
```
### Shake Effect
```dart
class ShakeCurve extends Curve {
@override
double transform(double t) {
return sin(t * pi * 2);
}
}
_animation = CurvedAnimation(
parent: _controller,
curve: ShakeCurve(),
);
```
### Parallax Scrolling
```dart
class ParallaxWidget extends StatelessWidget {
const ParallaxWidget({
required this.animation,
required this.child,
super.key,
});
final Animation<double> animation;
final Widget child;
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: animation,
builder: (context, child) {
return Transform.translate(
offset: Offset(0, animation.value * 50),
child: child,
);
},
child: child,
);
}
}
```
## Comparison: Implicit vs Explicit
| Feature | Implicit | Explicit |
|---------|----------|-----------|
| Controller | None needed | AnimationController required |
| Setup | Simple | More complex |
| Control | Limited (duration, curve) | Full (lifecycle, status, physics) |
| Multiple Properties | AnimatedContainer only | Unlimited |
| Performance | Good | Excellent (with patterns) |
| Reusability | Widget-based | Component-based |
| State Monitoring | Limited (onEnd) | Full (status listeners) |
## When to Use Explicit Animations
Use explicit animations when:
- Need full control over animation lifecycle
- Animating multiple properties
- Need to react to animation state changes
- Creating reusable animation components
- Need complex timing or sequencing
- Performance is critical
- Want physics-based or custom animations
Use implicit animations when:
- Simple, one-off animations
- Animation triggered by state change
- No need for fine-grained control
- Want simple, declarative code
references/hero.md
# Hero Animations Reference
Hero animations create shared element transitions between screens, making elements appear to "fly" from one route to another.
## Core Concept
Use two `Hero` widgets with matching `tag` in different routes. Flutter automatically animates the transition between them.
## Basic Hero Animation
### Simple Image Transition
**Source route (list screen):**
```dart
GestureDetector(
onTap: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (context) => const DetailScreen(),
),
);
},
child: Hero(
tag: 'hero-image',
child: Image.asset('images/thumbnail.png'),
),
)
```
**Destination route (detail screen):**
```dart
Scaffold(
appBar: AppBar(title: const Text('Detail')),
body: GestureDetector(
onTap: () => Navigator.of(context).pop(),
child: Hero(
tag: 'hero-image', // Same tag!
child: Image.asset('images/thumbnail.png'),
),
),
)
```
### Custom PhotoHero Widget
Reusable hero widget for images:
```dart
class PhotoHero extends StatelessWidget {
const PhotoHero({
super.key,
required this.photo,
this.onTap,
required this.width,
});
final String photo;
final VoidCallback? onTap;
final double width;
@override
Widget build(BuildContext context) {
return SizedBox(
width: width,
child: Hero(
tag: photo,
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
child: Image.asset(
photo,
fit: BoxFit.contain,
),
),
),
),
);
}
}
```
**Usage in source route:**
```dart
class SourceScreen extends StatelessWidget {
const SourceScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Gallery')),
body: GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
itemCount: 10,
itemBuilder: (context, index) {
return PhotoHero(
photo: 'images/photo_$index.png',
width: 100,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (context) => DetailScreen(photo: 'images/photo_$index.png'),
),
);
},
);
},
),
);
}
}
```
**Usage in destination route:**
```dart
class DetailScreen extends StatelessWidget {
const DetailScreen({super.key, required this.photo});
final String photo;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Detail')),
body: Center(
child: PhotoHero(
photo: photo,
width: 300,
onTap: () => Navigator.of(context).pop(),
),
),
);
}
}
```
## Hero Tag Best Practices
### Using Object as Tag
For unique, consistent tags:
```dart
// Data model
class Photo {
final String url;
final String id;
Photo({required this.url, required this.id});
}
// Source
Hero(
tag: photo.id, // Use unique identifier
child: Image.network(photo.url),
)
// Destination
Hero(
tag: photo.id, // Same unique identifier
child: Image.network(photo.url),
)
```
### Using Data Object as Tag
When data object is consistent:
```dart
// Source
Hero(
tag: photo, // Photo object must be same instance or implement ==
child: Image.network(photo.url),
)
// Destination
Hero(
tag: photo, // Same Photo object
child: Image.network(photo.url),
)
```
**Important:** If using object as tag, ensure proper `==` and `hashCode` implementation.
## Custom Hero Flight Path
### MaterialRectArcTween (Default)
```dart
Hero(
tag: 'hero-image',
flightShuttleBuilder: (flightContext, animation, direction, fromContext, toContext) {
return AnimatedBuilder(
animation: animation,
builder: (context, child) {
return Transform.scale(
scale: animation.value,
child: child,
);
},
child: child,
);
},
createRectTween: (begin, end) {
return MaterialRectArcTween(begin: begin, end: end);
},
child: Image.asset('image.png'),
)
```
### MaterialRectCenterArcTween
Center-based interpolation (good for maintaining aspect ratio):
```dart
static RectTween _createRectTween(Rect? begin, Rect? end) {
return MaterialRectCenterArcTween(begin: begin, end: end);
}
Hero(
tag: 'hero-image',
createRectTween: _createRectTween,
child: Image.asset('image.png'),
)
```
### Custom RectTween
For complete control:
```dart
class LinearRectTween extends Tween<Rect> {
LinearRectTween({required Rect begin, required Rect end})
: super(begin: begin, end: end);
@override
Rect lerp(double t) => Rect.lerp(begin!, end!, t);
}
Hero(
tag: 'hero-image',
createRectTween: (begin, end) => LinearRectTween(begin: begin, end: end),
child: Image.asset('image.png'),
)
```
## Radial Hero Animation
Transform from circle to rectangle during transition.
### RadialExpansion Widget
```dart
import 'dart:math' as math;
class RadialExpansion extends StatelessWidget {
const RadialExpansion({
super.key,
required this.maxRadius,
this.child,
}) : clipRectSize = 2.0 * (maxRadius / math.sqrt2);
final double maxRadius;
final double clipRectSize;
final Widget? child;
@override
Widget build(BuildContext context) {
return ClipOval(
child: Center(
child: SizedBox(
width: clipRectSize,
height: clipRectSize,
child: ClipRect(child: child),
),
),
);
}
}
```
### Radial Photo Widget
```dart
class RadialPhoto extends StatelessWidget {
const RadialPhoto({
super.key,
required this.photo,
this.onTap,
});
final String photo;
final VoidCallback? onTap;
@override
Widget build(BuildContext context) {
return Material(
color: Theme.of(context).primaryColor.withValues(alpha: 0.25),
child: InkWell(
onTap: onTap,
child: Image.asset(
photo,
fit: BoxFit.contain,
),
),
);
}
}
```
### Complete Radial Hero Example
```dart
class RadialHeroAnimation extends StatelessWidget {
const RadialHeroAnimation({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Radial Hero')),
body: ListView(
children: List.generate(6, (index) {
final photo = 'images/photo_$index.png';
return Hero(
tag: photo,
createRectTween: _createRectTween,
flightShuttleBuilder: (flightContext, animation, direction, fromContext, toContext) {
return AnimatedBuilder(
animation: animation,
builder: (context, child) {
return FadeTransition(
opacity: animation,
child: child,
);
},
child: child,
);
},
child: RadialExpansion(
maxRadius: 120,
child: RadialPhoto(
photo: photo,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (context) => DetailScreen(photo: photo),
),
);
},
),
),
);
}),
),
);
}
static RectTween _createRectTween(Rect? begin, Rect? end) {
return MaterialRectCenterArcTween(begin: begin, end: end);
}
}
class DetailScreen extends StatelessWidget {
const DetailScreen({super.key, required this.photo});
final String photo;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Detail')),
body: Center(
child: Hero(
tag: photo,
createRectTween: RadialHeroAnimation._createRectTween,
child: SizedBox(
width: 300,
height: 300,
child: RadialPhoto(
photo: photo,
onTap: () => Navigator.of(context).pop(),
),
),
),
),
);
}
}
```
## Custom Placeholder During Flight
### flightShuttleBuilder
Customize hero appearance during flight:
```dart
Hero(
tag: 'hero-image',
flightShuttleBuilder: (flightContext, animation, direction, fromContext, toContext) {
// fromContext - source hero's context
// toContext - destination hero's context
// direction - HeroFlightDirection.push or pop
// animation - Animation<double> for the flight
return AnimatedBuilder(
animation: animation,
builder: (context, child) {
return Transform.rotate(
angle: animation.value * math.pi,
child: child,
);
},
child: child,
);
},
child: Image.asset('image.png'),
)
```
### Replace Entire Hero During Flight
```dart
Hero(
tag: 'hero-image',
flightShuttleBuilder: (flightContext, animation, direction, fromContext, toContext) {
// Show different widget during flight
return Container(
width: 100,
height: 100,
color: Colors.blue,
);
},
child: Image.asset('image.png'),
)
```
## Transition Settings
### Animation Duration
```dart
MaterialApp(
// Set global hero animation duration
theme: ThemeData(
pageTransitionsTheme: PageTransitionsTheme(
builders: {
TargetPlatform.android: ZoomPageTransitionsBuilder(),
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
},
),
),
)
```
### Custom PageTransitionBuilder
```dart
class CustomHeroTransitionBuilder extends PageTransitionsBuilder {
@override
Widget buildTransitions<T>(
PageRoute<T> route,
BuildContext? secondaryContext,
Widget child,
) {
return FadeUpwardsPageTransitionsBuilder().buildTransitions(
route,
secondaryContext,
child,
);
}
}
```
## Hero Mode
### Disable Hero Animations
```dart
HeroMode(
enabled: false, // Disable all child hero animations
child: ListView(
children: [
Hero(tag: 'image1', child: Image.asset('1.png')),
Hero(tag: 'image2', child: Image.asset('2.png')),
],
),
)
```
### Conditional Hero Mode
```dart
HeroMode(
enabled: !_disableAnimations,
child: Hero(tag: 'image', child: Image.asset('image.png')),
)
```
## Debugging Hero Animations
### Slow Animation
```dart
void main() {
timeDilation = 10.0; // 10x slower
runApp(MyApp());
}
```
### Visualize Hero Bounds
```dart
void main() {
debugPaintSizeEnabled = true;
runApp(MyApp());
}
```
### Print Hero Tags
```dart
class DebugHero extends StatelessWidget {
const DebugHero({
super.key,
required this.tag,
required this.child,
});
final Object tag;
final Widget child;
@override
Widget build(BuildContext context) {
print('Building Hero with tag: $tag');
return Hero(tag: tag, child: child);
}
}
```
## Best Practices
### DO
- Use unique, consistent tags (often the data object itself)
- Keep hero widget trees similar between routes
- Wrap images in `Material` with transparent color for "pop" effect
- Use `timeDilation` to debug transitions
- Consider `createRectTween` for custom flight paths
- Use `flightShuttleBuilder` for custom flight appearance
### DON'T
- Use duplicate tags (conflicts!)
- Change hero structure significantly between routes (jarring transition)
- Forget `Material` wrapper (no splash effect)
- Use very large images in heroes (performance)
- Ignore aspect ratio during transition (distortion)
## Common Patterns
### Grid to Fullscreen Image
**Grid item:**
```dart
PhotoHero(
photo: photo.url,
width: 150, // Smaller in grid
onTap: () => Navigator.push(..., detailScreen),
)
```
**Fullscreen:**
```dart
PhotoHero(
photo: photo.url,
width: MediaQuery.of(context).size.width, // Full width
onTap: () => Navigator.pop(),
)
```
### List Header to Page Header
**List:**
```dart
Hero(
tag: 'header',
child: SizedBox(
height: 200,
child: Image.asset('header.jpg'),
),
)
```
**Page:**
```dart
Hero(
tag: 'header',
child: SizedBox(
height: 300, // Larger on detail page
child: Image.asset('header.jpg'),
),
)
```
### Shared Element with Content Update
```dart
// In both routes
Hero(
tag: 'card',
child: Card(
child: Column(
children: [
Image.asset('image.png'),
Text(showDetails ? 'Full description...' : 'Brief description'),
],
),
),
)
```
## Performance Tips
- Optimize hero images (compress, lazy load)
- Use `RepaintBoundary` around hero children if needed
- Test on low-end devices
- Profile with Flutter DevTools Performance overlay
- Avoid complex widget trees inside hero
## Accessibility
- Respect `MediaQuery.disableAnimations` setting
- Consider alternative navigation for users who prefer no animations
- Ensure hero content remains accessible during transition
- Test with screen readers
## Advanced Techniques
### Multiple Heroes on Same Route
```dart
class PhotoDetail extends StatelessWidget {
const PhotoDetail({super.key, required this.photos});
final List<String> photos;
@override
Widget build(BuildContext context) {
return Stack(
children: [
// Main photo hero
Positioned.fill(
child: Hero(
tag: photos[0],
child: Image.asset(photos[0]),
),
),
// Overlay hero (e.g., like button)
Positioned(
top: 16,
right: 16,
child: Hero(
tag: 'like-button',
child: Icon(Icons.favorite),
),
),
],
);
}
}
```
### Nested Heroes
```dart
Hero(
tag: 'parent',
child: Card(
child: Column(
children: [
Hero(
tag: 'child-image',
child: Image.asset('image.png'),
),
Hero(
tag: 'child-title',
child: Text('Title'),
),
],
),
),
)
```
### Hero with Scroll Views
```dart
class ScrollableHero extends StatelessWidget {
@override
Widget build(BuildContext context) {
return CustomScrollView(
slivers: [
SliverAppBar(
expandedHeight: 300,
flexibleSpace: FlexibleSpaceBar(
background: Hero(
tag: 'header-image',
child: Image.asset('header.jpg', fit: BoxFit.cover),
),
),
),
SliverList(
delegate: SliverChildListDelegate([
// Content
]),
),
],
);
}
}
```
references/implicit.md
# Implicit Animations Reference
Implicit animations automatically handle animations when widget properties change. No AnimationController or explicit state management needed.
## Core Concept
Implicitly animated widgets extend `ImplicitlyAnimatedWidget`. When you change a property (color, size, etc.), the widget automatically animates to the new value using a specified duration and curve.
## Available Widgets
### AnimatedContainer
Animates multiple properties simultaneously.
```dart
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 ? 20 : 8),
),
child: const FlutterLogo(),
)
```
**Animatable properties:**
- `width`, `height` - Size
- `color` - Background color
- `decoration` - BoxDecoration (includes border, shadow, image, etc.)
- `padding` - Padding
- `margin` - Margin (via decoration)
- `transform` - Matrix4 transformation
### AnimatedOpacity
Fades a widget in and out.
```dart
AnimatedOpacity(
opacity: _visible ? 1.0 : 0.0,
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut,
onEnd: () => print('Animation complete'),
child: const Text('Hello'),
)
```
### AnimatedPadding
Animates padding changes.
```dart
AnimatedPadding(
padding: EdgeInsets.all(_padded ? 16.0 : 8.0),
duration: const Duration(milliseconds: 300),
curve: Curves.ease,
child: const Card(child: Text('Padded content')),
)
```
### AnimatedPositioned
Animates position within a Stack.
```dart
Stack(
children: [
AnimatedPositioned(
top: _top ? 0 : 100,
left: _left ? 0 : 100,
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut,
child: const FlutterLogo(),
),
],
)
```
### AnimatedAlign
Animates alignment changes.
```dart
AnimatedAlign(
alignment: _aligned ? Alignment.topLeft : Alignment.bottomRight,
duration: const Duration(milliseconds: 400),
curve: Curves.easeInOut,
child: const Text('Align me'),
)
```
### AnimatedContainer (Multiple Properties)
```dart
AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
transform: Matrix4.rotationZ(_rotated ? 0.5 : 0),
child: const FlutterLogo(),
)
```
### TweenAnimationBuilder
Custom tween animation without creating a custom widget.
```dart
TweenAnimationBuilder<double>(
tween: Tween<double>(begin: 0, end: 1),
duration: const Duration(seconds: 1),
curve: Curves.easeInOut,
builder: (context, value, child) {
return Opacity(
opacity: value,
child: Transform.scale(
scale: value,
child: child,
),
);
},
child: const FlutterLogo(),
)
```
### AnimatedSwitcher
Cross-fades between two widgets.
```dart
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
transitionBuilder: (Widget child, Animation<double> animation) {
return FadeTransition(
opacity: animation,
child: child,
);
},
child: _showFirst
? const Text('First')
: const Text('Second'),
)
```
With custom size transition:
```dart
AnimatedSwitcher(
duration: const Duration(milliseconds: 500),
transitionBuilder: (Widget child, Animation<double> animation) {
return SizeTransition(
sizeFactor: animation,
axis: Axis.vertical,
child: child,
);
},
child: _expanded
? const Text('Expanded content')
: const Text('Collapsed content'),
)
```
### AnimatedDefaultTextStyle
Animates text style changes.
```dart
AnimatedDefaultTextStyle(
duration: const Duration(milliseconds: 300),
style: TextStyle(
fontSize: _large ? 32 : 16,
color: _colored ? Colors.blue : Colors.black,
fontWeight: _bold ? FontWeight.bold : FontWeight.normal,
),
child: const Text('Animated text'),
)
```
### AnimatedPhysicalModel
Animates elevation, color, and shape with physical shadow.
```dart
AnimatedPhysicalModel(
duration: const Duration(milliseconds: 300),
shape: BoxShape.rectangle,
elevation: _elevated ? 12 : 0,
color: _elevated ? Colors.blue : Colors.grey[300]!,
borderRadius: BorderRadius.circular(12),
child: const Padding(
padding: EdgeInsets.all(16),
child: Text('Physical model'),
),
)
```
### AnimatedTheme
Animates theme changes.
```dart
AnimatedTheme(
duration: const Duration(milliseconds: 500),
data: ThemeData(
brightness: _darkMode ? Brightness.dark : Brightness.light,
primaryColor: _darkMode ? Colors.blue[700] : Colors.blue,
),
child: const MyChildWidget(),
)
```
## Common Parameters
All implicit animations support these parameters:
```dart
AnimatedContainer(
duration: Duration(milliseconds: 300), // Required: Animation duration
curve: Curves.easeInOut, // Optional: Animation curve
onEnd: () {}, // Optional: Completion callback
child: Widget(), // The child widget to animate
)
```
### Duration
```dart
const Duration(milliseconds: 300) // 0.3 seconds
const Duration(seconds: 1) // 1 second
const Duration(days: 1) // 1 day
```
### Curves
Common curves from `Curves` class:
```dart
Curves.linear // Linear (no easing)
Curves.ease // Simple ease in and out
Curves.easeIn // Slow start, fast end
Curves.easeOut // Fast start, slow end
Curves.easeInOut // Slow start and end, fast middle
Curves.easeInCubic // Cubic ease in
Curves.easeOutCubic // Cubic ease out
Curves.easeInOutCubic // Cubic ease in and out
Curves.elasticIn // Bounces in
Curves.elasticOut // Bounces out
Curves.elasticInOut // Bounces in and out
Curves.bounceIn // Bounces in (more subtle)
Curves.bounceOut // Bounces out (more subtle)
```
Custom curve:
```dart
class ShakeCurve extends Curve {
@override
double transform(double t) => sin(t * pi * 2);
}
// Usage
AnimatedContainer(
curve: ShakeCurve(),
// ...
)
```
### onEnd Callback
Trigger action when animation completes:
```dart
AnimatedOpacity(
opacity: _visible ? 1.0 : 0.0,
duration: const Duration(milliseconds: 300),
onEnd: () {
if (!_visible) {
// Animation finished fading out
// Remove widget, navigate, etc.
}
},
child: const Text('Hello'),
)
```
## Best Practices
### DO
- Use implicit animations for simple, one-off animations
- Set appropriate durations for natural feel (200-500ms typical)
- Use curves to make animations feel natural
- Use `onEnd` callback for post-animation actions
- Test animations on various devices
- Consider `MediaQuery.disableAnimations` for accessibility
### DON'T
- Use implicit animations for complex, multi-property sequences (use staggered instead)
- Forget to set duration (defaults to instant change)
- Use extremely long durations (> 1s) without good reason
- Animate too many properties simultaneously (performance impact)
- Nest implicit animations deeply (can cause jank)
## Performance Tips
- Implicit animations are optimized but still rebuild during animation
- Avoid animating complex widget trees
- Use `RepaintBoundary` around animated children to isolate repaints
- Test performance with Flutter DevTools Performance overlay
- Consider using `AnimatedBuilder` for complex widgets to minimize rebuilds
## Debugging
### Slow Animations
```dart
// Slow all animations by 10x during development
timeDilation = 10.0;
```
### Show Repaint Rainbows
```dart
void main() {
debugPaintSizeEnabled = true;
runApp(MyApp());
}
```
### Performance Overlay
```dart
void main() {
runApp(MyApp());
}
// In debug mode, press 'p' to toggle performance overlay
```
## Comparison: Implicit vs Explicit
| Feature | Implicit | Explicit |
|---------|----------|-----------|
| Controller needed | No | Yes (AnimationController) |
| Setup complexity | Low | Medium/High |
| Reusability | Widget-based | Controller-based |
| Multiple properties | AnimatedContainer only | Unlimited |
| Lifecycle control | Limited | Full |
| State monitoring | Limited (onEnd) | Full (addStatusListener) |
| Performance | Good | Excellent (with proper patterns) |
## When to Use Implicit Animations
Use implicit animations when:
- Animating a single property
- Animation is triggered by state change
- No need for fine-grained control
- Want simple, declarative code
- Animation is one-time or simple toggle
Use explicit animations when:
- Need full control over animation lifecycle
- Animating multiple properties
- Need to react to animation state
- Creating reusable animation components
- Need complex timing or sequencing
- Performance is critical
references/physics.md
# Physics-Based Animations Reference
Physics-based animations create natural-feeling motion using simulations like springs, gravity, and momentum.
## Core Concept
Instead of linear interpolation, use physics simulations for realistic motion that responds naturally to forces like velocity and damping.
## Fling Animation
### Basic Fling
```dart
class FlingWidget extends StatefulWidget {
@override
State<FlingWidget> createState() => _FlingWidgetState();
}
class _FlingWidgetState extends State<FlingWidget>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 1),
vsync: this,
);
_controller.fling(velocity: 2.0);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform.translate(
offset: Offset(_controller.value * 100, 0),
child: child,
);
},
child: const FlutterLogo(),
);
}
}
```
### Fling with Gesture
```dart
class DraggableFling extends StatefulWidget {
@override
State<DraggableFling> createState() => _DraggableFlingState();
}
class _DraggableFlingState extends State<DraggableFling>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
double _dragX = 0;
Offset? _startPosition;
@override
void initState() {
super.initState();
_controller = AnimationController.unbounded(vsync: this);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _handlePanStart(DragStartDetails details) {
_controller.stop();
_startPosition = details.globalPosition;
}
void _handlePanUpdate(DragUpdateDetails details) {
if (_startPosition != null) {
setState(() {
_dragX += details.delta.dx;
});
}
}
void _handlePanEnd(DragEndDetails details) {
_controller.fling(velocity: details.velocity.pixelsPerSecond.dx / 1000);
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onPanStart: _handlePanStart,
onPanUpdate: _handlePanUpdate,
onPanEnd: _handlePanEnd,
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform.translate(
offset: Offset(_dragX + _controller.value, 0),
child: child,
);
},
child: const FlutterLogo(),
),
);
}
}
```
## Spring Simulation
### Basic Spring
```dart
class SpringAnimation extends StatefulWidget {
@override
State<SpringAnimation> createState() => _SpringAnimationState();
}
class _SpringAnimationState extends State<SpringAnimation>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController.unbounded(vsync: this);
_controller.animateWith(
SpringSimulation(
spring: const SpringDescription(
mass: 1.0,
stiffness: 200.0,
damping: 10.0,
),
start: 0.0,
end: 1.0,
velocity: 0.0,
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform.scale(
scale: _controller.value,
child: child,
);
},
child: const FlutterLogo(),
);
}
}
```
### Spring Description Parameters
```dart
SpringDescription(
mass: 1.0, // Mass of the object (higher = more inertia)
stiffness: 200.0, // Spring stiffness (higher = faster, stiffer)
damping: 10.0, // Damping ratio (higher = less oscillation)
)
)
```
**Mass:**
- How "heavy" the object feels
- Typical values: 0.5 - 5.0
- Higher mass = more momentum, slower response
**Stiffness:**
- How stiff the spring is
- Typical values: 100 - 500
- Higher stiffness = faster oscillation
- Lower stiffness = slower, more "bouncy"
**Damping:**
- How quickly oscillation stops
- Typical values: 5 - 20
- Higher damping = less bounce
- Lower damping = more bounce
- Critical damping: ~15-18
### Spring Presets
**Bouncy:**
```dart
SpringDescription(
mass: 0.5,
stiffness: 300,
damping: 8,
)
```
**Snappy:**
```dart
SpringDescription(
mass: 1.0,
stiffness: 400,
damping: 18,
)
```
**Gentle:**
```dart
SpringDescription(
mass: 2.0,
stiffness: 150,
damping: 20,
)
```
## Gravity Simulation
### Falling Animation
```dart
class GravityAnimation extends StatefulWidget {
@override
State<GravityAnimation> createState() => _GravityAnimationState();
}
class _GravityAnimationState extends State<GravityAnimation>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController.unbounded(vsync: this);
_controller.animateWith(
GravitySimulation(
acceleration: 980, // pixels/s²
distance: 500, // pixels to fall
startDistance: 0,
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform.translate(
offset: Offset(0, _controller.value),
child: child,
);
},
child: const FlutterLogo(),
);
}
}
```
## Scroll Physics
### Bouncing Scroll
```dart
CustomScrollView(
physics: const BouncingScrollPhysics(),
slivers: [
SliverList(
delegate: SliverChildListDelegate([
// Items
]),
),
],
)
```
### Clamping Scroll
```dart
CustomScrollView(
physics: const ClampingScrollPhysics(),
slivers: [
SliverList(
delegate: SliverChildListDelegate([
// Items
]),
),
],
)
```
### Fixed Scroll
```dart
CustomScrollView(
physics: const NeverScrollableScrollPhysics(),
slivers: [
SliverList(
delegate: SliverChildListDelegate([
// Items
]),
),
],
)
```
### Platform-Adaptive Scroll
```dart
CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
SliverList(
delegate: SliverChildListDelegate([
// Items
]),
),
],
)
```
## Custom Simulation
### Creating Custom Simulation
```dart
class CustomSimulation extends Simulation {
final double target;
CustomSimulation({required this.target});
@override
double x(double time) {
// Calculate position at given time
return target * (1 - math.exp(-time / 0.5));
}
@override
double dx(double time) {
// Calculate velocity (derivative) at given time
return target * math.exp(-time / 0.5) / 0.5;
}
@override
bool isDone(double time) {
return dx(time).abs() < 0.01;
}
}
```
**Usage:**
```dart
_controller.animateWith(
CustomSimulation(target: 1.0),
);
```
### Decay Simulation
```dart
class DecaySimulation extends Simulation {
final double velocity;
DecaySimulation({required this.velocity});
@override
double x(double time) {
return velocity * (1 - math.exp(-time));
}
@override
double dx(double time) {
return velocity * math.exp(-time);
}
@override
bool isDone(double time) {
return dx(time).abs() < 0.1;
}
}
```
## Combining Physics and Animation
### Spring with Fallback
```dart
void animateWithSpring({
required AnimationController controller,
required double start,
required double end,
}) {
try {
controller.animateWith(
SpringSimulation(
spring: const SpringDescription(
mass: 1.0,
stiffness: 200.0,
damping: 15.0,
),
start: start,
end: end,
velocity: 0.0,
),
);
} catch (e) {
// Fallback to linear animation
controller.animateTo(end);
}
}
```
### Spring with Completion Callback
```dart
_controller.animateWith(
SpringSimulation(
spring: const SpringDescription(
mass: 1.0,
stiffness: 200.0,
damping: 15.0,
),
start: 0.0,
end: 1.0,
velocity: 0.0,
),
);
_controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
// Animation complete
}
});
```
## Physics for Gestures
### Swipe to Dismiss with Physics
```dart
class DismissibleWithPhysics extends StatefulWidget {
final Widget child;
final VoidCallback onDismiss;
const DismissibleWithPhysics({
super.key,
required this.child,
required this.onDismiss,
});
@override
State<DismissibleWithPhysics> createState() => _DismissibleWithPhysicsState();
}
class _DismissibleWithPhysicsState extends State<DismissibleWithPhysics>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
double _dragX = 0;
bool _isDismissing = false;
@override
void initState() {
super.initState();
_controller = AnimationController.unbounded(vsync: this);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _handlePanEnd(DragEndDetails details) {
final velocity = details.velocity.pixelsPerSecond.dx;
final screenWidth = MediaQuery.of(context).size.width;
if (velocity.abs() > 500 || _dragX.abs() > screenWidth / 3) {
// Dismiss
_controller.fling(velocity: velocity / screenWidth);
_isDismissing = true;
_controller.addStatusListener((status) {
if (status == AnimationStatus.completed && _isDismissing) {
widget.onDismiss();
}
});
} else {
// Spring back
_controller.animateWith(
SpringSimulation(
spring: const SpringDescription(
mass: 1.0,
stiffness: 300.0,
damping: 15.0,
),
start: _dragX,
end: 0,
velocity: velocity,
),
);
}
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onPanUpdate: (details) {
if (!_controller.isAnimating) {
setState(() {
_dragX += details.delta.dx;
});
}
},
onPanEnd: _handlePanEnd,
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform.translate(
offset: Offset(_dragX + _controller.value, 0),
child: child,
);
},
child: widget.child,
),
);
}
}
```
## Performance Considerations
### Simulation Complexity
Physics simulations can be computationally expensive. Optimize by:
- Using simpler simulations when possible (e.g., linear interpolation)
- Reducing simulation resolution
- Caching simulation results
- Using built-in physics widgets
### Spring Tuning
Finding the right spring parameters can be trial and error:
1. Start with moderate values (mass: 1.0, stiffness: 200, damping: 15)
2. Adjust for feel:
- Too slow? Increase stiffness or decrease mass
- Too bouncy? Increase damping
- Not bouncy enough? Decrease damping or stiffness
3. Test on real devices
## Debugging
### Visualize Physics
```dart
void main() {
timeDilation = 5.0; // Slow down physics
runApp(MyApp());
}
```
### Print Simulation Values
```dart
_controller.addListener(() {
print('Position: ${_controller.value}');
});
_controller.addStatusListener((status) {
print('Status: $status');
});
```
### Physics Inspector
Use Flutter DevTools to profile physics animations:
1. Open Performance overlay
2. Look for frame drops during physics simulations
3. Identify expensive calculations
## Best Practices
### DO
- Use physics simulations for natural-feeling motion
- Tune spring parameters for desired feel
- Test on various devices and screen sizes
- Profile performance with DevTools
- Provide fallback animations for physics failures
### DON'T
- Use complex physics when simple curves suffice
- Forget to handle simulation completion
- Use overly bouncy animations (distracting)
- Ignore accessibility (respect disable animations preference)
- Assume physics simulation completes instantly
## Common Physics Patterns
### Bouncy Button Press
```dart
_controller.animateWith(
SpringSimulation(
spring: const SpringDescription(
mass: 0.5,
stiffness: 500,
damping: 10,
),
start: 1.0,
end: 0.9,
velocity: 0,
),
);
```
### Swipeable Card
```dart
_controller.fling(
velocity: details.velocity.pixelsPerSecond.dx / 1000,
);
```
### Pull to Refresh
```dart
RefreshIndicator(
onRefresh: () async {
// Refresh data
},
child: ListView(...),
)
```
## Accessibility
- Respect `MediaQuery.disableAnimations` setting
- Provide non-physics alternatives
- Ensure physics animations don't cause motion sickness
- Test with reduced motion settings
## Platform-Specific Physics
### Adaptive Scroll Physics
```dart
class AdaptiveScrollPhysics extends ScrollPhysics {
const AdaptiveScrollPhysics({super.parent});
@override
AdaptiveScrollPhysics applyTo(ScrollPhysics? ancestor) {
return AdaptiveScrollPhysics(parent: buildParent(ancestor));
}
@override
SpringDescription get spring => Platform.isIOS
? const SpringDescription(
mass: 0.5,
stiffness: 100,
damping: 10,
)
: const SpringDescription(
mass: 0.8,
stiffness: 150,
damping: 15,
);
}
```
**Usage:**
```dart
ListView(
physics: const AdaptiveScrollPhysics(),
children: [...],
)
```
references/staggered.md
# Staggered Animations Reference
Staggered animations run multiple animations with different timing offsets, creating sequential or overlapping visual effects.
## Core Concept
All animations share one `AnimationController`. Each animation has an `Interval` defining when it starts and ends within the controller's timeline.
## Basic Staggered Animation
### Two-Property Stagger
```dart
class StaggeredFadeSlide extends StatelessWidget {
const StaggeredFadeSlide({super.key, required this.controller});
final AnimationController controller;
// Fade in first (0.0 - 0.5 of controller)
late final Animation<double> opacity = Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(
parent: controller,
curve: const Interval(0.0, 0.5, curve: Curves.easeIn),
),
);
// Slide in later (0.25 - 1.0 of controller)
late final Animation<Offset> slide = Tween<Offset>(
begin: Offset(0, 0.5),
end: Offset.zero,
).animate(
CurvedAnimation(
parent: controller,
curve: const Interval(0.25, 1.0, curve: Curves.easeOut),
),
);
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: controller,
builder: (context, child) {
return FadeTransition(
opacity: opacity,
child: SlideTransition(
position: slide,
child: child,
),
);
},
child: const Text('Staggered animation'),
);
}
}
```
### Multiple Intervals Example
```dart
class MultiPropertyStagger extends StatelessWidget {
const MultiPropertyStagger({super.key, required this.controller});
final AnimationController controller;
// Opacity: 0.0 - 0.1 (10%)
late final Animation<double> opacity = Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(
parent: controller,
curve: const Interval(0.0, 0.1, curve: Curves.ease),
),
);
// Width: 0.125 - 0.25 (12.5% - 25%)
late final Animation<double> width = Tween<double>(begin: 50, end: 150).animate(
CurvedAnimation(
parent: controller,
curve: const Interval(0.125, 0.25, curve: Curves.ease),
),
);
// Height: 0.25 - 0.375 (25% - 37.5%)
late final Animation<double> height = Tween<double>(begin: 50, end: 150).animate(
CurvedAnimation(
parent: controller,
curve: const Interval(0.25, 0.375, curve: Curves.ease),
),
);
// Border radius: 0.375 - 0.5 (37.5% - 50%)
late final Animation<BorderRadius?> borderRadius = BorderRadiusTween(
begin: BorderRadius.circular(4),
end: BorderRadius.circular(75),
).animate(
CurvedAnimation(
parent: controller,
curve: const Interval(0.375, 0.5, curve: Curves.ease),
),
);
// Color: 0.5 - 0.625 (50% - 62.5%)
late final Animation<Color?> color = ColorTween(
begin: Colors.red,
end: Colors.orange,
).animate(
CurvedAnimation(
parent: controller,
curve: const Interval(0.5, 0.625, curve: Curves.ease),
),
);
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: controller,
builder: (context, child) {
return Opacity(
opacity: opacity.value,
child: Container(
width: width.value,
height: height.value,
decoration: BoxDecoration(
color: color.value,
borderRadius: borderRadius.value,
),
child: child,
),
);
},
child: const FlutterLogo(),
);
}
}
```
## Controller Setup
### Single Controller
```dart
class _StaggerState extends State<StaggerWidget>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 2000),
vsync: this,
);
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return StaggerAnimation(controller: _controller.view);
}
}
```
### Multiple Controllers
For independent animation sequences:
```dart
class _ComplexStaggerState extends State<ComplexStaggerWidget>
with TickerProviderStateMixin {
late AnimationController _fadeInController;
late AnimationController _slideController;
@override
void initState() {
super.initState();
_fadeInController = AnimationController(
duration: const Duration(milliseconds: 500),
vsync: this,
);
_slideController = AnimationController(
duration: const Duration(milliseconds: 500),
vsync: this,
);
// Start fade, then slide
_fadeInController.forward().then((_) {
_slideController.forward();
});
}
@override
void dispose() {
_fadeInController.dispose();
_slideController.dispose();
super.dispose();
}
}
```
## Interval Timing
### Understanding Interval
```dart
Interval(
0.25, // Start at 25% of controller duration
0.75, // End at 75% of controller duration
curve: Curves.easeInOut,
)
```
**Example:** If controller duration is 2000ms:
- Animation starts at 500ms (0.25 * 2000)
- Animation ends at 1500ms (0.75 * 2000)
- Animation takes 1000ms (1500 - 500)
### Overlapping Intervals
```dart
// Animation 1: 0.0 - 0.5 (first half)
anim1 = Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(
parent: controller,
curve: const Interval(0.0, 0.5),
),
);
// Animation 2: 0.3 - 0.8 (starts before anim1 ends)
anim2 = Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(
parent: controller,
curve: const Interval(0.3, 0.8),
),
);
// Animation 3: 0.6 - 1.0 (starts after anim1 ends)
anim3 = Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(
parent: controller,
curve: const Interval(0.6, 1.0),
),
);
```
### Gaps Between Animations
```dart
// Animation 1: 0.0 - 0.4
// Gap: 0.4 - 0.5
// Animation 2: 0.5 - 0.9
anim1 = Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(
parent: controller,
curve: const Interval(0.0, 0.4),
),
);
anim2 = Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(
parent: controller,
curve: const Interval(0.5, 0.9),
),
);
```
## Staggered List Animation
### Menu Items Animation
```dart
class StaggeredMenu extends StatefulWidget {
const StaggeredMenu({super.key});
static const _menuItems = [
'Home',
'Profile',
'Settings',
'About',
];
@override
State<StaggeredMenu> createState() => _StaggeredMenuState();
}
class _StaggeredMenuState extends State<StaggeredMenu>
with SingleTickerProviderStateMixin {
static const _itemDelayTime = Duration(milliseconds: 50);
static const _itemAnimationTime = Duration(milliseconds: 250);
late AnimationController _controller;
late List<Animation<double>> _itemAnimations;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: _itemAnimationTime +
(_itemDelayTime * (StaggeredMenu._menuItems.length - 1)),
vsync: this,
);
_itemAnimations = StaggeredMenu._menuItems.map((item) {
final index = StaggeredMenu._menuItems.indexOf(item);
return Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(
index * 0.1, // Start later for each item
(index * 0.1) + 0.4, // Each takes 40% of controller
curve: Curves.easeOut,
),
),
);
}).toList();
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: StaggeredMenu._menuItems.length,
itemBuilder: (context, index) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return SlideTransition(
position: Tween<Offset>(
begin: Offset(1.0, 0),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _controller,
curve: Interval(
index * 0.1,
(index * 0.1) + 0.4,
curve: Curves.easeOut,
),
)),
child: FadeTransition(
opacity: _itemAnimations[index],
child: child,
),
);
},
child: ListTile(
title: Text(StaggeredMenu._menuItems[index]),
),
);
},
);
}
}
```
### Grid Animation
```dart
class StaggeredGrid extends StatefulWidget {
const StaggeredGrid({super.key, required this.itemCount});
final int itemCount;
@override
State<StaggeredGrid> createState() => _StaggeredGridState();
}
class _StaggeredGridState extends State<StaggeredGrid>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late List<Animation<double>> _animations;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 1500),
vsync: this,
);
_animations = List.generate(widget.itemCount, (index) {
return Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(
index / widget.itemCount,
(index / widget.itemCount) + 0.3,
curve: Curves.easeOut,
),
),
);
});
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
itemCount: widget.itemCount,
itemBuilder: (context, index) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return FadeTransition(
opacity: _animations[index],
child: ScaleTransition(
scale: _animations[index],
child: child,
),
);
},
child: Card(
child: Center(child: Text('Item $index')),
),
);
},
);
}
}
```
## Complex Staggered Patterns
### Sequential Completion
```dart
animation1 = Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(
parent: controller,
curve: const Interval(0.0, 0.3),
),
);
animation2 = Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(
parent: controller,
curve: const Interval(0.3, 0.6),
),
);
animation3 = Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(
parent: controller,
curve: const Interval(0.6, 1.0),
),
);
```
### Ripple Effect
```dart
// Center expands first, then outward
for (int i = 0; i < itemCount; i++) {
final distance = (i - centerIndex).abs();
animations[i] = Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(
parent: controller,
curve: Interval(
distance * 0.1,
(distance * 0.1) + 0.3,
curve: Curves.easeOut,
),
),
);
}
```
### Staggered Reveal
```dart
// Reveal items one by one from top
for (int i = 0; i < itemCount; i++) {
animations[i] = CurvedAnimation(
parent: controller,
curve: Interval(
i * 0.1,
(i * 0.1) + 0.15,
curve: Curves.easeOut,
),
);
}
// Use for opacity or transform
Opacity(opacity: animations[i].value)
```
## Duration Calculation
### Calculate Total Duration
```dart
const itemDelay = Duration(milliseconds: 50);
const itemAnimationTime = Duration(milliseconds: 250);
const itemCount = 10;
final totalDuration = itemAnimationTime +
(itemDelay * (itemCount - 1));
// 250ms + (50ms * 9) = 700ms
```
### Using in Controller
```dart
_controller = AnimationController(
duration: totalDuration,
vsync: this,
);
```
### Dynamic Duration
```dart
late AnimationController _controller;
late Duration _totalDuration;
@override
void initState() {
super.initState();
final itemCount = _items.length;
const itemDelay = Duration(milliseconds: 50);
const itemAnimationTime = Duration(milliseconds: 250);
_totalDuration = itemAnimationTime + (itemDelay * (itemCount - 1));
_controller = AnimationController(
duration: _totalDuration,
vsync: this,
);
// Create animations...
_controller.forward();
}
```
## Repeating Staggered Animations
### Loop on Completion
```dart
_controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_controller.reset();
_controller.forward();
}
});
```
### Ping-Pong (Forward and Reverse)
```dart
_controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_controller.reverse();
} else if (status == AnimationStatus.dismissed) {
_controller.forward();
}
});
```
## Debugging Staggered Animations
### Slow Animation
```dart
void main() {
timeDilation = 10.0; // 10x slower
runApp(MyApp());
}
```
### Print Interval Ranges
```dart
for (int i = 0; i < _animations.length; i++) {
final anim = _animations[i] as CurvedAnimation;
print('Animation $i: ${anim.curve}');
}
// Output:
// Animation 0: Interval(0.0, 0.4, Curves.easeOut)
// Animation 1: Interval(0.1, 0.5, Curves.easeOut)
// ...
```
### Visualize Animation State
```dart
class DebugStaggeredWidget extends StatelessWidget {
const DebugStaggeredWidget({super.key, required this.controller, required this.animations});
final AnimationController controller;
final List<Animation<double>> animations;
@override
Widget build(BuildContext context) {
return Column(
children: [
for (int i = 0; i < animations.length; i++)
Text('Animation $i: ${animations[i].value.toStringAsFixed(2)}'),
],
);
}
}
```
## Performance Best Practices
### DO
- Use one controller when animations are related
- Calculate total duration correctly
- Use `AnimatedBuilder` for optimal rebuilds
- Profile with Flutter DevTools
- Test on various devices
- Consider reducing animation complexity on low-end devices
### DON'T
- Create too many controllers unnecessarily
- Forget to dispose controllers
- Use complex widget trees inside staggered animations
- Animate too many items simultaneously (jank)
- Use very long durations without good reason
## Common Patterns
### Loading Animation
```dart
// Dot 1: 0.0 - 0.3
// Dot 2: 0.1 - 0.4
// Dot 3: 0.2 - 0.5
// Repeat forever
```
### Success Animation Sequence
```dart
// Checkmark appears: 0.0 - 0.3
// "Success!" text fades in: 0.2 - 0.5
// Confetti falls: 0.3 - 1.0
```
### Onboarding Steps
```dart
// Step 1: 0.0 - 0.25
// Step 2: 0.25 - 0.5
// Step 3: 0.5 - 0.75
// Step 4: 0.75 - 1.0
```
## Accessibility
- Respect `MediaQuery.disableAnimations` setting
- Provide alternative to complex staggered animations
- Ensure content remains accessible during animation
- Test with screen readers
## Advanced Techniques
### Conditional Staggering
```dart
// Show animations based on device performance
final isLowEnd = Platform.isAndroid && deviceInfo.version.sdkInt < 21;
final staggerDelay = isLowEnd
? Duration(milliseconds: 100) // Slower on low-end
: Duration(milliseconds: 50);
```
### Adaptive Staggering
```dart
// Adjust based on screen size
final screenWidth = MediaQuery.of(context).size.width;
final itemsPerRow = screenWidth ~/ 150;
// Calculate stagger based on grid position
final row = index ~/ itemsPerRow;
final col = index % itemsPerRow;
final startDelay = (row * 0.2) + (col * 0.05);
```
### Physics-Influenced Staggering
```dart
// Use spring physics for staggered elements
for (int i = 0; i < itemCount; i++) {
animations[i] = Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(
parent: controller,
curve: Interval(
i * 0.05,
(i * 0.05) + 0.5,
curve: Curves.elasticOut,
),
),
);
}
```