
Flutter Animations
- 14.7k installs
- 105 repo stars
- Updated May 1, 2026
- madteacher/mad-agents-skills
Flutter Animations is a skill covering five animation patterns in Flutter with code templates and performance guidance for building smooth, responsive motion.
About
Flutter Animations is a comprehensive guide to motion and transitions in Flutter apps. It covers implicit animations (AnimatedContainer, TweenAnimationBuilder) for simple cases, explicit animations (AnimationController, Tween) for full control, hero transitions for shared elements, staggered effects for sequential motion, and physics-based animations for natural movement. Developers use it to add polish and visual feedback to Flutter UIs.
- Covers 5 animation types: Implicit, Explicit, Hero, Staggered, and Physics-based
- Ready-to-use Dart templates for each animation pattern
- Comprehensive reference docs on curves, spring simulations, and performance trade-offs
Flutter Animations by the numbers
- 14,737 all-time installs (skills.sh)
- +92 installs in the week ending Jul 29, 2026 (Skillselion tracking)
- Ranked #13 of 1,039 Mobile Development skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Jul 29, 2026 (Skillselion catalog sync)
flutter-animations capabilities & compatibility
- Capabilities
- ui animation · motion design
- Use cases
- frontend
npx skills add https://github.com/madteacher/mad-agents-skills --skill flutter-animationsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 14.7k |
|---|---|
| repo stars | ★ 105 |
| Security audit | 3 / 3 scanners passed |
| Last updated | May 1, 2026 |
| Repository | madteacher/mad-agents-skills ↗ |
What it does
Flutter Animations is a comprehensive guide to motion and transitions in Flutter apps. It covers implicit animations (AnimatedContainer, TweenAnimationBuilder) for simple cases, explicit animations (
Who is it for?
Adding motion effects, transitions, hero animations, and physics-based movement to Flutter apps
Skip if: Server-side or backend animation
When should I use this skill?
Implementing animations in Flutter, choosing between animation approaches, or fixing animation performance
What you get
Smooth, performant animations implemented with correct patterns for the specific use case
- Animated widget implementations
- Navigation transition handlers
- Interaction feedback animations
By the numbers
- Covers 5 animation categories: Implicit, Explicit, Hero, Staggered, Physics-based
- Provides 4+ code templates: implicit_animation.dart, explicit_animation.dart, hero_transition.dart, staggered_animation.
Files
Flutter Animations
You are a Flutter motion implementation specialist. Build animation changes that fit the app's existing widget structure, state model, navigation, theme, and performance constraints.
Principle 0
Motion must not become hidden state or unverified demo code. Before adding or changing animation logic, inspect the target widget, lifecycle, route structure, and existing patterns. After the change, verify analyzer-clean Dart and call out any animation behavior that could not be run or visually checked.
Workflow
1. Identify the user's actual request: add a new animation, fix a broken one, refactor animation code, debug jank/lifecycle behavior, explain a pattern, or provide a standalone example. 2. Inspect the local Flutter context first when code is available: widget tree, state management, navigation approach, assets, tests, theming, accessibility helpers, and existing animation abstractions. 3. Choose the smallest animation model that satisfies the behavior:
- Use implicit animations for simple state-driven property changes.
- Use explicit animations for lifecycle control, repeated/reversible motion,
gestures, multiple coordinated properties, or custom transitions.
- Use Hero for shared elements across route transitions.
- Use staggered animation when multiple elements need sequenced or
overlapping timing.
- Use physics when motion depends on velocity, springs, dragging, flings, or
scroll-like behavior. 4. Implement in the app's style. Preserve existing public APIs unless the user asked for a refactor. Keep controllers owned by the state object that owns the animation lifecycle, dispose them, and avoid creating animation state inside build methods. 5. Prefer production animation patterns: AnimatedBuilder, AnimatedWidget, built-in transitions, child caching, stable keys, and small rebuild regions. Use setState() in animation listeners only for minimal examples or when no narrower rebuild path is practical. 6. Respect accessibility and user settings. Check MediaQuery.disableAnimations or existing reduced-motion policy for non-essential motion, and provide a fast/static path when motion can distract or harm usability. 7. Validate with the strongest available local checks. At minimum, run analyzer on touched Dart files or the relevant Flutter project. Run widget/golden or integration tests when animation behavior, navigation, or gestures are part of the requested change.
Decision Guide
| Need | Default approach |
|---|---|
| One property or a small set of state-driven visual changes | AnimatedContainer, AnimatedOpacity, AnimatedAlign, AnimatedPadding, AnimatedSwitcher, or TweenAnimationBuilder |
| Full lifecycle control, repeat/reverse/status, or gesture-driven values | AnimationController with Tween, CurvedAnimation, AnimatedBuilder, or AnimatedWidget |
| Shared visual element between routes | Hero with stable unique tags and compatible source/destination widget trees |
| List/menu/card reveal with offset timings | One controller with Intervals, or per-item animation only when lifecycle truly differs |
| Spring/fling/drag or platform scroll feel | fling, animateWith, SpringSimulation, gesture velocity, or platform-specific ScrollPhysics |
| Motion feels wrong but code works | Tune duration, curve, interval, easing, or reduced-motion behavior before adding complexity |
Resource Routing
Read only the resources needed for the current task:
| Task | Read/use | Purpose |
|---|---|---|
| Simple state-driven animation or AnimatedSwitcher/TweenAnimationBuilder choice | references/implicit.md; optionally assets/templates/implicit_animation.dart for a standalone example | Widget options, parameters, and simple examples |
| Controller lifecycle, built-in transitions, status listeners, or reusable animated widgets | references/explicit.md; optionally assets/templates/explicit_animation.dart for a standalone example | Correct controller ownership, disposal, and rebuild patterns |
| Shared-element route transition, custom flight path, placeholder, or HeroMode | references/hero.md; optionally assets/templates/hero_transition.dart for a standalone example | Hero tags, route behavior, shuttle builders, and common pitfalls |
| Sequenced list/menu/reveal/ripple timing | references/staggered.md; optionally assets/templates/staggered_animation.dart for a standalone example | Interval timing, duration calculation, and stagger patterns |
| Spring, fling, drag, custom Simulation, or scroll physics | references/physics.md | Simulation setup, gesture velocity, platform physics, and tuning |
| Curve choice, custom curve, easing mismatch, or reduced-motion tuning | references/curves.md | Curve selection, custom curves, and accessibility notes |
Templates are complete demo files, not drop-in production modules. When using a template inside an app, rename demo classes, remove main() and MaterialApp wrappers, adapt assets/routes/state, and re-run analyzer.
Constraints
- Do not invent missing routes, asset paths, theme tokens, gesture behavior, or
state-management APIs. Inspect them or ask the user if they are absent.
- Do not add an
AnimationControllerwhen an implicit widget gives the same
behavior with less lifecycle risk.
- Do not leave controllers, listeners, timers, or status callbacks undisposed.
- Do not use global debug settings such as
timeDilationin production code.
Mention them only as a local debugging aid.
- Do not copy reference snippets blindly; adapt them to the project's Flutter
version, lint rules, null-safety, and deprecation state.
- Do not make accessibility optional for user-facing motion. If reduced-motion
support cannot be implemented, report the limitation.
Validation
- Run
dart formator the project's formatter on edited Dart files when edits
are made.
- Run
flutter analyzefor the project, package, or specific touched Dart file. - Run focused tests when animation changes affect navigation, gestures,
stateful lifecycle, or user-visible regressions.
- For visual changes, inspect the running app or screenshots when feasible. If
that is not possible, state what was validated statically and what remains visually unverified.
- When updating templates, verify each template as
lib/main.dartin a minimal
Flutter project with flutter analyze lib/main.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'),
),
],
),
),
);
}
}
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),
),
),
),
),
),
),
),
);
}
}
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'),
),
],
),
),
);
}
}
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])),
);
},
),
);
}
}
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
Curves.linearNo easing - straight line from 0 to 1. Use when you want constant speed.
Ease (Sigmoid)
Curves.ease
Curves.easeIn
Curves.easeOut
Curves.easeInOutCurves.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)
Curves.easeInCubic
Curves.easeOutCubic
Curves.easeInOutCubicStronger easing than basic ease. More dramatic slow/fast zones.
Elastic (Bouncy)
Curves.elasticIn
Curves.elasticOut
Curves.elasticInOutCurves.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
Curves.bounceIn
Curves.bounceOut
Curves.bounceInOutCurves.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)
Curves.backIn
Curves.backOut
Curves.backInOutCurves.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)
Curves.decelerateFast start, very slow end. Similar to easeOut but more dramatic.
Fast Out Slow In
Curves.fastOutSlowInVery fast start, very slow middle, very fast end. Dramatic.
Custom Curves in Flutter API
Flutter includes specialized curves:
// Material design curves
Curves.fastLinearToSlowEaseIn
Curves.slowMiddle
// Specific curves for certain transitions
Curves.easeUsing Curves
With Implicit Animation
AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
width: _expanded ? 200 : 100,
child: const FlutterLogo(),
)With Explicit Animation (CurvedAnimation)
animation = CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
);With Interval
animation = CurvedAnimation(
parent: _controller,
curve: const Interval(
0.0,
0.5,
curve: Curves.easeIn,
),
);With Curves.elasticOut
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
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:
AnimatedContainer(
curve: CustomCurve(),
duration: const Duration(milliseconds: 300),
width: _expanded ? 200 : 100,
child: const FlutterLogo(),
)Shake Curve
class ShakeCurve extends Curve {
@override
double transform(double t) {
return sin(t * pi * 2);
}
}Smooth Step Curve
class SmoothStep extends Curve {
final double steps;
const SmoothStep({this.steps = 5});
@override
double transform(double t) {
return (t * steps).floor() / steps;
}
}Exponential Curve
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:
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
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:
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
// 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
// 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
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
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
Curve getAdaptiveCurve(BuildContext context) {
if (MediaQuery.of(context).disableAnimations) {
return Curves.linear; // No easing when animations disabled
}
return Curves.easeInOut;
}Reduced Motion
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
_animation = CurvedAnimation(
parent: _controller,
curve: Curves.linear, // Constant speed
);Success Checkmark
_animation = CurvedAnimation(
parent: _controller,
curve: Curves.elasticOut, // Bouncy celebration
);Page Transition
_enterAnimation = CurvedAnimation(
parent: _controller,
curve: Curves.easeIn, // Accelerate in
);
_exitAnimation = CurvedAnimation(
parent: _controller,
curve: Curves.easeOut, // Decelerate out
);Modal Popup
_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)
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.
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:
// 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:
_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.
// 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 interpolationRectTween- Rectangle interpolationIntTween- Integer interpolationSizeTween- Size interpolationOffsetTween- Offset interpolationBorderRadiusTween- Border radius interpolation
CurvedAnimation
Applies a curve to transform animation values.
animation = CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
reverseCurve: Curves.easeIn, // Optional: different curve for reverse
);With Interval:
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.
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.
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.
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.
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
FadeTransition(
opacity: _animation,
child: const FlutterLogo(),
)ScaleTransition
ScaleTransition(
scale: _animation,
child: const FlutterLogo(),
)SlideTransition
SlideTransition(
position: Tween<Offset>(
begin: Offset(0, 1),
end: Offset.zero,
).animate(_animation),
child: const FlutterLogo(),
)SizeTransition
SizeTransition(
sizeFactor: _animation,
axis: Axis.vertical,
child: const FlutterLogo(),
)RotationTransition
RotationTransition(
turns: _animation,
child: const FlutterLogo(),
)DecoratedBoxTransition
DecoratedBoxTransition(
decoration: DecorationTween(
begin: BoxDecoration(color: Colors.red),
end: BoxDecoration(color: Colors.blue),
).animate(_animation),
child: const FlutterLogo(),
)PositionedTransition
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
_animation.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_controller.reverse();
} else if (status == AnimationStatus.dismissed) {
_controller.forward();
}
});One-way Animation
_animation.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_controller.stop(); // Or navigate, show dialog, etc.
}
});Reset and Repeat
_animation.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_controller.reset();
_controller.forward();
}
});Performance Best Practices
DO
- Always dispose AnimationController
- Use
AnimatedWidgetorAnimatedBuilderinstead ofsetState()in listeners - Minimize widget rebuilds during animation
- Use static Tweens when possible (avoid recreating)
- Profile with Flutter DevTools
- Use
timeDilationfor 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
RepaintBoundary(
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return ExpensiveWidget();
},
),
)Debugging
Slow Animations
void main() {
timeDilation = 10.0; // 10x slower
runApp(MyApp());
}Print Animation Values
_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
_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
_animation = CurvedAnimation(
parent: _controller,
curve: Curves.elasticOut,
);Shake Effect
class ShakeCurve extends Curve {
@override
double transform(double t) {
return sin(t * pi * 2);
}
}
_animation = CurvedAnimation(
parent: _controller,
curve: ShakeCurve(),
);Parallax Scrolling
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
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):
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):
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:
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:
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:
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:
// 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:
// 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)
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):
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:
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
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
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
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:
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
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
MaterialApp(
// Set global hero animation duration
theme: ThemeData(
pageTransitionsTheme: PageTransitionsTheme(
builders: {
TargetPlatform.android: ZoomPageTransitionsBuilder(),
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
},
),
),
)Custom PageTransitionBuilder
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
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
HeroMode(
enabled: !_disableAnimations,
child: Hero(tag: 'image', child: Image.asset('image.png')),
)Debugging Hero Animations
Slow Animation
void main() {
timeDilation = 10.0; // 10x slower
runApp(MyApp());
}Visualize Hero Bounds
void main() {
debugPaintSizeEnabled = true;
runApp(MyApp());
}Print Hero Tags
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
Materialwith transparent color for "pop" effect - Use
timeDilationto debug transitions - Consider
createRectTweenfor custom flight paths - Use
flightShuttleBuilderfor custom flight appearance
DON'T
- Use duplicate tags (conflicts!)
- Change hero structure significantly between routes (jarring transition)
- Forget
Materialwrapper (no splash effect) - Use very large images in heroes (performance)
- Ignore aspect ratio during transition (distortion)
Common Patterns
Grid to Fullscreen Image
Grid item:
PhotoHero(
photo: photo.url,
width: 150, // Smaller in grid
onTap: () => Navigator.push(..., detailScreen),
)Fullscreen:
PhotoHero(
photo: photo.url,
width: MediaQuery.of(context).size.width, // Full width
onTap: () => Navigator.pop(),
)List Header to Page Header
List:
Hero(
tag: 'header',
child: SizedBox(
height: 200,
child: Image.asset('header.jpg'),
),
)Page:
Hero(
tag: 'header',
child: SizedBox(
height: 300, // Larger on detail page
child: Image.asset('header.jpg'),
),
)Shared Element with Content Update
// 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
RepaintBoundaryaround hero children if needed - Test on low-end devices
- Profile with Flutter DevTools Performance overlay
- Avoid complex widget trees inside hero
Accessibility
- Respect
MediaQuery.disableAnimationssetting - 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
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
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
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
]),
),
],
);
}
}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.
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- Sizecolor- Background colordecoration- BoxDecoration (includes border, shadow, image, etc.)padding- Paddingmargin- Margin (via decoration)transform- Matrix4 transformation
AnimatedOpacity
Fades a widget in and out.
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.
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.
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.
AnimatedAlign(
alignment: _aligned ? Alignment.topLeft : Alignment.bottomRight,
duration: const Duration(milliseconds: 400),
curve: Curves.easeInOut,
child: const Text('Align me'),
)AnimatedContainer (Multiple Properties)
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.
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.
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:
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.
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.
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.
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:
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
const Duration(milliseconds: 300) // 0.3 seconds
const Duration(seconds: 1) // 1 second
const Duration(days: 1) // 1 dayCurves
Common curves from Curves class:
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:
class ShakeCurve extends Curve {
@override
double transform(double t) => sin(t * pi * 2);
}
// Usage
AnimatedContainer(
curve: ShakeCurve(),
// ...
)onEnd Callback
Trigger action when animation completes:
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
onEndcallback for post-animation actions - Test animations on various devices
- Consider
MediaQuery.disableAnimationsfor 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
RepaintBoundaryaround animated children to isolate repaints - Test performance with Flutter DevTools Performance overlay
- Consider using
AnimatedBuilderfor complex widgets to minimize rebuilds
Debugging
Slow Animations
// Slow all animations by 10x during development
timeDilation = 10.0;Show Repaint Rainbows
void main() {
debugPaintSizeEnabled = true;
runApp(MyApp());
}Performance Overlay
void main() {
runApp(MyApp());
}
// In debug mode, press 'p' to toggle performance overlayComparison: 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
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
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
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
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
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:
SpringDescription(
mass: 0.5,
stiffness: 300,
damping: 8,
)Snappy:
SpringDescription(
mass: 1.0,
stiffness: 400,
damping: 18,
)Gentle:
SpringDescription(
mass: 2.0,
stiffness: 150,
damping: 20,
)Gravity Simulation
Falling Animation
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
CustomScrollView(
physics: const BouncingScrollPhysics(),
slivers: [
SliverList(
delegate: SliverChildListDelegate([
// Items
]),
),
],
)Clamping Scroll
CustomScrollView(
physics: const ClampingScrollPhysics(),
slivers: [
SliverList(
delegate: SliverChildListDelegate([
// Items
]),
),
],
)Fixed Scroll
CustomScrollView(
physics: const NeverScrollableScrollPhysics(),
slivers: [
SliverList(
delegate: SliverChildListDelegate([
// Items
]),
),
],
)Platform-Adaptive Scroll
CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
SliverList(
delegate: SliverChildListDelegate([
// Items
]),
),
],
)Custom Simulation
Creating Custom Simulation
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:
_controller.animateWith(
CustomSimulation(target: 1.0),
);Decay Simulation
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
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
_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
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
void main() {
timeDilation = 5.0; // Slow down physics
runApp(MyApp());
}Print Simulation Values
_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
_controller.animateWith(
SpringSimulation(
spring: const SpringDescription(
mass: 0.5,
stiffness: 500,
damping: 10,
),
start: 1.0,
end: 0.9,
velocity: 0,
),
);Swipeable Card
_controller.fling(
velocity: details.velocity.pixelsPerSecond.dx / 1000,
);Pull to Refresh
RefreshIndicator(
onRefresh: () async {
// Refresh data
},
child: ListView(...),
)Accessibility
- Respect
MediaQuery.disableAnimationssetting - Provide non-physics alternatives
- Ensure physics animations don't cause motion sickness
- Test with reduced motion settings
Platform-Specific Physics
Adaptive Scroll Physics
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:
ListView(
physics: const AdaptiveScrollPhysics(),
children: [...],
)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
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
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
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:
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
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
// 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
// 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
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
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
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
// 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
// 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
const itemDelay = Duration(milliseconds: 50);
const itemAnimationTime = Duration(milliseconds: 250);
const itemCount = 10;
final totalDuration = itemAnimationTime +
(itemDelay * (itemCount - 1));
// 250ms + (50ms * 9) = 700msUsing in Controller
_controller = AnimationController(
duration: totalDuration,
vsync: this,
);Dynamic Duration
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
_controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_controller.reset();
_controller.forward();
}
});Ping-Pong (Forward and Reverse)
_controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_controller.reverse();
} else if (status == AnimationStatus.dismissed) {
_controller.forward();
}
});Debugging Staggered Animations
Slow Animation
void main() {
timeDilation = 10.0; // 10x slower
runApp(MyApp());
}Print Interval Ranges
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
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
AnimatedBuilderfor 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
// Dot 1: 0.0 - 0.3
// Dot 2: 0.1 - 0.4
// Dot 3: 0.2 - 0.5
// Repeat foreverSuccess Animation Sequence
// Checkmark appears: 0.0 - 0.3
// "Success!" text fades in: 0.2 - 0.5
// Confetti falls: 0.3 - 1.0Onboarding Steps
// Step 1: 0.0 - 0.25
// Step 2: 0.25 - 0.5
// Step 3: 0.5 - 0.75
// Step 4: 0.75 - 1.0Accessibility
- Respect
MediaQuery.disableAnimationssetting - Provide alternative to complex staggered animations
- Ensure content remains accessible during animation
- Test with screen readers
Advanced Techniques
Conditional Staggering
// 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
// 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
// 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,
),
),
);
}Related skills
How it compares
Choose flutter-animations over generic Flutter snippets when motion must integrate with navigation, theme, gestures, and accessibility in one widget tree.
FAQ
When should I use Implicit vs Explicit animations?
Use Implicit (AnimatedContainer, AnimatedOpacity) for simple state-driven transitions. Use Explicit (AnimationController) when you need full control over timing, multiple simultaneous animations, or monitoring animation state.
What is a hero animation?
Shared element transition that animates a widget from one screen to another. Standard hero transitions work on most navigation; radial hero animations fan out from a center point.
Is Flutter Animations safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.