Introduction
After functionality, your app's user experience is the first thing that distinguishes the product from others in a similar market. One of the best ways to do this is to add custom animations to your application. In this blog series, you'll learn how to create simple animations using ready-made or custom widgets, and build complex in-app transitions, leading up to an introduction to Rive, using Google's Flutter framework.
Pre-Requisites
A good understanding of Dart and Flutter widgets.
Animations in Flutter
In a broader sense, app animations in Flutter can be divided into two main categories;
Implicit: Those animations which do not need a controller and can change their values according to a change in a class variable.
Explicit: Those animations which need a designated animation controller to control their features.
Implicit Animations:
These are made using ready-made widgets which resemble Stateful widgets and do not need a designated animation controller.
1. Using ready-made widgets
By default, Flutter provides certain pre-made widgets with built-in animations that can be used to easily, and quickly beautify our applications. Such widgets accept a duration, and curve parameter to define the time and type of the transition. Since these widgets are also exactly like stateful widgets, we can define a variable to trigger changes in their properties.
For eg, in the given code sample, the animated container changes its color from Red to Green (and vice versa) when tapped, displaying an 'Animation Finished' message on completion.
(Fun Fact: All of such widgets have the word 'Animated' as a prefix, and are collectively known as AnimatedFoo widgets. Some examples include; AnimatedContainer, AnimatedAlign, AnimatedPadding etc.)
class AnimatedContainerExample extends StatefulWidget {
const AnimatedContainerExample({super.key});
@override
State<AnimatedContainerExample> createState() =>
_AnimatedContainerExampleState();
}
class _AnimatedContainerExampleState extends State<AnimatedContainerExample> {
bool val = false;
bool textval = true;
@override
Widget build(BuildContext context) {
return InkWell(
onTap: () => {
setState(() {
textval = true;
val = !val;
})
},
child: AnimatedContainer(
duration: const Duration(seconds: 2),
curve: Curves.bounceIn,
onEnd: () {
setState(() {
textval = false;
});
},
color: val == false ? Colors.green : Colors.red,
height: 100,
width: 100,
child: Center(child: Text(textval == true ? "" : "Animation Finished")),
),
);
}
}
2. Using Tweens
In general, tweens are used to define any animations that have a distinct beginning and ending value or range. We can create different types of Tweens to animate different properties of a widget, about which you'll learn more towards the end of this article.
In the given code sample, we create a simple slider widget. And since the slider has a range of values from 0 to 1, we use TweenAnimationBuilder for this purpose.
Please note that tweens are not animations, and are just a collection of ranges. To animate such tweens, a TweenAnimationBuilder is used.
class TweenAnimationExample extends StatefulWidget {
const TweenAnimationExample({super.key});
@override
State<TweenAnimationExample> createState() => _TweenAnimationExampleState();
}
class _TweenAnimationExampleState extends State<TweenAnimationExample> {
double sliderval = 0;
@override
Widget build(BuildContext context) {
return TweenAnimationBuilder(
tween: Tween(begin: 0, end: sliderval),
duration: const Duration(milliseconds: 10),
builder: ((context, value, child) {
return Container(
height: 100,
width: 300,
color: Colors.pink,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: child,
),
);
}),
child: Column(
children: [
Text("${sliderval.toStringAsPrecision(2)}%"),
Slider(
thumbColor: Colors.black,
value: sliderval,
min: 0,
onChanged: ((value) {
setState(() {
sliderval = value;
});
})),
],
),
);
}
}
Explicit Animations:
What is an Animation Controller?
Before diving into working with explicit animations, you must know what an animation controller is, and how it works.
An animated controller is simply a remote control for an animation. Think of it exactly like a television remote. The remote tells the TV when to turn on, which channel to display, what sound level to use, and so on. Similarly, an animated controller is used to describe an animation and;
- Play it forward or in reverse, or stop it.
- Set it to a specific value.
- Define its upperBound and lowerBound values.
But how does a controller know when to start or stop its animation in sync with our app's startup?
The animation controller requires a vsync parameter of type TickerProvider. Simply put, this parameter keeps track of our app's build frames and registers itself with the SchedulerBinding. So, every time a new app frame is generated, the vysnc parameter updates our animation. This is how we see it as continuously moving throughout the app lifecycle.
Now, let's learn how to use an animation controller to efficiently beautify our user interface.
3. Creating Custom Animated Widgets
Notice how there is no pre-made GradientAnimatedContainer in Flutter. To achieve something similar, we can easily define a custom container by using a controller to animate its gradient.
(Note: It is a good practice to dispose of your controllers once they are no longer required. This prevents any chance of leaks)
class AnimatedBuilderExample extends StatefulWidget {
const AnimatedBuilderExample({super.key});
@override
State<AnimatedBuilderExample> createState() => _AnimatedBuilderExampleState();
}
class _AnimatedBuilderExampleState extends State<AnimatedBuilderExample>
with SingleTickerProviderStateMixin {
// please read the note given below to understand the use of the SingleTickerProviderStateMixin in this context
late final AnimationController animController;
@override
void initState() {
animController =
AnimationController(vsync: this, duration: const Duration(seconds: 3))
..repeat(reverse: true);
super.initState();
}
@override
void dispose() {
animcontroller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
height: 100,
color: Colors.blueAccent,
child: AnimatedBuilder(
animation: animController,
builder: ((context, child) {
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(colors: const [
Colors.blueAccent,
Colors.purpleAccent,
Colors.pinkAccent,
], stops: [
0,
animController.value,
1
])),
);
})));
}
}
Code Explanation: To register a vsync value for our animation controller here, we extend our widget's state to use the SingleTickerStateMixin. The 'this' property of the mixin automatically provides app frames to our controller.
Please note that this is the most efficient method to handle a single controller. If you want to work with multiple animation controllers, please refer here.
4. Using Ready-Made Transitions
This animation example exploits two major concepts; namely, FooTransition widgets and Advanced Tweens.
Like with AnimatedFoo, FooTransition is a collection of Transition widgets, that are pre-made and provided by Flutter inside the material library. These widgets require an animation parameter, wherein we can add any custom or ready-made transition.
Now, with animations that start from a beginning value, and end at a distinct value (of any type), we use Tweens. We can create different types of Animations to provide to our AnimatedBuilder using such tweens.
For eg, if we want to change the alignment of a widget when triggered, we can define a tween of type; Tween<AlignmentGeometry>. Similarly, to define a widget that changes its size, we can create a Tween<double> object.
But, this tween would just be a range between two values. To animate it, either we use a TweenAnimationBuilder as we did in our previous examples, or we simply use an animation controller. The controller can be added using the .animate() property of the tween.
In the given example, I've used simple tweens to create a rotation and alignment animation and animated them within the FooTransition widgets. (AlignTransition, RotationTransition etc.)
class TransitionExample extends StatefulWidget {
const TransitionExample({super.key});
@override
State<TransitionExample> createState() => _TransitionExampleState();
}
class _TransitionExampleState extends State<TransitionExample>
with SingleTickerProviderStateMixin {
late final AnimationController animcontroller;
late final Animation<AlignmentGeometry> alignAnimation;
late final Animation<double> rotateAnimation;
@override
void initState() {
super.initState();
animcontroller =
AnimationController(vsync: this, duration: const Duration(seconds: 10))
..repeat(reverse: true);
alignAnimation = Tween<AlignmentGeometry>(
begin: Alignment.centerLeft, end: Alignment.centerRight)
.animate(
CurvedAnimation(parent: animcontroller, curve: Curves.bounceIn));
rotateAnimation = Tween<double>(begin: 1, end: 3).animate(animcontroller);
}
@override
void dispose() {
animcontroller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
height: 100,
color: Colors.yellow,
child: AlignTransition(
alignment: alignAnimation,
child: RotationTransition(
turns: rotateAnimation,
child: Container(
height: 50,
width: 50,
color: Colors.black,
),
)),
);
}
}
Conclusion
In this article, you learned how to create simple widget-based animations in your Flutter app. [You can find the entire code along with a working prototype of this blog here.] While most of these are simple animations, they can be performed using multiple widgets, and ways. In the following articles of this series, we're going to explore more complex in-app animations like drag-and-drop image transitions, and complex routing animations.