Skip to content

Instantly share code, notes, and snippets.

@TuleSimon
Last active March 30, 2026 18:55
Show Gist options
  • Select an option

  • Save TuleSimon/f0ce818d962e01607782345a7da59961 to your computer and use it in GitHub Desktop.

Select an option

Save TuleSimon/f0ce818d962e01607782345a7da59961 to your computer and use it in GitHub Desktop.
Animated Check
package com.anonymous.animatedtoggle
import android.graphics.BlurMaskFilter
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import kotlinx.coroutines.delay
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.asAndroidPath
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.lerp
import com.anonymous.animatedtoggle.ui.theme.AnimatedToggleTheme
import kotlin.math.abs
import kotlin.math.atan2
import kotlin.math.sqrt
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
AnimatedToggleTheme {
var checked by remember { mutableStateOf(false) }
val backgroundColor = animateColorAsState(if(checked) Color(0xFF1A2E4A) else Color(0xFFB8D4E8))
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.fillMaxSize()
.background(backgroundColor.value. copy(alpha = 0.7f))
) {
Toggle(checked = checked, onCheckedChange = { checked = it })
}
}
}
}
}
@Composable
fun Toggle(
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
modifier: Modifier = Modifier
) {
val leadProgress by animateFloatAsState(
targetValue = if (checked) 1f else 0f,
animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy, stiffness = Spring.StiffnessMedium),
label = ""
)
val trailProgress by animateFloatAsState(
targetValue = if (checked) 1f else 0f,
animationSpec = tween(durationMillis = 480, delayMillis = 160, easing = FastOutSlowInEasing),
label = ""
)
val trackColor by animateColorAsState(
targetValue = if (checked) Color(0xFF1A2E4A) else Color(0xFFB8D4E8),
animationSpec = tween(400),
label = ""
)
val squish = remember { Animatable(0f) }
LaunchedEffect(checked) {
delay(300)
squish.snapTo(1f)
squish.animateTo(
targetValue = 0f,
animationSpec = spring(
dampingRatio = Spring.DampingRatioLowBouncy,
stiffness = Spring.StiffnessMediumLow
)
)
}
Box(
modifier = modifier
.size(130.dp, 54.dp)
.pointerInput(checked) { detectTapGestures { onCheckedChange(!checked) } }
.drawBehind {
val corner = size.height / 2f
drawRoundRect(color = trackColor, cornerRadius = CornerRadius(corner))
val r = corner - 5.dp.toPx()
val onCenter = size.width - corner
val leadX = lerp(corner, onCenter, leadProgress)
val trailX = lerp(corner, onCenter, trailProgress)
drawThumb(leadX, trailX, size.height / 2f, r, Color(0xFFF0F8FF), squish.value)
}
)
}
private fun DrawScope.drawThumb(leadX: Float, trailX: Float, cy: Float, r: Float, color: Color, squish: Float = 0f) {
val dist = abs(leadX - trailX)
if (dist < 0.5f) {
val rx = r * (1f + squish * 0.4f)
val ry = r * (1f - squish * 0.25f)
drawOval(color, topLeft = Offset(leadX - rx, cy - ry), size = Size(rx * 2, ry * 2))
return
}
val stretch = (dist / (size.width - r * 4f)).coerceIn(0f, 1f)
taperedPill(
bigX = leadX,
smallX = trailX,
cy = cy,
r1 = r,
r2 = r * (1f - stretch * 0.95f),
color = color,
blurStretch = stretch
)
}
private fun DrawScope.taperedPill(
bigX: Float, smallX: Float, cy: Float, r1: Float, r2: Float, color: Color,
blurStretch: Float = 0f
) {
val d = smallX - bigX
val dist = abs(d)
val sgnD = if (d >= 0f) 1f else -1f
val sinA = ((r1 - r2) / dist).coerceIn(0f, 1f)
val cosA = sqrt((1f - sinA * sinA).coerceAtLeast(0f))
val upperBig = Offset(bigX + sgnD * r1 * sinA, cy - r1 * cosA)
val lowerBig = Offset(bigX + sgnD * r1 * sinA, cy + r1 * cosA)
val upperSmall = Offset(smallX + sgnD * r2 * sinA, cy - r2 * cosA)
val lowerSmall = Offset(smallX + sgnD * r2 * sinA, cy + r2 * cosA)
fun angleOf(p: Offset, cx: Float) =
(Math.toDegrees(atan2((p.y - cy).toDouble(), (p.x - cx).toDouble()))
.toFloat() + 360f) % 360f
val bigLowerAngle = angleOf(lowerBig, bigX)
val bigUpperAngle = angleOf(upperBig, bigX)
val smallUpperAngle = angleOf(upperSmall, smallX)
val smallLowerAngle = angleOf(lowerSmall, smallX)
val bigSweep = sgnD * ((sgnD * (bigUpperAngle - bigLowerAngle) + 360f) % 360f)
val smallSweep = sgnD * ((sgnD * (smallLowerAngle - smallUpperAngle) + 360f) % 360f)
val path = Path()
path.moveTo(upperBig.x, upperBig.y)
path.lineTo(upperSmall.x, upperSmall.y)
path.arcTo(Rect(smallX - r2, cy - r2, smallX + r2, cy + r2), smallUpperAngle, smallSweep, false)
path.lineTo(lowerBig.x, lowerBig.y)
path.arcTo(Rect(bigX - r1, cy - r1, bigX + r1, cy + r1), bigLowerAngle, bigSweep, false)
path.close()
if (blurStretch > 0.01f) {
drawIntoCanvas { canvas ->
val nativePaint = android.graphics.Paint().apply {
isAntiAlias = true
maskFilter = BlurMaskFilter(blurStretch * 12.dp.toPx(), BlurMaskFilter.Blur.NORMAL)
setColor(
android.graphics.Color.argb(
(255 * 0.4f * blurStretch).toInt(),
(color.red * 255).toInt(),
(color.green * 255).toInt(),
(color.blue * 255).toInt()
)
)
}
canvas.nativeCanvas.drawPath(path.asAndroidPath(), nativePaint)
}
}
drawPath(path, color)
}
@Preview(showBackground = true, backgroundColor = 0xFF7AB5D4)
@Composable
private fun Preview() {
var checked by remember { mutableStateOf(false) }
Toggle(checked = checked, onCheckedChange = { checked = it })
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment