You might already be familiar with creating basic animations in Flutter. But how about transforming these simple widgets into stunning animations? Let’s dive in and discover how to make it happen, starting with the impressive animation you can see below 👇

Animation preview

Before we start, thanks to Louis for this fantastic animation. Also, for those who prefer video guides, a tutorial is for you!

So, let’s start

You can create a new Flutter project or use an existing one. The black hole in the animation is actually an image, so let’s add it to the project. Next, let’s create our animation page, I’m gonna call it CardHiddenAnimationPage

class CardHiddenAnimationPage extends StatefulWidget {
const CardHiddenAnimationPage({Key? key}) : super(key: key);

@override
State<CardHiddenAnimationPage> createState() =>
CardHiddenAnimationPageState();
}

class CardHiddenAnimationPageState extends State<CardHiddenAnimationPage>
with TickerProviderStateMixin {
final cardSize = 150.0;

@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: Row(
mainAxisSize: MainAxisSize.min,
children: [
FloatingActionButton(
onPressed: () {},
child: const Icon(Icons.remove),
),
const SizedBox(width: 20),
FloatingActionButton(
onPressed: () {},
child: const Icon(Icons.add),
),
],
),
// TODO: add body
);
}
}

 

It’s a StatefulWidget with TickerProviderStateMixin where we have two floating buttons on the bottom right side and define the cardSize = 150

Black hole animation

The plan is simple when the minus button is clicked, the black hole appears then after a moment it disappears. In scenarios like this, the Tween widget is extremely useful. Let me show you how

late final holeSizeTween = Tween<double>(
begin: 0,
end: 1.5 * cardSize,
);
late final holeAnimationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 300),
);
double get holeSize => holeSizeTween.evaluate(holeAnimationController);

Next, we need to ensure that animation changes are listened, and it’s crucial to dispose of it appropriately when it’s no longer needed.

@override
void initState() {
holeAnimationController.addListener(() => setState(() {}));
super.initState();
}

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

 

We are done with the animation setup. Now, let’s put it on the page. Just change TODO: add body to the below code👇

body: Center(
child: SizedBox(
height: cardSize * 1.25,
width: double.infinity,
// TODO: wrap Stack with ClipPath
child: Stack(
alignment: Alignment.bottomCenter,
clipBehavior: Clip.none,
children: [
SizedBox(
width: holeSize, // animate the black hole 
child: Image.asset(
'images/hole.png',
fit: BoxFit.fill,
),
),
// TODO: Hello world card
],
),
),
),

It’s show time! Let’s return to the minus floating action button and make its onPressed function asynchronous.

FloatingActionButton(
onPressed: () async {
await holeAnimationController.forward();
Future.delayed(const Duration(milliseconds: 200),
() => holeAnimationController.reverse());
},
....
)

What we’re aiming to do here is let the animation finish first, wait 200 milliseconds, and then reverse the animation.

Preview black hole animation

Card animation

First, let’s make a card. I’m going to call it HelloWorldCard

class HelloWorldCard extends StatelessWidget {
const HelloWorldCard({
Key? key,
required this.size,
required this.elevation,
}) : super(key: key);

final double size;
final double elevation;

@override
Widget build(BuildContext context) {
return Material(
elevation: elevation,
borderRadius: BorderRadius.circular(10),
child: SizedBox.square(
dimension: size,
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: Colors.blue,
),
child: const Center(
child: Text(
‘Hello\nWorld’,
textAlign: TextAlign.center,
style: TextStyle(color: Colors.white, fontSize: 18),
),
),
),
),
);
}
}

 

Now, change TODO: Hello world card to the code given below

Positioned(
child: Center(
child: Transform.translate(
offset: Offset(0, 0), // TODO: Animate the offset
child: Transform.rotate(
angle: 0, // TODO: Animate the angle
child: Padding(
padding: const EdgeInsets.all(20.0),
child: HelloWorldCard(
size: cardSize,
elevation: 2, // TODO: Animate the elevation
),
),
),
),
),
),

