Flutter Animations Basics
Animations make apps feel alive and polished. Flutter has a rich animation system. This topic covers implicit animations (simple, automatic) and explicit animations (full control), so you know which tool to use for each situation.
Two Types of Animations in Flutter
Implicit Animations: ─────────────────────────────────────────────────────── You change a value. Flutter animates the transition. Low code. Great for simple effects. Example: AnimatedContainer, AnimatedOpacity Explicit Animations: ─────────────────────────────────────────────────────── You control start, stop, repeat, and direction. More code. Great for complex or looping effects. Example: AnimationController + Tween
Implicit Animations — AnimatedContainer
AnimatedContainer automatically animates any property that changes — size, color, padding, border radius.
class GrowingBox extends StatefulWidget {
@override
State<GrowingBox> createState() => _GrowingBoxState();
}
class _GrowingBoxState extends State<GrowingBox> {
bool _expanded = false;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
setState(() { _expanded = !_expanded; });
},
child: AnimatedContainer(
duration: Duration(milliseconds: 400),
curve: Curves.easeInOut,
width: _expanded ? 250 : 100,
height: _expanded ? 250 : 100,
color: _expanded ? Colors.indigo : Colors.blue,
child: Center(child: Text(_expanded ? 'Big!' : 'Tap', style: TextStyle(color: Colors.white))),
),
);
}
}
Before tap: After tap:
┌────────┐ ┌──────────────────────┐
│ Tap │ ──► │ │
└────────┘ │ Big! │
(100×100, blue) │ │
└──────────────────────┘
(250×250, indigo)
AnimatedOpacity — Fade In / Fade Out
class FadeText extends StatefulWidget {
@override
State<FadeText> createState() => _FadeTextState();
}
class _FadeTextState extends State<FadeText> {
bool _visible = true;
@override
Widget build(BuildContext context) {
return Column(
children: [
AnimatedOpacity(
opacity: _visible ? 1.0 : 0.0,
duration: Duration(milliseconds: 500),
child: Text('Hello Flutter!', style: TextStyle(fontSize: 24)),
),
ElevatedButton(
onPressed: () => setState(() { _visible = !_visible; }),
child: Text(_visible ? 'Hide' : 'Show'),
),
],
);
}
}
AnimatedSwitcher — Cross-Fade Between Widgets
AnimatedSwitcher(
duration: Duration(milliseconds: 300),
child: _isLoading
? CircularProgressIndicator(key: ValueKey('loading'))
: Text('Done!', key: ValueKey('done')),
)
Common Implicit Animation Widgets
| Widget | Animates |
|---|---|
| AnimatedContainer | Size, color, padding, border radius |
| AnimatedOpacity | Transparency (fade in/out) |
| AnimatedSwitcher | Transition between two widgets |
| AnimatedPositioned | Position inside a Stack |
| AnimatedAlign | Alignment within a parent |
| TweenAnimationBuilder | Any value between two endpoints |
Explicit Animations — AnimationController
When you need full control — looping, reversing, pausing, or syncing multiple animations — use an AnimationController.
class PulsingDot extends StatefulWidget {
@override
State<PulsingDot> createState() => _PulsingDotState();
}
class _PulsingDotState extends State<PulsingDot>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _scaleAnim;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: Duration(milliseconds: 800),
)..repeat(reverse: true); // Loops back and forth
_scaleAnim = Tween<double>(begin: 0.8, end: 1.4).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
);
}
@override
void dispose() {
_controller.dispose(); // Always dispose!
super.dispose();
}
@override
Widget build(BuildContext context) {
return ScaleTransition(
scale: _scaleAnim,
child: Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: Colors.blue,
shape: BoxShape.circle,
),
),
);
}
}
Pulsing animation cycle:
─────────────────────────────────────────
Scale: 0.8 → 1.4 → 0.8 → 1.4 → ...
(grows and shrinks continuously)
Animation Curves
Curves control the speed profile of an animation — whether it starts fast and slows, or bounces at the end.
Curves.linear → constant speed ────────────► Curves.easeIn → starts slow, ends fast ──────► Curves.easeOut → starts fast, slows down ──────► Curves.easeInOut → slow → fast → slow ──────────► Curves.bounceOut → bouncy landing ──────────↓↑↓► Curves.elasticOut → overshoots and snaps back ────►
Hero Animation — Shared Element Transition
A Hero animation moves a widget from one screen to another with a smooth shared transition — like a product image expanding from a list card into a detail page.
// Screen 1: Product List
Hero(
tag: 'product_image_42', // Unique tag
child: Image.network('https://example.com/shoe.jpg', width: 100),
)
// Screen 2: Product Detail
Hero(
tag: 'product_image_42', // Same tag — Flutter animates between them
child: Image.network('https://example.com/shoe.jpg', width: double.infinity),
)
When to Use Which Animation
| Need | Use |
|---|---|
| Animate a value change on tap | AnimatedContainer, AnimatedOpacity |
| Swap between two widgets smoothly | AnimatedSwitcher |
| Loop, reverse, or pause an animation | AnimationController + Tween |
| Shared element between two screens | Hero animation |
| Complex custom paths or paints | CustomPainter + AnimationController |
