Flutter Animation basics explained with Stacked Cards

Sriram Thiagarajan
CodeChai
Published in
7 min readSep 29, 2018

Animation in flutter is simple once you understand few basic concept such as Animation Controller, Tween. Today we will learn these by implementing one of the design I found on Dribble by Ramakrishna V.

Getting Started

I started learning flutter by implementing some of the design by talented designers and I feel this is much more easy to learn a framework when you have a concrete idea of what you want to achieve. You can follow my learning journey from my other post. Thanks to Ramakrishna for this beautiful design and lets get started leaning about animations.

Implementation — Source Code

Theory on Animation

When planning to add animation to your widget, you should create animation controller in your Stateful Widget. This widget’s state class should be added with Mixin called as SingleTickerProviderStateMixin. As the name suggest this is used when you have only one Animation Controller in your widget and its job is to provide with ticker value to the Stateful Widget.

What is Ticker ???

When you add your SingleTickerProviderStateMixin, it tells Flutter that there is some animation in this widget and this widget needs to notified about the animation frames of flutter.

Controller

Animation controller is one of the basic things necessary for creating magic in flutter. You can imagine this as the dictator of the animation on screen. It contains the duration of the animation and so it will split out values between 0 to 1 based on the duration and the ticker value.

Controller value is 0 -> Start of your animation

Controller value is 1 -> End of your animation

class _CardStackState extends State<CardStack> with SingleTickerProviderStateMixin
.....
controller = AnimationController(
vsync: this,
duration: Duration(seconds: 2),
);

This is the how you will create your controller. vsync property is related to the Ticker and if the stateful widget has the Mixin, you can pass this to this property.

Animation with just the controller is possible when your animated value is also from just 0 to 1. You can pass the controller value to the widget and start the animation by calling controller.forward()

...Opacity(
opacity: controller.value,
child: TouristCard()
)
...
controller.forward()

This will animate TouristCard widget’s opacity from 0 to 1 in 2 seconds.

Definition of Tweening

By Vangie Beal

Short for in-betweening, the process of generating intermediate frames between two images to give the appearance that the first image evolves smoothly into the second image.

Mostly animation will be more complex than just 0 to 1. So we have Tween to help us with those animations. Consider a widget moving from its current position to left. We can achieve it in flutter using FractionalOffset by adding offset property as Offset(-5.0, 0.0). This will move the Widget 5 units to the left. But there will be no animation. It will directly jump to that spot.

FractionalOffset(
offset: Offset(-5.0, 0),
child: TouristCard()
)

Creating the in-between values is the responsibility of Tween. Tween takes a begin ,end, properties and to create a Animation<T> variable we should use the animate method and pass the controller.

Animation<Offset> _animation = new Tween<Offset>(   
begin: const Offset(0.0, 0.0),
end: const Offset(-5.0, 0.0),
).animate(controller);

Now based on the controller duration, begin, end properties, Flutter does the magic to spit out a value when accessing _animation field which will be of type Offset and now we can use that directly in our widget.

FractionalOffset(
offset: _animation.value,
child: TouristCard()
)

When you call animation.forward() it will produce a smooth animation from the begin Offset value to end Offset value.

This is a linear curve of animation as the values increase constantly over time. You can have more natural animations if you can tweak you curves to be different than linear animation. We can do this in flutter using CurvedAnimation class

As you can see the begin and end values are same but how they approach those values can make a big different when working with UI animations. We are observing objects in real life following laws of physics and when a object moving at constant speed stop instantly, it does not feel right and so having the object accelerate faster at start and decelerate at the end of the animation makes it more natural. We can achieve this by modifying easing. To learn more on Easing you can see this post from google.

CurvedAnimation curvedAnimation = CurvedAnimation(parent: controller, curve: Curves.easeOut);

To create a Curved Animation, you can pass the animation controller as parent and specify the curve you would like to have.

This curveAnimation object is very similar to controller object in which it drives the animation forward and so it can be passed to any animation object

Animation<Offset> _animation = new Tween<Offset>(   
begin: const Offset(0.0, 0.0),
end: const Offset(-5.0, 0.0),
).animate(curvedAnimation); // Before we were passing controller

One Last thing to remember

If you have done all of this but still you don’t see the beautiful animation you have created, you might have to check one last thing. When the animation value changes, Flutter needs to know that it has to rebuild the Tree. We can achieve this by using listener on the animation object.

Animation<Offset> _animation = new Tween<Offset>(   
begin: const Offset(0.0, 0.0),
end: const Offset(-5.0, 0.0),
).animate(curvedAnimation)
..addListener(() {
setState(() {});
});

AddListener() function calls the setState() so as to make the current frame as dirty which ensures that the build() is called again.

Back to implementation of design

After such a long theory about animation, I hope you have some energy left to see how the actual design in implemented. I have a Stateless Widget call TouristCard which is responsible for the UI you can see in the design. It is a trivial Flutter widget and you can see the source code of it in the repo.

CardStack Stateful Widget

This is the main widget which controls the animation on the screen. There are three small animation in the widget.

  1. Animation of current card going left
  2. Animation of next card moving up and into place
  3. Animation of next card scaling up

First of all, only the top three cards in the cards variable are used and once they are swiped away, they move to the back of the stack. Each animation has a method where there is some logic to returning the correct value for that card. There are some constant values in these methods which are just value which has worked for me in this example.

class CardStack extends StatefulWidget {
final Function onCardChanged;
CardStack({this.onCardChanged});
@override
_CardStackState createState() => _CardStackState();
}
class _CardStackState extends State<CardStack> with SingleTickerProviderStateMixin {var cards = [
TouristCard(index: 0, imageUrl: "image1.jpeg"),
TouristCard(index: 1, imageUrl: "image2.jpeg"),
TouristCard(index: 2, imageUrl: "image3.jpeg"),
TouristCard(index: 3, imageUrl: "image4.jpeg"),
TouristCard(index: 4, imageUrl: "image1.jpeg"),
TouristCard(index: 5, imageUrl: "image2.jpeg")
];
int currentIndex;
AnimationController controller;
CurvedAnimation curvedAnimation;
Animation<Offset> _translationAnim;
Animation<Offset> _moveAnim;
Animation<double> _scaleAnim;
@override
void initState() {
super.initState();
currentIndex = 0;
controller = AnimationController(
vsync: this,
duration: Duration(milliseconds: 150),
);
curvedAnimation = CurvedAnimation(parent: controller, curve: Curves.easeOut);
_translationAnim = Tween(begin: Offset(0.0, 0.0), end: Offset(-1000.0, 0.0))
.animate(controller)
..addListener(() {
setState(() {});
});
_scaleAnim = Tween(begin: 0.965, end: 1.0).animate(curvedAnimation);
_moveAnim = Tween(begin: Offset(0.0, 0.05), end: Offset(0.0, 0.0))
.animate(curvedAnimation);
}@override
Widget build(BuildContext context) {
return Stack(
overflow: Overflow.visible,
children: cards.reversed.map((card) {
if (cards.indexOf(card) <= 2) {
return GestureDetector(
onHorizontalDragEnd: _horizontalDragEnd,
child: Transform.translate(
offset: _getFlickTransformOffset(card),
child: FractionalTranslation(
translation: _getStackedCardOffset(card),
child: Transform.scale(
scale: _getStackedCardScale(card),
child: Center(child: card),
),
),
),
);
} else {
return Container();
}
}).toList());
}
Offset _getStackedCardOffset(TouristCard card) {
int diff = card.index - currentIndex;
if (card.index == currentIndex + 1) {
return _moveAnim.value;
} else if (diff > 0 && diff <= 2) {
return Offset(0.0, 0.05 * diff);
} else {
return Offset(0.0, 0.0);
}
}
double _getStackedCardScale(TouristCard card) {
int diff = card.index - currentIndex;
if (card.index == currentIndex) {
return 1.0;
} else if (card.index == currentIndex + 1) {
return _scaleAnim.value;
} else {
return (1 - (0.035 * diff.abs()));
}
}
Offset _getFlickTransformOffset(TouristCard card) {
if (card.index == currentIndex) {
return _translationAnim.value;
}
return Offset(0.0, 0.0);
}
void _horizontalDragEnd(DragEndDetails details) {
if (details.primaryVelocity < 0) {
// Swiped Right to Left
controller.forward().whenComplete(() {
setState(() {
controller.reset();
TouristCard removedCard = cards.removeAt(0);
cards.add(removedCard);
currentIndex = cards[0].index;
if (widget.onCardChanged != null)
widget.onCardChanged(cards[0].imageUrl);
});
});
}
}
}

Wrapping Up

You can get the whole source code here. It is easy to create beautiful animation in flutter and I hope you have learned the basic on how to add animations. Stay tuned for more implementations of mobile designs.

Read my other articles on Flutter

Please feel free to ask any question or leave better ways of doing this in the comments. If this article has helped you in any way please share and hit that clap button👏 👏👏 . Happy Fluttering !

Sriram Thiagarajan — Programmer, Mobile Technology Enthusiast who is passionate about providing the best User Experience. You can follow me on Github, Twitter.

The Flutter Pub is a medium publication to bring you the latest and amazing resources such as articles, videos, codes, podcasts etc. about this great technology to teach you how to build beautiful apps with it. You can find us on Facebook, Twitter, and Medium or learn more about us here. We’d love to connect! And if you are a writer interested in writing for us, then you can do so through these guidelines.

--

--

Sriram Thiagarajan
CodeChai

Programmer, Mobile Technology Enthusiast who is passionate about providing the best User Experience | #Flutter #Xamarin #Ionic| Writer at FlutterPub