Skip to content

Instantly share code, notes, and snippets.

@iprashantpanwar
Created October 11, 2025 16:58
Show Gist options
  • Select an option

  • Save iprashantpanwar/26f9c763b14dfc81c200ab1b1535de7b to your computer and use it in GitHub Desktop.

Select an option

Save iprashantpanwar/26f9c763b14dfc81c200ab1b1535de7b to your computer and use it in GitHub Desktop.
A Continues Ripple animation using Jetpack Compose
@Composable
fun ContinuousRipple(
modifier: Modifier = Modifier,
backgroundColor: Color = Color(0xFFF0F0F3),
rings: Int = 3,
durationMillis: Int = 10000
) {
val infinite = rememberInfiniteTransition()
val clock = infinite.animateFloat(
initialValue = 0f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = durationMillis, easing = LinearEasing),
repeatMode = RepeatMode.Restart
)
)
Canvas(modifier = modifier.background(backgroundColor)) {
val cx = size.width / 2f
val cy = size.height / 2f
val maxRadius = hypot(size.width, size.height) / 2f * 1.2f // flow outside bounds
val baseStroke = min(size.width, size.height) / 14f
drawIntoCanvas { canvas ->
val native = canvas.nativeCanvas
val offsetPx = 6f
val blurRadius = 20f
val highlightColor = android.graphics.Color.WHITE
val shadowColor = "#AEAEC0".toColorInt()
repeat(rings) { index ->
// Each ripple is just the same clock, shifted in phase
val phaseShift = 1f / rings * index
val progress = (clock.value + phaseShift) % 1f
val radius = lerp(0f, maxRadius, progress)
val stroke = baseStroke * (1f - progress * 0.7f)
val alpha = (1f - progress).coerceIn(0f, 1f)
// Highlight ring
val highlightPaint = Paint().apply {
isAntiAlias = true
style = Paint.Style.STROKE
strokeWidth = stroke
color = highlightColor
this.alpha = (alpha * 255).toInt()
maskFilter = BlurMaskFilter(blurRadius, BlurMaskFilter.Blur.NORMAL)
}
native.drawCircle(cx - offsetPx, cy - offsetPx, radius, highlightPaint)
// Shadow ring
val shadowPaint = Paint().apply {
isAntiAlias = true
style = Paint.Style.STROKE
strokeWidth = stroke
color = shadowColor
this.alpha = (alpha * 255 * 0.25f).toInt()
maskFilter = BlurMaskFilter(blurRadius, BlurMaskFilter.Blur.NORMAL)
}
native.drawCircle(cx + offsetPx, cy + offsetPx, radius, shadowPaint)
}
// center cutout (static)
val ringSpacing = baseStroke * 2f
val cutoutRadius = maxRadius - rings * ringSpacing + (ringSpacing / 2f)
val fillPaint = Paint().apply {
isAntiAlias = true
style = Paint.Style.STROKE
color = backgroundColor.toArgb()
}
native.drawCircle(cx, cy, cutoutRadius, fillPaint)
}
}
}
@Composable
fun StaggedRipple(
modifier: Modifier = Modifier,
backgroundColor: Color = Color(0xFFF0F0F3),
rings: Int = 3,
durationMillis: Int = 4000
) {
val infiniteTransition = rememberInfiniteTransition()
// Animate multiple rings, each with a delayed offset
val progresses = List(rings) { index ->
infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = durationMillis,
easing = LinearEasing,
delayMillis = (durationMillis / rings) * index
),
repeatMode = RepeatMode.Restart,
initialStartOffset = StartOffset(index * durationMillis / rings) // stagger
)
)
}
Canvas(modifier = modifier.background(backgroundColor)) {
val cx = size.width / 2f
val cy = size.height / 2f
val maxRadius = hypot(size.width, size.height) / 2f // expand beyond bounds
val baseStroke = min(size.width, size.height) / 14f
drawIntoCanvas { canvas ->
val native = canvas.nativeCanvas
val offsetPx = 6f
val blurRadius = 20f
val highlightColor = android.graphics.Color.WHITE
val shadowColor = "#AEAEC0".toColorInt()
progresses.forEach { anim ->
val progress = anim.value
// Animate radius from near 0 → max
val radius = lerp(0f, maxRadius, progress)
val stroke = baseStroke * (1f - progress * 0.7f) // shrink stroke as it grows
val alpha = (1f - progress).coerceIn(0f, 1f) // fade alpha as it grows
// highlight
val highlightPaint = Paint().apply {
isAntiAlias = true
style = Paint.Style.STROKE
strokeWidth = stroke
color = highlightColor
this.alpha = (alpha * 255).toInt()
maskFilter = BlurMaskFilter(blurRadius, BlurMaskFilter.Blur.NORMAL)
}
native.drawCircle(cx - offsetPx, cy - offsetPx, radius, highlightPaint)
// shadow
val shadowPaint = Paint().apply {
isAntiAlias = true
style = Paint.Style.STROKE
strokeWidth = stroke
color = shadowColor
this.alpha = (alpha * 255 * 0.25f).toInt()
maskFilter = BlurMaskFilter(blurRadius, BlurMaskFilter.Blur.NORMAL)
}
native.drawCircle(cx + offsetPx, cy + offsetPx, radius, shadowPaint)
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment