Building Animations in Flutter — Simplified

Prashant Singh
5 min readJul 21, 2024

--

Photo by Annie Spratt on Unsplash

I will keep it really simple. Broadly speaking there are two types of animations one can make in flutter.

1. Implicit Animations
2. Explicit Animations

It’s really easy. Let me explain.

Implicit Animations

These are the animations that flutter provides to be used directly, examples of this include AnimatedOpacity, AnimatedScale, AnimatedRotation and so on. These are generally called ‘AnimatedFoo’ widgets, ‘Foo’ can represent any property that is being animated. Below is a code snippet with a container that scales when tapped on it using the AnimatedScale widget.

class ContainerThatScales extends StatefulWidget {
const ContainerThatScales({super.key});

@override
State<ContainerThatScales> createState() => _ContainerThatScalesState();
}

class _ContainerThatScalesState extends State<ContainerThatScales> {
double containerScale = 1.0;

@override
Widget build(BuildContext context) {
return Center(
child: GestureDetector(
onTap: () {
setState(() {
if (containerScale > 1.0) {
containerScale = 1.0;
} else {
containerScale = 2.0;
}
});
},
child: AnimatedScale(
duration: const Duration(milliseconds: 1000),
scale: containerScale,
child: Container(
width: 100,
height: 100,
color: Colors.red,
),
),
),
);
}
}

Here’s the output:

Container Scale Animation Using Implicit Animations

As we can see in the above code, the AnimatedFoo widgets needs the property that needs to be animated which is most of the times a state variable and the duration of the transition. Whenever the state variable changes and the widget is rebuilt the AnimatedFoo widget animates the property from the last state variable value to the new value in the given duration.

Explicit Animations

Implicit Animations are really easy to use and can server a lot of use cases, but in complex projects with lots of animations we might need better control on the triggering of animations, the timings, synchronising multiple animations together and lots of other stuff. Anything that you feel can’t be done with Implicit animation widget, should make you think about Explicit Animations.

Explicit animation uses “AnimationController” and then a bunch of Animation “Tween” to build an animation. Animation controller gives you control to start and animate from any certain point, reverse it, have status listeners to have callbacks at various events during the animation, etc. Below is the code to do the same thing which we did above but using AnimationController.

class ContainerThatScales extends StatefulWidget {
const ContainerThatScales({super.key});

@override
State<ContainerThatScales> createState() => _ContainerThatScalesState();
}

class _ContainerThatScalesState extends State<ContainerThatScales>
with SingleTickerProviderStateMixin {
late AnimationController containerAnimController;
late Animation<double> containerScaleAnimation;

@override
void initState() {
super.initState();

containerAnimController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1000),
);
containerScaleAnimation =
Tween(begin: 1.0, end: 2.0).animate(containerAnimController);
}

@override
void dispose() {
containerAnimController.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return Center(
child: GestureDetector(
onTap: () {
if (containerScaleAnimation.isDismissed) {
containerAnimController.forward();
} else if (containerAnimController.isCompleted) {
containerAnimController.reverse();
}
},
child: AnimatedBuilder(
animation: containerScaleAnimation,
builder: (_, Widget? child) {
return Transform.scale(
scale: containerScaleAnimation.value,
child: child,
);
},
child: Container(
width: 100,
height: 100,
color: Colors.red,
),
),
),
);
}
}

There is definitely more boilerplate code when using AnimationController, but it lets you control the animations better.

We can even make staggered animations using AnimationController, like for example, that total animation duration is 1000 milliseconds and in 1000 milliseconds we have to scale it up to 2 times, but in the first 500 milliseconds we also have to rotate the container. That can be done using a single AnimationController and two Animation Tweens, this is what is called staggered animation. Here’s an example:

class ContainerThatScales extends StatefulWidget {
const ContainerThatScales({super.key});

@override
State<ContainerThatScales> createState() => _ContainerThatScalesState();
}

class _ContainerThatScalesState extends State<ContainerThatScales>
with SingleTickerProviderStateMixin {
late AnimationController containerAnimController;
late Animation<double> containerScaleAnimation;
late Animation<double> containerRotateAnimation;

@override
void initState() {
super.initState();

containerAnimController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1000),
);

containerScaleAnimation = Tween(begin: 1.0, end: 2.0).animate(
containerAnimController,
);

containerRotateAnimation = Tween(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: containerAnimController,
curve: const Interval(0, 0.5),
),
);
}

@override
void dispose() {
containerAnimController.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return Center(
child: GestureDetector(
onTap: () {
if (containerScaleAnimation.isDismissed) {
containerAnimController.forward();
} else if (containerAnimController.isCompleted) {
containerAnimController.reverse();
}
},
child: AnimatedBuilder(
animation: containerAnimController,
builder: (_, Widget? child) {
return Transform.rotate(
angle: containerRotateAnimation.value,
child: Transform.scale(
scale: containerScaleAnimation.value,
child: child,
),
);
},
child: Container(
width: 100,
height: 100,
color: Colors.red,
),
),
),
);
}
}

Here’s the output:

So in the initialisation of ‘containerRotateAnimation’, you can see “Interval”, that is how we control for what part of the animation controller should the tween run, here 0 to 0.5 means from 0 seconds to 500 milliseconds the rotation animation will take place.

Another change that can be seen is using the AnimationController as the animation parameter to AnimatedBuilder, that is preferred to do because not all the animations will run for the entire duration of the animation controller and if all the animations are added to the same widget passing the AnimationController is a better option. For example, if I put containerRotateAnimation in the animation parameter of AnimatedBuilder, then the builder will only build for the first 500 milliseconds because that is the duration for which containerRotateAnimation runs, that’s why it is recommended to use animation controller in such cases.

We are almost done.

Just one more thing, there are another set of inbuilt widgets ‘FooTransitions’ like ScaleTransition, RotateTransition, SlideTransition, etc. They still require us to make the Tween animation and the AnimationController to run the Tween, just the difference is that the tween animation can be directly passed to the widget, without the need of wrapping it with AnimatedBuilder, this can come handy sometimes and help in reducing the boiler plate code.

You can always read more about these things on flutter.dev

Thanks for reading. Go animate something.

Please drop a comment and follow me if you liked this.

--

--