Skip to content

Instantly share code, notes, and snippets.

@remziakgoz
Created June 4, 2025 12:03
Show Gist options
  • Save remziakgoz/2844c1b9c1d20d1a7ef652a4d82d9231 to your computer and use it in GitHub Desktop.
Save remziakgoz/2844c1b9c1d20d1a7ef652a4d82d9231 to your computer and use it in GitHub Desktop.
A Pokéball flip animation that reveals a card using Jetpack Compose

🎴 Flip Pokéball Animation (Jetpack Compose)

A Jetpack Compose animation where a Pokéball splits and reveals a special offer card with Pikachu.

Features

  • Custom Pokéball drawing using Canvas
  • Smooth opening with animated split
  • Animated card reveal using AnimatedVisibility
  • Reverse animation on card click

Built for fun and as a UI animation exercise using modern Android tools.

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.pikachudiscountcard.R
import kotlinx.coroutines.delay
@Composable
fun FlipPokeball() {
var isOpen by remember { mutableStateOf(false) }
var hideBall by remember { mutableStateOf(false) }
var showCard by remember { mutableStateOf(false) }
val angle by animateFloatAsState(
targetValue = if (isOpen) 90f else 0f,
animationSpec = tween(durationMillis = 600),
label = "angleAnim"
)
val ballAlpha by animateFloatAsState(
targetValue = if (hideBall) 0f else 1f,
animationSpec = tween(durationMillis = 600),
label = "alphaAnim"
)
val cardOffset by animateDpAsState(
targetValue = if (hideBall) 0.dp else 60.dp,
animationSpec = tween(durationMillis = 600),
label = "cardOffsetAnim"
)
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Box(modifier = Modifier
.clickable {
if (!isOpen) {
isOpen = true
}
}) {
if (!hideBall) {
Canvas(modifier = Modifier.size(200.dp).graphicsLayer { alpha = ballAlpha }) {
val canvasSize = size.minDimension
val radius = canvasSize / 2
val center = Offset(size.width / 2, size.height / 2)
drawArc(
color = Color.Red,
startAngle = 180f - angle,
sweepAngle = 180f,
useCenter = true,
topLeft = Offset(center.x - radius, center.y - radius),
size = Size(radius * 2, radius * 2)
)
drawArc(
color = Color.White,
startAngle = 0f - angle,
sweepAngle = 180f,
useCenter = true,
topLeft = Offset(center.x - radius, center.y - radius),
size = Size(radius * 2, radius * 2)
)
val angleRad = Math.toRadians(angle.toDouble()).toFloat()
val dx = radius * kotlin.math.cos(angleRad)
val dy = radius * kotlin.math.sin(angleRad)
drawLine(
color = Color.Black,
start = Offset(center.x - dx, center.y + dy),
end = Offset(center.x + dx, center.y - dy),
strokeWidth = 17f
)
drawCircle(
color = Color.White,
radius = radius / 5,
center = center
)
drawCircle(
color = Color.Black,
radius = radius / 5,
center = center,
style = Stroke(width = 17f)
)
}
}
}
LaunchedEffect(isOpen) {
if (isOpen) {
delay(1000)
hideBall = true
}
}
LaunchedEffect(hideBall) {
if (hideBall) {
delay(1)
showCard = true
}
}
if (isOpen) {
Spacer(modifier = Modifier.height(cardOffset))
AnimatedVisibility(
visible = showCard,
enter = fadeIn(tween(500)) + scaleIn(tween(500)),
exit = fadeOut(tween(500)) + scaleOut(tween(500))
) {
Box(modifier = Modifier.clickable {
showCard = false
hideBall = false
isOpen = false
}) {
PikachuCard()
}
}
}
}
}
@Composable
fun PikachuCard() {
Surface(
modifier = Modifier
.width(280.dp)
.height(180.dp)
.clip(RoundedCornerShape(16.dp)),
color = Color.White,
shadowElevation = 4.dp
) {
Row(
modifier = Modifier
.fillMaxSize()
.padding(12.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Image(
painter = painterResource(id = R.drawable.pikachu),
contentDescription = "Pikachu",
contentScale = ContentScale.Fit,
modifier = Modifier.size(100.dp)
)
Column(verticalArrangement = Arrangement.Center) {
Text("SPECIAL OFFER", fontSize = 12.sp, color = Color.Gray)
Text("Pikachu", fontWeight = FontWeight.Bold, fontSize = 18.sp)
Row(verticalAlignment = Alignment.CenterVertically) {
Text("50$", fontSize = 14.sp, color = Color.Gray, textAlign = TextAlign.Start, textDecoration = TextDecoration.LineThrough)
Spacer(modifier = Modifier.width(6.dp))
Text("39$", fontSize = 16.sp, color = Color.Red, fontWeight = FontWeight.Bold)
}
Spacer(modifier = Modifier.height(4.dp))
Button(onClick = {}, colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFFFD700))) {
Text("GET THIS OFFER", color = Color.Black)
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment