Flutter Lesson 54 of 83 4 min read
Implicit Animations in Flutter: AnimatedContainer and Friends
Add animation to Flutter with no controllers: AnimatedContainer, AnimatedOpacity, AnimatedSwitcher, TweenAnimationBuilder and curves.
On this page
An implicit animation is the easiest kind. You change a value, and the widget animates from the old value to the new one by itself. There is no controller to manage.
AnimatedContainer #
It is a Container that animates any change to its properties.
import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(home: Scaffold(body: Center(child: MorphBox()))));
class MorphBox extends StatefulWidget {
const MorphBox({super.key});
@override
State<MorphBox> createState() => _MorphBoxState();
}
class _MorphBoxState extends State<MorphBox> {
bool _big = false;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => setState(() => _big = !_big),
child: AnimatedContainer(
duration: const Duration(milliseconds: 400),
curve: Curves.easeInOut,
width: _big ? 240 : 120,
height: _big ? 240 : 120,
decoration: BoxDecoration(
color: _big ? Colors.deepPurple : Colors.teal,
borderRadius: BorderRadius.circular(_big ? 48 : 12),
),
alignment: Alignment.center,
child: const Text('Tap me', style: TextStyle(color: Colors.white)),
),
);
}
}
The only animation code is duration and curve. You call setState as usual and Flutter fills in the frames between.
The Animated family #
Most layout and styling widgets have an animated twin.
| Widget | Animates |
|---|---|
AnimatedContainer | Size, colour, padding, border, radius, alignment |
AnimatedOpacity | Fading in and out |
AnimatedPadding | Padding |
AnimatedAlign | Position within a parent |
AnimatedPositioned | Position inside a Stack |
AnimatedScale, AnimatedRotation, AnimatedSlide | Transforms |
AnimatedDefaultTextStyle | Text style |
AnimatedSize | Its own size, when the child’s size changes |
AnimatedCrossFade | A fade between two children |
AnimatedSwitcher | A transition when the child is replaced |
They all work the same way: give a duration, change a property, call setState.
AnimatedOpacity(
opacity: _visible ? 1 : 0,
duration: const Duration(milliseconds: 300),
child: const Text('Now you see me'),
)
Curves #
A curve controls how the speed changes during the animation.
| Curve | Feel |
|---|---|
Curves.linear | Constant speed. Mechanical |
Curves.easeInOut | Slow start and end. A safe default |
Curves.easeOut | Fast start, gentle stop. Good for things entering |
Curves.easeIn | Gentle start, fast end. Good for things leaving |
Curves.easeOutBack | Overshoots slightly, then settles |
Curves.elasticOut | Springy |
Curves.bounceOut | Bounces at the end |
AnimatedSwitcher #
When the child is replaced by a different widget, AnimatedSwitcher fades between them.
AnimatedSwitcher(
duration: const Duration(milliseconds: 250),
transitionBuilder: (child, animation) =>
ScaleTransition(scale: animation, child: child),
child: Text(
'$_count',
key: ValueKey(_count), // a new key tells the switcher the child changed
style: const TextStyle(fontSize: 48),
),
)
Without the key, the switcher sees “a Text replaced by a Text” and does nothing. It is also ideal for swapping a loading spinner for content.
TweenAnimationBuilder #
When no ready-made widget animates what you need, TweenAnimationBuilder animates any value and lets you build from it.
TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: _progress), // animates whenever `end` changes
duration: const Duration(milliseconds: 600),
curve: Curves.easeOut,
builder: (context, value, child) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: 120,
height: 120,
child: CircularProgressIndicator(value: value, strokeWidth: 10),
),
const SizedBox(height: 12),
Text('${(value * 100).round()}%'),
],
);
},
)
Use ColorTween, IntTween or Tween<Offset> for other types. A count-up number is one line: animate an IntTween and show its value.
Expanding and collapsing #
AnimatedSize(
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
alignment: Alignment.topCenter,
child: _expanded
? const Text('A long description that appears smoothly when expanded...')
: const SizedBox(width: double.infinity),
)
How long should an animation be? #
| Kind | Duration |
|---|---|
| Small feedback: a toggle, a colour change | 100 to 200 ms |
| An element entering, leaving or expanding | 200 to 300 ms |
| A full-screen transition | 300 to 400 ms |
Longer than half a second feels slow. Users see these animations hundreds of times, so keep them brief.
Respect reduced motion #
Some people turn animations off in their device settings because motion makes them unwell.
final reduceMotion = MediaQuery.disableAnimationsOf(context);
AnimatedContainer(
duration: reduceMotion ? Duration.zero : const Duration(milliseconds: 300),
// ...
)
When implicit is not enough #
Implicit animations run once, from one value to another, when you change something. For animations that loop, reverse, are driven by a gesture, or need several properties in sequence, use an explicit animation.
Try it yourself #
Build a “like” button: tapping it animates the heart’s colour and scale, and the count beside it changes with an AnimatedSwitcher. Then build an expandable FAQ item using AnimatedSize and an arrow that turns with AnimatedRotation.