Creating a rotating card in Jetpack Compose

How to implement a composable that rotates on the horizontal or vertical axis

Introduction

In this short article we’ll learn how to create a Composable card that rotates (or flips) on its axis, to show content on its back. The final result we are looking for is shown in this animation:

Defining our Composable signature

We’ll start by defining our Composable signature. These are the parameters we want to receive that will drive the card behaviour:

  • a flag that indicates if the front or the back is to be shown

  • a callback for when the user taps on the card

  • a Modifier, as is customary, so that our Composable can be customized at its point of use

  • a Composable function that emits the content for the front of the card

  • a Composable function that emits the content for the back of the card

For the flag that indicates which card face to show we could use a boolean, but, while booleans have their valid usages, they can also be somewhat ambiguous as to what true and false mean in some circumstances, so instead we will use an Enum to indicate which face of the card we want to show. This will also allow us to encapsulate some of the card information in the Enum itself, as we will see a bit later.

For the callback, we will notify the card user of clicks on the card, we will use that to trigger the card rotation.

With all that said, let’s see our Enum and the Composable signature:

Our Enum defines the angles we want to use for the 2 card faces, and what the next step is from a given card face.

Displaying the content

Next we are going to display the card with its content. For now there will be no animation, we will just display the front or the back of the card based on the CardFace argument and we will add the animation later.

The way we want this to work is to have the front content displayed until the card reaches the mid point in the flip animation, and at that moment we will switch the content to the back. Because we switch while the card is half way through the rotation (when it is edge on from the user’s point of view and they can only see the edge), the switch will be seamless.

The card rotation will be in degrees, with 0° being the front, and 180° being the back, so we will swap the content at 90°.

For the main Composable we will use a Card Composable, but we could use something else based on what best fits our needs. Let’s flesh out our Composable and display the front or the back based on the input arguments:

This is fairly straightforward, we have a Card as the outermost Composable, then we have a Box that wraps our content. If the angle is 90° or less we show the front, otherwise we show the back.

We can use this card like this:

And this is the result we get so far:

Our content switches when we tap, but as we know, there is no flip animation yet. Let’s fix that.

Adding the flip animation

To animate the card we need to rotate it on its vertical (Y) axis. We need a fire-and-forget animation that runs once. If we look at the chart on the Android Developers webpage on Compose animations here we can see that what we need is an animate*AsState animation. There are several variants of these animations, like animateColorAsState, but there isn’t one specific to rotation, so we will use animateFloatAsState and the animation value will indicate the rotation in degrees for our Card.

So, our animation will be driven by this animate object:

The animateFloatAsState object takes 2 parameters, a targetValue that indicates what value we are animating to, and an animationSpec that determines how we animate to that target value from the current value. In this case our targetValue is the rotation in angles based on the card face we want to show, and the animateSpec is a tween with a duration of 400 milliseconds and an easing of FastOutSlowInEasing — the animation will start fast, then slow down and come to a stop.

One of the nicest things about Compose animations is that they can seamlessly change target values while the animation is running, it all happens under the hood. If our animation is running and rotating the Card from the front to the back (our targetValue is 180f), we can set the targetValue back to 0f and the animation will come back to the front in a natural way.

Now that we have our animation indicating the rotation in degrees for our Card, it’s time to apply that to the Card itself. When animating properties like rotation, translation, scale, alpha, … it is recommended to use a graphicsLayer for better performance, so we will do that here as well. graphicsLayer is a Modifier extension that allows us to specify a range of transformations on the Composable this graphicsLayer is applied to; in our case we are only interested in rotation, so let’s add that our Card:

We have added our animateFloatAsState object to the Composable, then applied a graphicsLayer to the Card Composable based on the value of our animatable object. When we do this, we get this result:

Well, there are a couple of problems with this animation — our back composable is flipped, and the animation is a bit unsightly, the perspective is wrong and the card becomes too large (close to the user) when it’s rotating.

Both of these issues are simple to fix; for the back Composable, it is flipped on its axis because the Card itself is flipped, so to restore the back Composable to its rightful state, we just need to apply an additional rotation of 180°. For the perspective issue, the graphicsLayer Modifier extension offers a cameraDistance that indicates the distance on the Z axis of the plane where the layer is applied, so we just need to specify a value other than the default. You can experiment with different values, but in this case we are going to use 12 dp, which we will convert to pixels as that’s what the value should be in.

With these 2 changes, our Composable is about complete:

Adding additional axis rotation

Now that we have our Composable complete, we could make it a bit more generic by allowing the Card to flip either horizontally (around the Y axis, as it does now), or vertically (around the X axis). For that we would need to add an extra argument to the Card Composable to specify which axis we want to rotate around, and then, based on that, use the rotateX or rotateY properties on the graphicsLayer Modifier. This is a fairly small change, but will make our Card more versatile. Let’s make those changes; the full code is available in this gist.

And this is our final result: