Created
September 26, 2025 14:53
-
-
Save Kyriakos-Georgiopoulos/8c452daa1fd21c303b14f117c73deb48 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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