Skip to content

Instantly share code, notes, and snippets.

@ardakazanci
Created May 30, 2025 17:14
Show Gist options
  • Save ardakazanci/9f122ebc15e2d99939624b53c49a74da to your computer and use it in GitHub Desktop.
Save ardakazanci/9f122ebc15e2d99939624b53c49a74da to your computer and use it in GitHub Desktop.
Stepper with Jetpack Compose
@Composable
fun Stepper(
modifier: Modifier = Modifier,
initialValue: Int = 16,
onValueChange: (Int) -> Unit = {}
) {
var value by remember { mutableIntStateOf(initialValue) }
var dragOffset by remember { mutableFloatStateOf(0f) }
var isDragging by remember { mutableStateOf(false) }
val thresholdPx = with(LocalDensity.current) {
StepperDefaults.ThresholdDp.toPx()
}
val animatedOffset by animateFloatAsState(
targetValue = if (isDragging) dragOffset else 0f,
animationSpec = spring(
dampingRatio = StepperDefaults.SpringDamping,
stiffness = StepperDefaults.SpringStiffness
)
)
val iconRotation by animateFloatAsState(
targetValue = if (isDragging) {
(dragOffset / thresholdPx * StepperDefaults.MaxIconRotation)
.coerceIn(-StepperDefaults.MaxIconRotation, StepperDefaults.MaxIconRotation)
} else 0f,
animationSpec = spring()
)
Box(
modifier
.size(StepperDefaults.PillWidth, StepperDefaults.PillHeight)
.pointerInput(Unit) {
detectDragGestures(
onDragStart = { isDragging = true },
onDrag = { ch, delta ->
if (ch.positionChange() != Offset.Zero) ch.consume()
dragOffset += delta.y
},
onDragEnd = {
when {
dragOffset <= -thresholdPx -> {
value++; onValueChange(value)
}
dragOffset >= thresholdPx -> {
value--; onValueChange(value)
}
}
dragOffset = 0f
isDragging = false
}
)
}
.graphicsLayer {
transformOrigin = TransformOrigin(0f, 0.5f)
rotationZ = animatedOffset / StepperDefaults.RotationDivider
}
.shadow(
elevation = StepperDefaults.ShadowElevation,
shape = RoundedCornerShape(StepperDefaults.CornerRadius),
clip = false
)
.background(
brush = Brush.horizontalGradient(
listOf(StepperDefaults.GradientStart, StepperDefaults.GradientEnd)
)
)
) {
Row(
Modifier
.fillMaxSize()
.padding(horizontal = StepperDefaults.HorizontalPadding),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
AnimatedContent(
targetState = value,
transitionSpec = {
if (targetState > initialState) {
slideInVertically { it } + fadeIn() togetherWith
slideOutVertically { -it } + fadeOut()
} else {
slideInVertically { -it } + fadeIn() togetherWith
slideOutVertically { it } + fadeOut()
}.using(SizeTransform(clip = false))
}
) { target ->
Text(
text = target.toString(),
color = Color.White,
fontSize = 32.sp,
fontWeight = FontWeight.Bold
)
}
Icon(
imageVector = Icons.Default.ThumbUp,
contentDescription = null,
modifier = Modifier
.size(StepperDefaults.IconSize)
.graphicsLayer { rotationZ = iconRotation },
tint = StepperDefaults.IconTint
)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment