Types of animations in Flutter and its implementation

AppVesto LLC
9 min readDec 21, 2020

--

Good day everyone, my name is Andrey. I am a Flutter developer. In the last article, we looked at creating an object with canvas and its animation. So this time, I decided to talk more about the different types of animation in Flutter and their work and implementation principles.

To begin with, have you ever wondered why you need animation in a mobile application? After all, it is not a necessary detail in the application, then why waste time on it? It is unlikely that the user will pay attention to how some object smoothly disappears. But the thing is that the user might not see the smooth decrease of transparency, but he will pay attention to it if it happens abruptly. You can compare it yourself with a small example and draw conclusions.

A good animation has 2 main functions:
1. Improving the UI (user interface) by making the application run more smoothly.
2. In fact, animation is not only improves the UI, but is engaged in what teaches the user to use our application. It’s thanks to the animation you can let the user know that the action occurred, for example, that the user pressed a button. It’s a little thing that you never pay attention to, but you come across it every day.

Flutter itself, in terms of animation and smoothness, is very good. Its main advantages in terms of animation are many pre-made widgets that easily allow you to work with animation and its work with animations under the “hood”.
The animation system in flutter is based on typed animation objects. Widgets can either incorporate these animations into their build functions directly by reading their current value and listening to their state changes. They can use the animations to create more complex animations that they pass on to other widgets.
Well, let’s move on from theory to practice and look at each of these types of animations in more detail.
As I mentioned earlier, there are many pre-made widgets in a flutter to create animations in a flutter. There are so many that for most animation-related tasks, they are quite enough.
Since we can’t look at every widget, we will stop at a few of the most commonly used and most interesting. However, you can get acquainted with each of them on the official website flutter in the widgets catalog for animation.
And so, let’s start by taking a detailed look at the AnimatedOpacity widget. As the name suggests, this widget is responsible for the transparency of the object passed to the child parameter. It’s essentially the animated counterpart of the Opacity widget. Almost all animated widgets have a mandatory parameter, duration, responsible for the transition time from father state to son state. Interestingly, if, for example, during the transition from the start state to the end state, which lasts 4 seconds, 1 second has passed, and we again change states to start, then back animation time will take 1 second instead of 4.

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Animation example')),
body: Center(
child: AnimatedOpacity(
duration: Duration(milliseconds: 500),
opacity: isView ? 0.0 : 1.0,
child: InkWell(
onTap: () => setState(() => isView = !isView),
child: Container(
width: 200.0,
height: 200.0,
color: Colors.green,
),
),
),
),
);
}
}

The next important parameter is the curve. This is the curve, which is responsible for how the parameter, in this case, opacity, will change while the animations are running.
It is used to adjust the animation’s speed over time, allowing it to accelerate and decelerate rather than moving at a constant speed. To use a curve, you can choose one of the many pre-made ones in the Curves class. The default is Curves. linear, which is just straight linear animation. You can find a complete list of curves on the official flutter website.

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Animation example')),
body: Center(
child: AnimatedOpacity(
duration: Duration(milliseconds: 2000),
opacity: isView ? 0.0 : 1.0,
curve: Curves.bounceIn,
child: InkWell(
onTap: () => setState(() => isView = !isView),
child: Container(
width: 200.0,
height: 200.0,
color: Colors.green,
),
),
),
),
);
}

The second option is to create your curves, but this is a very rarely used option because we are usually satisfied with the standard curves. To make your curves, we must create a new class based on the Curve class.

class _MyAppState extends State<MyApp> {
bool isView = false;

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Animation example')),
body: Center(
child: AnimatedOpacity(
duration: Duration(milliseconds: 400),
opacity: isView ? 0.0 : 1.0,
curve: ExampleCurve(),
child: GestureDetector(
onTap: () => setState(() => isView = !isView),
child: Container(
width: 200.0,
height: 200.0,
color: Colors.green,
),
),
),
),
);
}
}

class ExampleCurve extends Curve {
final double count;

ExampleCurve({this.count = 3});

@override
double transformInternal(double t) {
var val = sin(count * 2 * pi * t) * 0.5 + 0.5;
return val;
}
}

So with the example of the AnimatedOpacity widget, we’ve covered the necessary parameters for any animation. Let’s take a less detailed look at a few more widgets.
AnimatedPosition, like AnimatedOpacity, is an animated version of another widget, namely Position. Performs an animation if the Position of one or more sides has changed.

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Animation example')),
body: Stack(
alignment: Alignment.centerLeft,
children: [
AnimatedPositioned(
left: position,
duration: Duration(milliseconds: 400),
child: Image.network( 'https://cdn.pixabay.com/photo/2012/04/11/17/34/car-29078_640.png',
width: 200.0,
),
),
],
),
floatingActionButton: FloatingActionButton(
child: Text('GO!'),
onPressed: () {
setState(() {
position += 40;
});
},
),
);
}

The next useful widget is AnimatedCrossFade, with which you can create an animated transition with decreasing transparency between two widgets. It has some particular parameters, namely firstChild and secondChild, representing the first widget and the second. To change them, we must change the crossfade state parameter, specifying which widget we should display. Let’s see a small example:

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Animation example')),
body: Center(
child: SizedBox(
width: 200.0,
height: 200.0,
child: InkWell(
onTap: () => setState(() => isChangeWidget = !isChangeWidget),
child: AnimatedCrossFade(
crossFadeState: isChangeWidget ? CrossFadeState.showFirst : CrossFadeState.showSecond,
duration: const Duration(seconds: 2),
firstChild: Container(color: Colors.green),
secondChild: Container(color: Colors.red),
),
),
),
),
);
}

When clicked, the selected widget will change depending on the isChangeWidget variable.
The AnimateContainer, a very easy to understand widget, is analogous to the regular Container widget, with the difference that almost every parameter is animated. In case you have a Container, which has a variable parameter, for example, color or size, I recommend replacing it with AnimatedContainer, logically, nothing will not change, but for the user everything will be smoother.
Let’s look at an example:

bool startAnimation = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Animation example')),
body: Center(
child: InkWell(
onTap: () => setState(() => startAnimation = !startAnimation),
child: AnimatedContainer(
decoration: BoxDecoration(
color: startAnimation ? Colors.lightGreen : Colors.red,
borderRadius: BorderRadius.circular(startAnimation ? 15.0 : 0.0),
),
width: startAnimation ? 100 : 200,
height: startAnimation ? 100 : 200,
curve: Curves.easeInOutCubic,
duration: Duration(seconds: 1),
),
),
),
);
}

Although the functionality, the basic widgets for animation is quite extensive, it is not infinite, and the biggest problem is when the animation is greatly complicated and become complex, that is not enough just change the transparency or size, and you need, for example, first reduce the transparency, and then reduce the size.
Of course, you can solve this problem using multiple variables and widgets or other “crutches,” but the best and easiest solution is to use the AnimationController and the widget Animation. But first, we need to create our class with SingleTickerProviderStateMixin.
It is needed to create a class with only one AnimationController. During initialization, which we need to do in initState, we pass the current object in the vsync parameter. This mixin only supports one ticker. If you may have multiple AnimationController objects over the lifetime of the state, use TickerProviderStateMixin instead.
In flutter, the animation object doesn’t know anything about what’s on the screen. The animation is an abstract class that understands its current value and its state. One of the most commonly used animation types is animation.
It allows you to move smoothly from one value to another over time. For example, let’s try to create a container that will change size and transparency when clicked.

class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
AnimationController _animationController;

@override
void initState() {
_animationController = AnimationController(duration: Duration(milliseconds: 1000), vsync: this);
_animationController.addListener(() => setState(() {}));
super.initState();
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Animation example')),
body: InkWell(
onTap: () => _animationController.forward(),
child: Opacity(
opacity: 1.0 - _animationController.value,
child: Container(
color: Colors.orange,
height: 200.0,
width: 200.0 + (200 * _animationController.value),
),
),
),
);
}
}

And so the important thing to say is that after initializing the controller, we create a listener:

_animationController.addListener(() => setState(() {}));
This is necessary in order to update the states as the animation runs. To start the animation, the forward method is called when the button is clicked:
onTap: () => _animationController.forward(),
This allows you to run the animation, to run it in reverse, the reverse method is used:
onTap: () => _animationController.reverse(),
By default, the AnimationController object is between 0.0 and 1.0. If you need a different range or data type, you can use Tween to set the animation to interpolate to a different range or data type.
For example, the following Tween goes from -200.0 to 0.0:
tween = Tween<double>(begin: -200, end: 0);
This is actually a very important detail, because you can use tween to get more functionality, for example it is much easier to change the color in an animated way. Let’s try to change the color of the container, for example.

To begin, we create a controller and animate. In the initState method, we initialize the controller. Next, based on the ColorTween, which we set to initial blue and the final result to red, we initialize animate and link it to the controller. Then we create a listener that will update the states every time the state changes.
We call the _animationConroller’s repeat method when the container is clicked, which frequently performs the animation, repeating it every time.

class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
AnimationController _animationController;
Animation<Color> _animation;

@override
void initState() {
_animationController = AnimationController(duration: Duration(milliseconds: 400), vsync: this);
_animation = ColorTween(begin: Colors.blue, end: Colors.red).animate(_animationController);
_animationController.addListener(() => setState(() {}));
super.initState();
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Animation example')),
body: Center(
child: InkWell(
onTap: () => _animationController.repeat(),
child: Container(
width: 200.0,
height: 200.0,
color: _animation.value,
),
),
),
);
}
}

In the end, let’s try to resize the widget and change its color using some controllers. In this case, the resizing must happen after the color change. To do this, we will need to use TickerProviderStateMixin, not SIngleTickerProviderStateMixin as before. We’ll also need to use a new kind of listener, namely addStatusListener, which will be called only when the state changes. We’re interested in having the second controller called at the end of the first animation.

class _MyAppState extends State<MyApp> with TickerProviderStateMixin {
AnimationController _colorController;
AnimationController _sizeController;

Animation<Color> _colorAnimation;
Animation<double> _sizeAnimation;

@override
void initState() {
_colorController = AnimationController(duration: Duration(milliseconds: 1000), vsync: this);
_sizeController = AnimationController(duration: Duration(milliseconds: 1000), vsync: this);

_colorAnimation = ColorTween(begin: Colors.blue, end: Colors.red).animate(_colorController);
_sizeAnimation = Tween<double>(begin: 200.0, end: 300.0).animate(_sizeController);

_colorController.addListener(() => setState(() {}));
_sizeController.addListener(() => setState(() {}));

_colorController.addStatusListener((status) {
if (status == AnimationStatus.completed) _sizeController.forward();
});
super.initState();
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Animation example')),
body: Center(
child: InkWell(
onTap: () => _colorController.forward(),
child: Container(
color: _colorAnimation.value,
height: _sizeAnimation.value,
width: _sizeAnimation.value,
),
),
),
);
}
}

This time, we learned different ways to create animation and examined various examples and widgets to help us. Leave your comments, what you liked, and what you did not like, or what you would like to add. Perhaps you have questions. I’ll be happy to answer them.

--

--

AppVesto LLC
AppVesto LLC

Written by AppVesto LLC

We are a team of rock-star developers, which provides fully-flexible and powerful products 💥.

No responses yet