Flutter : flip animation

David Gonzalez
Flutter Community
Published in
7 min readJan 12, 2021

When I first saw the AnimationSwitcher widget, i thought in my mind that I will be able to flip a widget, revealing its rear side.

What we want to achieve

I was all wrong: the AnimationSwitcher allows you to… switch between several widgets, with an animation you define (the default animation is a fade transition). This component is too generic for that purpose.

I should have read carefully…

But its usage is very generic, so I will show you how that animation can be done.

I discovered a flutter package that may do that flip animation, named animated_card_switcher, but it seems to be not properly maintained, and the code is too complex.

Here is the development steps :

  • Create the front and rear widgets
  • Use AnimationSwitcher widget to animate
  • Code your custom transition builder to rotate your card
  • Add curves

Create Front and Rear widgets

In this example, I will take simplified versions of front and rear widgets, because it’s not very important.

The only thing to keep in mind is that you have to set a key to your top-level widgets, to let the AnimationSwitcher detect that the widget has changed (and therefore execute the animation).

Like a piece of cake !

Here is an example of widgets layout I will use :

Widget __buildLayout({Key key, String faceName, Color backgroundColor}) {
return Container(
key: key,
decoration: BoxDecoration(
shape: BoxShape.rectangle,
borderRadius: BorderRadius.circular(20.0),
color: backgroundColor,
),
child: Center(
child: Text(faceName.substring(0, 1), style: TextStyle(fontSize: 80.0)),
),
);

So my widget front and rear views will be:

Widget _buildFront() {
return __buildLayout(
key: ValueKey(true),
backgroundColor: Colors.blue,
faceName: "F",
);
}

Widget _buildRear() {
return __buildLayout(
key: ValueKey(false),
backgroundColor: Colors.blue.shade700,
faceName: "R",
);
}

Use AnimationSwitcher widget to animate

Now, we can use the AnimationSwitcher widget to animate transition between front and rear widgets.

In a StatefulWidget, I override the build method to create a page that will show the animation in its center.

class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);

final String title;

@override
_MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
bool _displayFront;
bool _flipXAxis;

@override
void initState() {
super.initState();
_displayFront = true;
_flipXAxis = true;
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(this.widget.title),
centerTitle: true,
),
body: Center(
child: Container(
constraints: BoxConstraints.tight(Size.square(200.0)),
child: _buildFlipAnimation(),
),
);
}
}

I separated the animation from the page into _buildFlipAnimation method, to clarify my code.

Here is a first version of that method:

Widget _buildFlipAnimation() {
return GestureDetector(
onTap: () => setState(() =>_showFrontSide = !_showFrontSide),
child: AnimatedSwitcher(
duration: Duration(milliseconds: 600),
child: _showFrontSide ? _buildFront() : _buildRear(),
),
);
}

When we click on the widget, we can see that the front widget is fading, revealing the rear side. Clicking again will fade the rear widget to reveal the front side.

At least something is happening.

We want to rotate widgets from the Y axis. Hopefully, the AnimationSwitcher allows us to redefine the transition, thanks to the input transitionBuilder.

Code your custom transition builder to rotate your card

So here is the plan: we want a rotation of 180° (pi). We will wrap our widgets into an AnimatedBuidler, and use the Transform widget to apply the rotation.

Widget __transitionBuilder(Widget widget, Animation<double> animation) {
final rotateAnim = Tween(begin: pi, end: 0.0).animate(animation);
return AnimatedBuilder(
animation: rotateAnim,
child: widget,
builder: (context, widget) {
return Transform(
transform: Matrix4.rotationY(value),
child: widget,
alignment: Alignment.center,
);
},
);
}
Flip is correct, but we don’t feel depth.

That’s a good start, but not exactly what we want. We can see that when animating transition, the rear widget is on top from beginning to the end.

The display of the final side comes too fast.

We need that the front being progressively replaced by the rear widget.

So we need to modify two things:

  • The display order must be reversed: the widget to replace must be on top of the stack.
  • At the middle of the animation, the widget to be replaced must disappear.

To do that, we will change the layoutBuilder input of our AnimationSwitcher instance.

layoutBuilder: (widget, list) => Stack(children: [widget, ...list]),

Then, at the middle of the animation, the rotation of pi/2 makes the widget‘s width to 0.0. So we will block that rotation for the previous widget (only) to animate.

Widget __transitionBuilder(Widget widget, Animation<double> animation) {
final rotateAnim = Tween(begin: pi, end: 0.0).animate(animation);
return AnimatedBuilder(
animation: rotateAnim,
child: widget,
builder: (context, widget) {
final isUnder = (ValueKey(_showFrontSide) != widget.key);
final value = isUnder ? min(rotateAnim.value, pi / 2) : rotateAnim.value;
return Transform(
transform: Matrix4.rotationY(value),
child: widget,
alignment: Alignment.center,
);
},
);
}

That’s better now, but we haven’t finished yet! To reinforce the feeling that the widget is rotating, we will add a little “tilt” on the widgets.

We need depth… A lovely little fluffy depth.

This “tilt” value must be 0.0 at the beginning and the end of the animation. Also, because we will apply an animation for each side of our widget, then their tilt must be opposite. For example, if front widget tilt is 0.2, then the rear tilt must be -0.2.

Widget __transitionBuilder(Widget widget, Animation<double> animation) {
final rotateAnim = Tween(begin: pi, end: 0.0).animate(animation);
return AnimatedBuilder(
animation: rotateAnim,
child: widget,
builder: (context, widget) {
final isUnder = (ValueKey(_showFrontSide) != widget.key);
var tilt = ((animation.value - 0.5).abs() - 0.5) * 0.003;
tilt *= isUnder ? -1.0 : 1.0;

final value = isUnder ? min(rotateAnim.value, pi / 2) : rotateAnim.value;
return Transform(
transform: Matrix4.rotationY(value)..setEntry(3, 0, tilt),
child: widget,
alignment: Alignment.center,
);
},
);
}

To apply the tilt to the widget, we manually set one specific value to the Matrix4 object defining the rotation.

You can get more information about Matrix4 here: https://medium.com/flutter-community/advanced-flutter-matrix4-and-perspective-transformations-a79404a0d828

Add curves

Finally, to add a little vigor / strength to the animation, you may want to modify curves inputs parameters of the AnimationSwitcher.

It’s always better with curves !

Here is my first attempt:

Widget _buildFlipAnimation() {
return GestureDetector(
onTap: _switchCard,
child: AnimatedSwitcher(
duration: Duration(milliseconds: 4600),
transitionBuilder: __transitionBuilder,
layoutBuilder: (widget, list) => Stack(children: [widget, ...list]),
child: _showFrontSide ? _buildFront() : _buildRear(),
switchInCurve: Curves.easeInBack,
switchOutCurve: Curves.easeOutBack,

),
);
}

I have to show you in slow motion what the problem is.

Ho nooooooooooo …

As the curves are not totally identical, and because the previous widget animation will be played reverse, you will get that artifact where the two widgets are not properly superimposed. There will be a slightly shift between the two.

The animation during few frames…

To avoid that, we have to use the flipped property of the curve we want to use.

switchInCurve: Curves.easeInBack,
switchOutCurve: Curves.easeInBack.flipped,
Sloooow Mooooo … (now perfect)

Conclusion

As you can see, it is not a big deal: I made this animation with around 30 lines of code (animation only), using only one attribute (the displayed side of the widget).

I don’t think it’s wise to create a package for that. Adding dependency to your code means that if it does not work on version update (for example), your project will be stuck for a certain amount of time. Worse, if dependency is not maintained anymore, you won’t be able to guaranty that your project will compile correctly in the next 6 months, 1 or two years…

Copy-paste this example wisely :)

So feel free to use my code. I’m not fan of copy-paste, so I would say “appropriate my code”, meaning copy-paste it, understand it, modify it to your needs!

You will find the demonstration used in this article here:

Follow Flutter Community on Twitter: https://www.twitter.com/FlutterComm

--

--

David Gonzalez
Flutter Community

Hi, I’m David, a french mobile applications developer. As a freelance, I work on Android and iOS applications since 2010. I also work on Flutter now !