-
-
Save L10n42/195738fb1a200fe1253aa9045ab616ab to your computer and use it in GitHub Desktop.
Custom Circle Loader in Jetpack Compose
This file contains 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.core.Animatable | |
import androidx.compose.animation.core.LinearEasing | |
import androidx.compose.animation.core.RepeatMode | |
import androidx.compose.animation.core.animateFloat | |
import androidx.compose.animation.core.infiniteRepeatable | |
import androidx.compose.animation.core.rememberInfiniteTransition | |
import androidx.compose.animation.core.tween | |
import androidx.compose.foundation.Canvas | |
import androidx.compose.foundation.layout.aspectRatio | |
import androidx.compose.runtime.Composable | |
import androidx.compose.runtime.LaunchedEffect | |
import androidx.compose.runtime.getValue | |
import androidx.compose.runtime.remember | |
import androidx.compose.ui.Modifier | |
import androidx.compose.ui.draw.rotate | |
import androidx.compose.ui.geometry.toRect | |
import androidx.compose.ui.graphics.Brush | |
import androidx.compose.ui.graphics.Color | |
import androidx.compose.ui.graphics.Paint | |
import androidx.compose.ui.graphics.PaintingStyle | |
import androidx.compose.ui.graphics.StrokeCap | |
import androidx.compose.ui.graphics.drawscope.DrawScope | |
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas | |
import androidx.compose.ui.graphics.drawscope.rotate | |
import androidx.compose.ui.unit.Dp | |
import androidx.compose.ui.unit.dp | |
/** | |
* Data class representing the style configuration for the stroke of the CircleLoader. | |
* | |
* @property width The width of the loader's stroke. | |
* @property strokeCap The cap style for the stroke. | |
* @property glowRadius Optional radius for adding a glow effect to the stroke. | |
*/ | |
data class StrokeStyle( | |
val width: Dp = 4.dp, | |
val strokeCap: StrokeCap = StrokeCap.Round, | |
val glowRadius: Dp? = 4.dp | |
) | |
/** | |
* Composable function to render a circular loading indicator. | |
* | |
* @param modifier Modifier for the composable. | |
* @param isVisible Determines whether the loader is visible or not. | |
* @param color The primary color of the loader. | |
* @param secondColor The secondary color of the loader. | |
* @param tailLength Length of the loader's tail in degrees. | |
* @param smoothTransition Indicates whether the transition between tail lengths should be smooth or immediate. | |
* @param strokeStyle Style configuration for the stroke of the loader. | |
* @param cycleDuration Duration of a single cycle of the loader animation in milliseconds. | |
*/ | |
@Composable | |
fun CircleLoader( | |
modifier: Modifier, | |
isVisible: Boolean, | |
color: Color, | |
secondColor: Color? = color, | |
tailLength: Float = 140f, | |
smoothTransition: Boolean = true, | |
strokeStyle: StrokeStyle = StrokeStyle(), | |
cycleDuration: Int = 1400, | |
) { | |
val tailToDisplay = remember { Animatable(0f) } | |
LaunchedEffect(isVisible) { | |
val targetTail = if (isVisible) tailLength else 0f | |
when { | |
smoothTransition -> tailToDisplay.animateTo( | |
targetValue = targetTail, | |
animationSpec = tween(cycleDuration, easing = LinearEasing) | |
) | |
else -> tailToDisplay.snapTo(targetTail) | |
} | |
} | |
val transition = rememberInfiniteTransition() | |
val spinAngel by transition.animateFloat( | |
initialValue = 0f, | |
targetValue = 360f, | |
animationSpec = infiniteRepeatable( | |
animation = tween( | |
durationMillis = cycleDuration, | |
easing = LinearEasing | |
), | |
repeatMode = RepeatMode.Restart | |
) | |
) | |
Canvas( | |
modifier | |
.rotate(spinAngel) | |
.aspectRatio(1f) | |
) { | |
listOfNotNull(color, secondColor).forEachIndexed { index, color -> | |
rotate(if (index == 0) 0f else 180f) { | |
val brush = Brush.sweepGradient( | |
0f to Color.Transparent, | |
tailToDisplay.value / 360f to color, | |
1f to Color.Transparent | |
) | |
val paint = setupPaint(strokeStyle, brush) | |
drawIntoCanvas { canvas -> | |
canvas.drawArc( | |
rect = size.toRect(), | |
startAngle = 0f, | |
sweepAngle = tailToDisplay.value, | |
useCenter = false, | |
paint = paint | |
) | |
} | |
} | |
} | |
} | |
} | |
/** | |
* Sets up and configures the Paint object for drawing the loader. | |
* | |
* @param style Style configuration for the stroke. | |
* @param brush Brush configuration for the paint. | |
* @return Paint object configured with the provided stroke style and brush. | |
*/ | |
private fun DrawScope.setupPaint(style: StrokeStyle, brush: Brush): Paint { | |
val paint = Paint().apply paint@{ | |
this@paint.isAntiAlias = true | |
this@paint.style = PaintingStyle.Stroke | |
this@paint.strokeWidth = style.width.toPx() | |
this@paint.strokeCap = style.strokeCap | |
brush.applyTo(size, this@paint, 1f) | |
} | |
style.glowRadius?.let { radius -> | |
paint.asFrameworkPaint().setShadowLayer( | |
/* radius = */ radius.toPx(), | |
/* dx = */ 0f, | |
/* dy = */ 0f, | |
/* shadowColor = */ android.graphics.Color.WHITE | |
) | |
} | |
return paint | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment