Created
October 11, 2025 16:58
-
-
Save iprashantpanwar/26f9c763b14dfc81c200ab1b1535de7b to your computer and use it in GitHub Desktop.
A Continues Ripple animation using Jetpack Compose
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
| @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) | |
| } | |
| } | |
| } |
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
| @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