Skip to content

Instantly share code, notes, and snippets.

@Kyriakos-Georgiopoulos
Created September 26, 2025 14:53
Show Gist options
  • Save Kyriakos-Georgiopoulos/8c452daa1fd21c303b14f117c73deb48 to your computer and use it in GitHub Desktop.
Save Kyriakos-Georgiopoulos/8c452daa1fd21c303b14f117c73deb48 to your computer and use it in GitHub Desktop.
import androidx.compose.animation.animateColor
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateDp
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
enum class ButtonState { Idle, Loading, Success }
@Composable
fun PrimaryActionButton(
modifier: Modifier = Modifier,
state: ButtonState = ButtonState.Idle,
onClick: () -> Unit = {},
idleLabel: String = "Add to Cart"
) {
val transition = updateTransition(targetState = state, label = "PrimaryActionTransition")
val width by transition.animateDp(
label = "width",
transitionSpec = { tween(350, easing = FastOutSlowInEasing) }
) { s ->
when (s) {
ButtonState.Idle -> 280.dp
ButtonState.Loading -> 66.dp
ButtonState.Success -> 200.dp
}
}
val corner by transition.animateDp(
label = "corner",
transitionSpec = { tween(350, easing = FastOutSlowInEasing) }
) { s ->
when (s) {
ButtonState.Idle -> 16.dp
ButtonState.Loading -> 32.dp
ButtonState.Success -> 20.dp
}
}
val bg by transition.animateColor(
label = "bg",
transitionSpec = { tween(350) }
) { s ->
when (s) {
ButtonState.Idle -> Color(0xFF5B86E5)
ButtonState.Loading -> Color(0xFF2A2F3A)
ButtonState.Success -> Color(0xFF2BB673)
}
}
val textAlpha by transition.animateFloat(
label = "textAlpha",
transitionSpec = { tween(180, delayMillis = 120, easing = LinearOutSlowInEasing) }
) { s -> if (s == ButtonState.Idle) 1f else 0f }
val spinnerAlpha by transition.animateFloat(
label = "spinnerAlpha",
transitionSpec = { tween(120, easing = LinearOutSlowInEasing) }
) { s -> if (s == ButtonState.Loading) 1f else 0f }
val checkAlpha by transition.animateFloat(
label = "checkAlpha",
transitionSpec = { tween(160, delayMillis = 120) }
) { s -> if (s == ButtonState.Success) 1f else 0f }
val checkScale by transition.animateFloat(
label = "checkScale",
transitionSpec = {
spring(
dampingRatio = Spring.DampingRatioLowBouncy,
stiffness = Spring.StiffnessLow
)
}
) { s -> if (s == ButtonState.Success) 1.15f else 0.8f }
val checkTilt by transition.animateFloat(
label = "checkTilt",
transitionSpec = {
spring(
dampingRatio = Spring.DampingRatioLowBouncy,
stiffness = Spring.StiffnessMediumLow
)
}
) { s -> if (s == ButtonState.Success) 4f else 0f }
val contentPad by transition.animateDp(
label = "contentPad",
transitionSpec = { tween(250) }
) { s ->
when (s) {
ButtonState.Idle -> 14.dp
ButtonState.Loading -> 12.dp
ButtonState.Success -> 16.dp
}
}
Box(
modifier = modifier
.width(width)
.height(56.dp)
.clip(RoundedCornerShape(corner))
.background(bg)
.clickable(enabled = state == ButtonState.Idle) { onClick() }
.padding(horizontal = contentPad),
contentAlignment = Alignment.Center
) {
if (textAlpha > 0f) {
Text(
text = idleLabel,
color = Color.White.copy(alpha = textAlpha),
style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.SemiBold),
modifier = Modifier.alpha(textAlpha)
)
}
if (spinnerAlpha > 0f) {
CircularProgressIndicator(
color = Color.White.copy(alpha = spinnerAlpha),
strokeWidth = 3.dp,
modifier = Modifier
.size(28.dp)
.alpha(spinnerAlpha)
)
}
if (checkAlpha > 0f) {
Text(
text = "✓",
color = Color.White.copy(alpha = checkAlpha),
style = MaterialTheme.typography.titleLarge,
modifier = Modifier
.alpha(checkAlpha)
.scale(checkScale)
.rotate(checkTilt)
)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment