Getting Started with Animations in Flutter

Jackie Moraa
7 min read2 days ago

--

Animations are changes in the appearance or behaviour of a widget over a period of time and they are an amazing way to bring your Flutter applications to life. These changes could be in e.g., size, color, opacity, position etc.

Animations that are well-designed will make the user interface feel more intuitive which in turn, will improve the user’s experience.

We touched on animations while experimenting with Lottie package a while back, but in this article we’ll be focusing on the animation widgets provided by Flutter’s core library. Flutter is a rich animation framework which allow developers to create smooth and visually appealing transitions.

It is important to note that these Flutter animation widgets are for simple animations (code based animations). If your project is dealing with complex animations e.g., that require vector graphics or raster images (drawing based animations) then perhaps consider Lottie or other animation packages like Rive.

Types of animations
Code based Flutter animations can be categorised into two main types; implicit and explicit. The difference between these two categories is mainly on the control the developer has over the animation and how that animation is implemented.

Implicit animations
These are animations that are pre-programmed in the framework so the developer doesn’t need to explicitly write the animation code. Instead, Flutter will handle the animation based on changes to some properties or values as specified by the developer. e.g., AnimatedContainer, AnimatedPositioned, AnimatedOpacity. Because of this, implicit animations are the better option when it comes to creating simple animations like changing the size or position of a widget in based on an event or user input.

  • Easy to implement because it requires minimal code
  • Minimal control over the animation process.

Explicit animations
Unlike implicit animations, these are animations that the developer can control explicitly by specifying the animation behaviour and properties e.g., animation values, such as starting and ending points. They provide a greater degree of flexibility and control compared to implicit animations. e.g., AnimationController, AnimatedBuilder, Tween. Because of this, explicit animations are suitable for complex and custom animation scenarios.

  • Harder and more complicated to implement because they require additional code
  • More control over the animation logic e.g., timing, progress etc

Implicit Animations
Let’s look at a few examples of implicit animation widgets.

AnimatedContainer
The AnimatedContainer widget is an animated version of the Container widget that gradually changes its value over time. It will automatically animate between old and new values of properties using the parameter provided. Its child and descendants are not animated.

import 'package:flutter/material.dart';

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

@override
State<AnimatedContainerExample> createState() => _AnimatedContainerExampleState();
}

class _AnimatedContainerExampleState extends State<AnimatedContainerExample> {
bool _isExpanded = false;

@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.pink[100],
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedContainer(
width: _isExpanded ? 200.0 : 100.0,
height: _isExpanded ? 200.0 : 100.0,
color: _isExpanded ? Colors.purpleAccent : Colors.yellowAccent,
duration: const Duration(seconds: 1),
curve: Curves.easeInOut,
),
ElevatedButton(
onPressed: () {
setState(() {
_isExpanded = !_isExpanded;
});
},
child: Text('Resize Block'),
),
],
),
),
);
}
}

In this example, we are using the AnimatedContainer widget to animate the size and colour change of the Container based on the button click which toggles the isExpanded value. We are specifying the duration of the animation and how that transition should look like using the curve property. Check it out below.

AnimatedPositioned
The AnimatedPositioned widget is an animated version of the Positioned widget that automatically transitions the child’s position over a given duration whenever the given position changes. This widget animates smoothly between the old and new positions when the values e.g., top, left, right, or bottom are changed.

import 'package:flutter/material.dart';

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

@override
State<AnimatedPositionedExample> createState() => _AnimatedPositionedExampleState();
}

class _AnimatedPositionedExampleState extends State<AnimatedPositionedExample> {
bool _isMoved = false;

@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.blue[100],
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
width: 400,
height: 400,
child: Stack(
children: [
AnimatedPositioned(
top: _isMoved ? 50.0 : 200.0,
left: _isMoved ? 50.0 : 200.0,
duration: const Duration(seconds: 1),
curve: Curves.easeInOut,
child: Container(
width: 100.0,
height: 100.0,
color: Colors.pinkAccent,
),
),
],
),
),
ElevatedButton(
onPressed: () {
setState(() {
_isMoved = !_isMoved;
});
},
child: const Text('Move Block'),
),
],
),
);
}
}

In this example, we are using the AnimatedPositioned widget to animate the position of the Container based on the button click which toggles the isMoved value. Check it out below.

AnimatedOpacity
The AnimatedOpacity widget is the animated version of the Opacity class which automatically transitions the child’s opacity over a given duration whenever the given opacity changes. The opacity ranges from 0.0 (fully transparent) to 1.0 (fully opaque) as seen in the demo below.

import 'package:flutter/material.dart';

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

@override
State<AnimatedOpacityExample> createState() => _AnimatedOpacityExampleState();
}

class _AnimatedOpacityExampleState extends State<AnimatedOpacityExample> {
double opacityLevel = 1.0;

@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.lime,
appBar: AppBar(
title: const Text('Animated Opacity'),
automaticallyImplyLeading: true,
backgroundColor: Colors.redAccent,
),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Center(
child: AnimatedOpacity(
opacity: opacityLevel,
duration: const Duration(seconds: 7),
child: Container(
width: 200.0,
height: 200.0,
color: Colors.redAccent,
),
),
),
ElevatedButton(
onPressed: () {
setState(() => opacityLevel = opacityLevel == 0 ? 1.0 : 0.0);
},
child: const Text('Fade Block'),
),
],
),
);
}
}

Explicit Animations
Similarly, let’s look at a few examples of explicit animation widgets.

AnimationController
The AnimationController class controls the lifecycle of an animation. It allows you to specify the following properties:

  • duration: The total time taken to run the animation.
  • forward(): Starts the animation in the forward direction.
  • reverse(): Reverses the animation back to its start.
  • stop(): Stops the animation.
  • repeat(): Repeats the animation indefinitely.
  • value: The current value of the animation. By default, the values range from 0.0 to 1.0, during a given duration. The animation controller generates a new value whenever the device running your app is ready to display a new frame (typically, this rate is around 60 values per second).
  • vsync: An AnimationController requires a TickerProvider. Tickers can be used by any object that wants to be notified whenever a frame triggers. A TickerProvider signals when to refresh the animation using SingleTickerProviderStateMixin or TickerProviderStateMixin. This TickerProvider is configured using the vsync argument on the constructor.

PS. An AnimationController should be disposed when it is no longer needed. This will reduces any memory leaks that might occur.

AnimatedBuilder
The AnimatedBuilder class allows you to build animations without having to rebuild the entire widget tree. It’s particularly useful when the animation affects only a part of the widget tree, as it prevents unnecessary rebuilds. This will optimise the app performance.

The widget takes an animation and a builder function, and it rebuilds the relevant parts of the UI whenever the animation’s value changes.

  • animation: The animation object that changes over time.
  • builder: The function that will return the widget tree based on the animation’s current value. If there is a subtree which doesn’t depend on the animation in this builder function, it will only be built once and not rebuilt on every animation tick.

Tween
As mentioned above (while discussing AnimationController), the default value ranges from 0.0 to 1.0, during a given duration. However, if you require a different range or a different data type, you can use theTween class to configure the animation and to interpolate to a different range or data type e.g., 100.0 to 200.0.

Tween takes only begin and end values and it’s sole purpose is to define a mapping from an input range to an output range.

How they all work together
These three classes can work together in one animation in that the AnimationController will drive the animation, the Tween will map the controller’s values and the AnimatedBuilder will work to update the parts of the widget tree affected by the value changes. See the example below.

import 'package:flutter/material.dart';

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

@override
State<ExplicitAnimationsExample> createState() => _ExplicitAnimationsExampleState();
}

class _ExplicitAnimationsExampleState extends State<ExplicitAnimationsExample> 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: 50.0, end: 200.0).animate(_controller);

_controller.forward();
_controller.repeat();
}

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

@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.amberAccent[100],
body: Center(
child: AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Container(
width: _animation.value,
height: _animation.value,
color: Colors.greenAccent,
);
},
),
),
);
}
}

This is the resulting animation. The width and height of the container are animated based on the given value over the specified duration.

Working with animations is one of things that seem pretty daunting, but once you get the basics right, things will start falling into place. In a future article, I’ll take on the challenge to work with CutomPainter and dive deeper into this world of animations. But until then, check out the code for these basic animation examples from this article on my GitHub. Here is the link.

“Animation is not the art of drawings that move but the art of movements that are drawn.”
— Norman McLaren

Como siempre, thank you for reading! ❤

--

--