How to Create a Coin Flipper Animation in Jetpack Compose
Kappdev on 2023-12-11
How to Create a Coin Flipper Animation in Jetpack Compose
Welcome 👋
In this article, I’ll demonstrate how to create a beautiful Coin Flipper Animation in Jetpack Compose. While its practical utility might seem limited, the scope for creativity 🌟 is boundless.
Let’s dive in together and uncover the possibilities!

Preparation
Before we start building this beautiful view, we need to define some utilities that we will use later on.
Quarter calculation
The determineQuarter
function takes a degree
of your rotate animation and returns its quarter which we will use to display an appropriate coin side.
fun determineQuarter(degree: Float): Int { // Normalize the degree value within the range 0 to 359 degrees val normalizedDegree = (degree + 360) % 360 return when { normalizedDegree <= 90f -> 1 normalizedDegree <= 180f -> 2 normalizedDegree <= 270f -> 3 else -> 4 } }
Custom jump animation
The jumpAnimation
function creates a keyframe animation that simulates a jump with a quick ascent and a slower descent.
fun jumpAnimation(duration: Int, jumpTarget: Float) = keyframes { durationMillis = duration 0f at 0 jumpTarget at (duration * 0.3f).toInt() 0f at duration }
Reflected view
Since the back side of the coin will be reflected by rotation, we need to undo the reflection. I have created the ReflectVertically
function for this purpose.
@Composable fun BoxScope.ReflectVertically( content: @Composable BoxScope.() -> Unit ) { Box( modifier = Modifier .matchParentSize() .graphicsLayer { rotationX = 180f }, content = content ) }
Fine! Now we’re ready to move on. 😊
Crafting the view
Function Signature and Parameters:
Let’s start by defining the function and its parameters.
@Composable fun CoinFlipper( modifier: Modifier = Modifier, duration: Int = 2_000, maxJumpHeight: Dp = 100.dp, forceRange: IntRange = (5..20), frontSide: @Composable BoxScope.(isRunning: Boolean, flip: () -> Unit) -> Unit, backSide: @Composable BoxScope.(isRunning: Boolean, flip: () -> Unit) -> Unit )
duration
: Represents the duration of the entire animation in milliseconds.maxJumpHeight
: Denotes the maximum height the coin might reach during the simulated jump animation.forceRange
: Defines the range of force applied to the coin, determining the intensity of the flip.frontSide
andbackSide
: Composable functions representing the UI of the coin's sides. It receives parameters indicating the animation state and a function to trigger the flip.
Before diving into the animation logic, it’s crucial to ensure the provided input parameters are valid. We use the require
function to validate the duration and force range parameters:
require(duration > 0) { "Duration should be a positive value." } require(forceRange.first >= 0 && forceRange.last >= forceRange.first) { "Invalid force range." }
Following the validation, we initialize the density
variable for precise calculations. Additionally, we create a coroutine scope
using rememberCoroutineScope()
.
val density = LocalDensity.current val scope = rememberCoroutineScope()
Animation Logic:
Let’s delve into the implementation of our animation logic:
// Define animatable values for rotation and translation val rotationAnim = remember { Animatable(0f) } val translationAnim = remember { Animatable(0f) } // Calculate the jump height in pixels val jumpHeight = remember(maxJumpHeight) { with(density) { maxJumpHeight.toPx() } } // Calculate the jump step based on jump height and force range val jumpStep = remember(jumpHeight, forceRange) { (jumpHeight / forceRange.last) } // Check if any animation is currently running val isRunning = remember(rotationAnim.isRunning, translationAnim.isRunning) { (rotationAnim.isRunning || translationAnim.isRunning) } // Lambda function initiating the flip animation val performFlip = { if (!isRunning) { // Calculate force and set rotation and translation targets val force = forceRange.random() val rotationTarget = rotationAnim.value + (force * 3) * 180f val translationTarget = (force * jumpStep) // Launch the animations scope.launch { rotationAnim.animateTo( targetValue = rotationTarget, animationSpec = tween(duration, easing = FastOutSlowInEasing) ) // Reset the rotation value once the animation completes rotationAnim.snapTo(rotationAnim.value % 360f) } scope.launch { translationAnim.animateTo( targetValue = 0f, animationSpec = jumpAnimation(duration, -translationTarget) ) } } }
UI Composition:
To put the final touch, let's craft the UI.
Box( modifier = modifier.graphicsLayer { rotationX = rotationAnim.value translationY = translationAnim.value cameraDistance = (10f * density) } ) { // Determine the quarter and display coin sides accordingly val quarter = determineQuarter(rotationAnim.value) if (quarter == 1 || quarter == 4) { // Display the front side of the coin frontSide(isRunning, performFlip) } else { // Display the back side of the coin with vertical reflection ReflectVertically { backSide(isRunning, performFlip) } } }
Congratulations🥳! We’ve successfully built it👏. For the complete code implementation, you can access it on GitHub Gist. In the next section, we’ll explore an example of how to utilize this animation.
Example
Here we’ll make a simple coin with the T symbol on one side and F on the other.
Note: To enhance the visual appeal of the coin, I’ll use the
innerShadow
modifier in this example. For a detailed explanation, refer to my related article provided below or find the code on the GitHub Gist
Inner Shadow in Jetpack Compose Welcome! In this article, I’ll demonstrate how to create an inner shadow modifier in Jetpack Compose.medium.com
Now, let's use the GenericShape
to create the T and F symbols as a shape
.
val TShape = GenericShape { size, _ -> val height = size.height val width = size.width val heightTenth = (height / 10) val widthTenth = (width / 10) moveTo((widthTenth * 3), (heightTenth * 2.5f)) lineTo(width - (widthTenth * 3), (heightTenth * 2.5f)) lineTo(width - (widthTenth * 3), (heightTenth * 3.5f)) lineTo((width / 2) + (widthTenth / 2), (heightTenth * 3.5f)) lineTo((width / 2) + (widthTenth / 2), (height - (heightTenth * 2.5f))) lineTo((width / 2) - (widthTenth / 2), (height - (heightTenth * 2.5f))) lineTo((width / 2) - (widthTenth / 2), (heightTenth * 3.5f)) lineTo((widthTenth * 3), (heightTenth * 3.5f)) close() }
val FShape = GenericShape { size, _ -> val height = size.height val width = size.width val heightTenth = (height / 10) val widthTenth = (width / 10) moveTo((widthTenth * 3), (heightTenth * 2.5f)) lineTo(width - (widthTenth * 3), (heightTenth * 2.5f)) lineTo(width - (widthTenth * 3), (heightTenth * 3.5f)) lineTo((widthTenth * 4), (heightTenth * 3.5f)) lineTo((widthTenth * 4), (heightTenth * 4.5f)) lineTo(width - (widthTenth * 3), (heightTenth * 4.5f)) lineTo(width - (widthTenth * 3), (heightTenth * 5.5f)) lineTo((widthTenth * 4), (heightTenth * 5.5f)) lineTo((widthTenth * 4), height - (heightTenth * 2.5f)) lineTo((widthTenth * 3), height - (heightTenth * 2.5f)) close() }
Also, here are the colors I’ll use:
val ShadowBlack = Color.Black.copy(0.8f) val ShadowWhite = Color.White.copy(0.8f) val LightShadowBlack = Color.Black.copy(0.36f) val LightShadowWhite = Color.White.copy(0.36f) val Gold = Color(0xFFFFD700) val Violet = Color(0xFF5c5174)
Alright! Let's create the Coin
function. There's also a small scale animation when you click on it.
@Composable fun BoxScope.Coin( symbolShape: Shape, enabled: Boolean, onClick: () -> Unit ) { val interSource = remember { MutableInteractionSource() } // Collect the pressed state using interaction source val pressed by interSource.collectIsPressedAsState() // Animate the scale of the coin based on pressed state val coinScale by animateFloatAsState( targetValue = if (pressed) 1.15f else 1f, animationSpec = spring(dampingRatio = Spring.DampingRatioHighBouncy), label = "Coin Scale Animation" ) Box( modifier = Modifier .matchParentSize() .scale(coinScale) // Scale the coin .background(Color.Yellow, CircleShape) // Apply inner shadows for a convex effect .innerShadow( shape = CircleShape, color = ShadowBlack, offsetY = (-2).dp, offsetX = (-2).dp ) .innerShadow( shape = CircleShape, color = ShadowWhite, offsetY = 2.dp, offsetX = 2.dp ) .clickable( interactionSource = interSource, indication = null, enabled = enabled, onClick = onClick ) ) { Canvas( modifier = Modifier .matchParentSize() // Apply inner shadows for a concave effect .innerShadow( shape = symbolShape, color = LightShadowWhite, blur = 2.dp, offsetY = (-1).dp, offsetX = (-1).dp ) .innerShadow( shape = symbolShape, color = LightShadowBlack, blur = 2.dp, offsetY = 1.dp, offsetX = 1.dp ) ) { // Draw the symbol val outline = symbolShape.createOutline(size, layoutDirection, this) drawOutline(outline, Gold) } } }
At last, let’s create the CoinFlipper!
CoinFlipper( modifier = Modifier.size(100.dp), maxJumpHeight = 400.dp, frontSide = { isRunning, flip -> Coin( symbolShape = TShape, enabled = !isRunning, onClick = flip ) }, backSide = { isRunning, flip -> Coin( symbolShape = FShape, enabled = !isRunning, onClick = flip ) } )
🎉 Amazing! Take a look at the output:

Thank you for reading this article! ❤️ If you found it enjoyable and valuable, show your appreciation by clapping 👏 and following Kappdev for more exciting articles 😊

🪙Wobbly Coin Animation in Jetpack Compose Welcome 👋! In this article, we’ll create a Wobbly Coin Animation with Jetpack Compose. This means the coin (or any…medium.com
