Animations in Flutter done right [Flutter 2 & 3]
Animations are really important! It gives the sense of motion that drives the attention of users.
Since Flutter draws everything pixel-by-pixel, it offers a lot of ways by which a really-really rich experience can be made.
The 5 Ways
To me, there are five ways by which animations can be achieved in Flutter. They are:
The first two fall in the category of implicit animations. The third & the last one are explicit animations.
tip
In a common & simple case, the first two will be enough for your purpose !!!
Animation Type | When To Use | Possible Cases |
---|---|---|
AnimatedXYZ Widget s e.g. AnimatedContainer , AnimatedOpacity , AnimatedScale , AnimatedRotation etc. | 1. Your animation is simple & operates only between discrete values e.g. from one value to another & then another. 2. You don't want to listen the current animation position / percentage. 3. There is no need of pausing / playing the animation. | 1. Changing height of a Container from 32.0 to 156.0 with given duration & curve.2. Increasing a scale of something upon tap/hover. 3. Changing color to another color. |
TweenAnimationBuilder | 1. You want to start your animation when your Widget is mounted.2. There is no requirement of listening to current animation position / percentage. 3. There is no need of pausing / playing the animation. 4. Existing AnimatedXYZ in Flutter aren't enough for your purpose.4. Works inside StatelessWidget. | 1. Something that needs to be animated as soon as drawn to screen. 2. Animating Color of something, see ColorTween . |
AnimationController + XYZTransition Widget s e.g. SlideTransition , RotationTransition , ScaleTransition etc. | 1. You want to have strong control over the animation 2. You need capablity of playing / pausing the animation 3.You need access to current animation progress / percentage e.g. controlling another animation. 4. More complex stuff / configuration in your animation. 5. You possibly wanna repeat your animation upon completion (not mandatory). | Something very unique or some animated UI component with a lot of stuff going on in it. |
AnimationController + AnimatedBuilder / AnimatedWidget | 1. You want to animate such UI property which is not already available as XYZTransition in Flutter.2. All same requirements as above one. | Likely, noone of the above options fit your needs & you wish to animate any arbitrary Flutter property of a Widget which isn't present as AnimatedXYZ or XYZTransition in Flutter.THOUGH, most options like scale, rotation, color, slide are already present. You should decide if you really want to use this. |
Hero widget | You need to animate an element between two screens as user navigates to the second screen from first one. This is very common & brings a nice experience to the navigation. | Most simple of all, just same tag to both Hero widgets wrapping the element you want to animate, one on the first screen & another on the screen you're navigating to. |
Few Other Things
Two primary things that you'd generally define for any kind of animation are:
- The time that it takes to complete the animation. a.k.a
duration
. - The curve (NOT the path, but how much percent of animation should be completed with respect to time at a given moment) that the animation should follow. a.k.a
curve
. See THIS.
Other than this, we will be definitely providing the width / scale / rotation / color etc. values between which the animation should take place.
Show Me Code
info
All the code snippets present here are complete & can be run just by copying & pasting a snippet entirely.
AnimatedContainer & friends
Here, you just need to use any of AnimatedContainer
, AnimatedOpacity
, AnimatedScale
, AnimatedRotation
etc. Find more of these with the help of intellisense in your code editor or on flutter.dev.
tip
If you know how to use setState
, you already know how this works.
Example below shows AnimatedRotation
, AnimatedScale
, AnimatedContainer
& AnimatedSlide
.
import 'dart:math';
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return const MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key}) : super(key: key);
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
double turns = 0.0;
double scale = 1.0;
Color color = Colors.blue;
Offset offset = Offset.zero;
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('animations')),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
alignment: Alignment.center,
height: 64.0,
padding: const EdgeInsets.all(12.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
onPressed: () {
setState(() {
turns += 1;
});
},
child: const Text('AnimatedRotation'),
),
ElevatedButton(
onPressed: () {
setState(() {
scale = scale == 1.0 ? 1.5 : 1.0;
});
},
child: const Text('AnimatedScale'),
),
ElevatedButton(
onPressed: () {
setState(() {
color = Color.fromARGB(
255,
Random().nextInt(256),
Random().nextInt(256),
Random().nextInt(256),
);
});
},
child: const Text('AnimatedContainer'),
),
ElevatedButton(
onPressed: () {
setState(() {
offset = offset == Offset.zero
? const Offset(1.0, 1.0)
: Offset.zero;
});
},
child: const Text('AnimatedSlide'),
),
],
),
),
Expanded(
child: Center(
child: AnimatedSlide(
offset: offset,
duration: const Duration(milliseconds: 800),
child: AnimatedScale(
scale: scale,
duration: const Duration(milliseconds: 800),
child: AnimatedRotation(
turns: turns,
duration: const Duration(milliseconds: 800),
// Animate any properties on the AnimatedContainer.
child: AnimatedContainer(
duration: const Duration(milliseconds: 800),
curve: Curves.easeInOut,
height: 96.0,
width: 96.0,
color: color,
),
),
),
),
),
),
],
),
);
}
}
TweenAnimationBuilder
Takes a Tween
and a Widget
and animates the Widget
according to the Tween
you provide. Tween
actually defines a range of values defined by a begin
and an end
. These values may be double
or even Color
.
TweenAnimationBuilder
automatically animates on mount & whenever the Tween
changes due to setState
etc.
tip
Notice how animation is automatically played on mount & changing the Tween
causes the animation to continue from that point to new value.
Notice the large number of methods & properties that are available to highly customize the animation.
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return const MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key}) : super(key: key);
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
double end = 1.0;
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('animations')),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
alignment: Alignment.center,
height: 64.0,
padding: const EdgeInsets.all(12.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
onPressed: () {
setState(() {
end = end == 0.0 ? 1.0 : 0.0;
});
},
child: Text('Tween<double>.end = ${end == 0.0 ? 1.0 : 0.0}'),
),
],
),
),
Expanded(
child: Center(
child: TweenAnimationBuilder<double>(
tween: Tween<double>(begin: 0.0, end: end),
duration: const Duration(milliseconds: 400),
builder: (context, value, child) => Transform.scale(
scale: value,
child: Container(
height: 256.0,
width: 256.0,
alignment: Alignment.center,
color: Colors.blue,
child: child,
),
),
// Widget passed as child will not be redrawn when the animation is updated.
child: const Text(
'This text is not being redrawn.',
style: TextStyle(color: Colors.white),
),
),
),
),
],
),
);
}
}
AnimationController & XYZTransition Widgets
Existence of an AnimationController
itself gives a lot of configurable options & other features.
warning
For using AnimationController
in your Widget
, you need to use TickerProviderStateMixin
or SingleTickerProviderStateMixin
(if there's only one AnimationController
animation).
e.g.
From:
class _MyHomePageState extends State<MyHomePage> {
To:
class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin {
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return const MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key}) : super(key: key);
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage>
with SingleTickerProviderStateMixin {
late AnimationController animationController;
void initState() {
super.initState();
animationController = AnimationController(
vsync: this,
lowerBound: 0.0,
upperBound: 2.2,
duration: const Duration(seconds: 1),
reverseDuration: const Duration(seconds: 1),
);
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('animations')),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
alignment: Alignment.center,
height: 64.0,
padding: const EdgeInsets.all(12.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
onPressed: () {
animationController.forward();
},
child: const Text('Forward'),
),
ElevatedButton(
onPressed: () {
animationController.reverse();
},
child: const Text('Reverse'),
),
ElevatedButton(
onPressed: () {
animationController.stop();
},
child: const Text('Stop'),
),
ElevatedButton(
onPressed: () {
animationController.reset();
},
child: const Text('Reset'),
),
],
),
),
Expanded(
child: Center(
child: RotationTransition(
turns: animationController,
child: Container(
height: 96.0,
width: 96.0,
color: Colors.red,
),
),
),
),
// Just for demo.
AnimationControllerStateViewer(
animationController: animationController,
),
],
),
);
}
}
/// Just to show the available properties in the [AnimationController] class.
class AnimationControllerStateViewer extends StatefulWidget {
final AnimationController animationController;
const AnimationControllerStateViewer({
Key? key,
required this.animationController,
}) : super(key: key);
AnimationControllerStateViewerState createState() =>
AnimationControllerStateViewerState();
}
class AnimationControllerStateViewerState
extends State<AnimationControllerStateViewer> {
void initState() {
super.initState();
// Redraw this widget to show updated properties.
widget.animationController.addListener(() {
setState(() {});
});
}
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(12.0),
width: double.infinity,
height: 156.0,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'animationController.isAnimating: ${widget.animationController.isAnimating}',
),
Text(
'animationController.isCompleted: ${widget.animationController.isCompleted}',
),
Text(
'animationController.isDismissed: ${widget.animationController.isDismissed}',
),
Text(
'animationController.status: ${widget.animationController.status}',
),
Text(
'animationController.value: ${widget.animationController.value}',
),
Text(
'animationController.velocity: ${widget.animationController.velocity}',
),
],
),
);
}
}
tip
If you noticed, there's actually no curve
argument in AnimationController
. You need to use CurvedAnimation
together.
late AnimationController animationController;
late Animation<double> animation;
void initState() {
super.initState();
animationController = AnimationController(
vsync: this,
duration: const Duration(seconds: 1),
reverseDuration: const Duration(seconds: 1),
);
animation = Tween<double>(begin: 0.0, end: 2.0).animate(
CurvedAnimation(
curve: Curves.easeInOut,
reverseCurve: Curves.easeInCirc,
parent: animationController,
),
);
}
In above example, just use CurvedAnimation
with Tween<T>.animate
and then pass animation
as the turns
in RotationTransition
instead of animationController
itself.
AnimationController & AnimatedBuilder / AnimatedWidget
This should be only used when you wish to animate a Flutter Widget
property which is not already present as XYZTransition
in the Flutter framework.
Here, I'll be animating fontSize
of a Text
.
Though, it would've been better to use ScaleTransition
instead!
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return const MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key}) : super(key: key);
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage>
with SingleTickerProviderStateMixin {
late AnimationController animationController;
void initState() {
super.initState();
animationController = AnimationController(
vsync: this,
lowerBound: 16.0,
upperBound: 24.0,
duration: const Duration(seconds: 1),
reverseDuration: const Duration(seconds: 1),
);
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('animations')),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
alignment: Alignment.center,
height: 64.0,
padding: const EdgeInsets.all(12.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
onPressed: () {
animationController.forward();
},
child: const Text('Forward'),
),
ElevatedButton(
onPressed: () {
animationController.reverse();
},
child: const Text('Reverse'),
),
ElevatedButton(
onPressed: () {
animationController.stop();
},
child: const Text('Stop'),
),
ElevatedButton(
onPressed: () {
animationController.reset();
},
child: const Text('Reset'),
),
],
),
),
Expanded(
child: Center(
child: AnimatedBuilder(
animation: animationController,
builder: (context, child) => Text(
'Well, this is a text.',
style: TextStyle(
fontSize: animationController.value,
fontWeight: FontWeight.w700,
),
),
),
),
),
// Just for demo.
],
),
);
}
}
End Notes
Even though, the examples shown above are quite simple, they show most of the things which are available to animate a Flutter Widget
in your own way.
A lot of animations can be combined together to create a complex animation. A listener can be added to AnimationController
to listen to various events & possibly alter the curve, duration or any other property of animation in the middle of animation itself etc. Endless possibilities are there!
Few of the properties were shown in the AnimationController & XYZTransition Widgets video.
So, these are my tips on adding animations in a Flutter app.