We’re almost there. We’ve added the black hole and the hello world card. The plan is, when the user clicks the minus button, the card will move downward, rotate slightly, and its elevation will increase. To do that, we’ll use the same Tween technique that we used to animate the hole size.

late final cardOffsetAnimationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1000),
);

late final cardOffsetTween = Tween<double>(
begin: 0,
end: 2 * cardSize,
).chain(CurveTween(curve: Curves.easeInBack));
late final cardRotationTween = Tween<double>(
begin: 0,
end: 0.5,
).chain(CurveTween(curve: Curves.easeInBack));
late final cardElevationTween = Tween<double>(
begin: 2,
end: 20,
);

double get cardOffset =>
cardOffsetTween.evaluate(cardOffsetAnimationController);
double get cardRotation =>
cardRotationTween.evaluate(cardOffsetAnimationController);
double get cardElevation =>
cardElevationTween.evaluate(cardOffsetAnimationController);

 

In the initState, add the listener for observing changes

cardOffsetAnimationController.addListener(() => setState(() {}));

Don’t forget to dispose of it when it’s no longer needed

cardOffsetAnimationController.dispose();

Let’s back to our HelloWorldCard and update the values for offset, angle, and elevation. Once these changes are completed, it should look like this 👇

Positioned(
child: Center(
child: Transform.translate(
offset: Offset(0, cardOffset), // Offset updated 
child: Transform.rotate(
angle: cardRotation, // angle updated 
child: Padding(
padding: const EdgeInsets.all(20.0),
child: HelloWorldCard(
size: cardSize,
elevation: cardElevation, // elavetion updated
),
),
),
),
),
),

Returning to the minus button, also start the card animation upon its click

holeAnimationController.forward();
await cardOffsetAnimationController.forward();
Future.delayed(const Duration(milliseconds: 200),
() => holeAnimationController.reverse());

The duration of the cardOffsetAnimationController is set to 1000 milliseconds. After the hole animation finishes, I want it to wait until the card animation is complete before reversing it. That’s why put await before the cardOffsetAnimationController

Last but not least, on the plus button, all we need to do is reverse the animations.

cardOffsetAnimationController.reverse();
holeAnimationController.reverse();
Preview of our progress till now

The wall — CustomClipper

The black hole is displayed and the card animates as expected, yet it doesn’t quite create the effect we’re aiming for. We want to give the impression that the card is moving towards the black hole, and then returning from it. We can get this done with ClipPath and CustomClipper. First, let’s create the CustomClipper, I’m gonna call it BlackHoleClipper.

class BlackHoleClipper extends CustomClipper<Path> {
@override
Path getClip(Size size) {
final path = Path();
path.moveTo(0, size.height / 2);
path.arcTo(
Rect.fromCenter(
center: Offset(size.width / 2, size.height / 2),
width: size.width,
height: size.height,
),
0,
pi,
true,
);
// Using -1000 guarantees the card won't be clipped at the top, regardless of its height
path.lineTo(0, -1000);
path.lineTo(size.width, -1000);
path.close();
return path;
}

@override
bool shouldReclip(BlackHoleClipper oldClipper) => false;
}

 

At TODO: wrap Stack with ClipPath, wrap the Stack widget with ClipPath and assign BlackHoleClipper as the clipper.

....
ClipPath(
clipper: BlackHoleClipper(),
child: Stack( .... ),
)
....
The final preview of the animation

We’ve done it! 🎉 Now it appears precisely as we intended. To my eyes, it looks smooth and satisfying. Here’s the complete source code for reference, or if you encountered any difficulties following my instructions.

If I got something wrong? Let me know in the comments. I would love to improve.

Clap 👏 If this article helps you.

#Learn #Flutter #Animation #Build #Complex #Animations #Simple #Flutter