Welcome 👋
In this article, we’ll create a stunning Animated Infinity Loader with Jetpack Compose.
Let’s dive in! 🚀
Setting Up Customization
Before building our infinity loader function, let’s introduce the Glow
data class. This class enables a glowing effect on the loader path, offering customizable options:
data class Glow(
val radius: Dp = 8.dp, // Controls glow size
val xShifting: Dp = 0.dp, // Adjusts horizontal position
val yShifting: Dp = 0.dp // Adjusts vertical position
)
Defining the Function and Parameters
Let’s start by introducing the InfinityLoader
composable function and go through its parameters:
@Composable
fun InfinityLoader(
modifier: Modifier,
brush: Brush,
duration: Int = 3_000,
strokeWidth: Dp = 4.dp,
strokeCap: StrokeCap = StrokeCap.Round,
glow: Glow? = null,
placeholderColor: Color? = null,
)
modifier
👉 Adjusts the appearance and layout of the loader.brush
👉 Defines the painting style of the loader.duration
👉 Sets the time for one complete cycle of the loader.strokeWidth
👉 Determines the thickness of the loader's line.strokeCap
👉 Defines the style of the ends of the loader's line.glow
👉 An optional parameter that introduces a luminous, glowing effect to enhance the appearance of the loader.placeholderColor
👉 An optional parameter that designates a visible color for areas where the loader animation isn’t active.
Explaining the Implementation
In this section, we’ll dive into function implementation. We’ll break the code into bite-sized functions to enhance readability and understanding and then put it all together.
Create Path
This function constructs an infinity symbol path based on the provided width
and height
. It uses cubic Bézier curves to draw the right and left sides of the symbol.
fun createPath(width: Float, height: Float): Path {
return Path().apply {
// Move to Center(c)
moveTo((width / 2), (height / 2))
// Draw the right side
cubicTo(
x1 = width, y1 = 0f, // 1
x2 = width, y2 = height, // 2
x3 = (width / 2), (height / 2) // Center(c)
)
// Draw the left side
cubicTo(
x1 = 0f, y1 = 0f, // 3
x2 = 0f, y2 = height, // 4
x3 = (width / 2), (height / 2) // Center(c)
)
}
}
Paht Segment
This function implements the core logic of the animation by determining a segment of the path based on the completion of the animation.
fun calculatePathSegment(path: Path, pathCompletion: Float): Path {
// Create a PathMeasure instance for the given path
val pathMeasure = PathMeasure().apply {
setPath(path, false)
}
// Create a new Path to store the segment
val pathSegment = Path()
// Calculate the distance to stop drawing the path segment
val stopDistance = when {
(pathCompletion < 1f) -> (pathCompletion * pathMeasure.length)
else -> pathMeasure.length
}
// Calculate the distance to start drawing the path segment
val startDistance = when {
(pathCompletion > 1f) -> ((pathCompletion - 1f) * pathMeasure.length)
else -> 0f
}
// Retrieve the segment of the path based on start and stop distances
pathMeasure.getSegment(startDistance, stopDistance, pathSegment, true)
return pathSegment
}
If you’re curious about the use of <1f
and >1f
logic, it’s because the animation value spans from 0f
to 2f
. Between 0f
and 1f
, we animate the head of the path segment, and as it progresses from 1f
to 2f
, we animate the tail. This concept will become clearer when you examine the picture:
Setup Paint
This function configures a Paint
object for drawing based on the provided parameters.
// Extend the DrawScope to utilize 'toPx()' and access the Canvas size
fun DrawScope.setupPaint(
strokeWidth: Dp,
strokeCap: StrokeCap,
brush: Brush,
): Paint {
return Paint().apply paint@{
// Set anti-aliasing for smoother edges
this@paint.isAntiAlias = true
// Set the painting style to Stroke (outline)
this@paint.style = PaintingStyle.Stroke
// Set the stroke width by converting from Dp to pixels
this@paint.strokeWidth = strokeWidth.toPx()
// Set the stroke cap style
this@paint.strokeCap = strokeCap
// Apply the brush to the paint
brush.applyTo(size, this@paint, 1f)
}
}
Apply Glow
This is an extension function for the Glow
class that applies it to a provided Paint
object.
fun Glow.applyToPaint(paint: Paint, density: Density) = with(density) {
val frameworkPaint = paint.asFrameworkPaint()
frameworkPaint.setShadowLayer(
/* radius = */ radius.toPx(),
/* dx = */ xShifting.toPx(),
/* dy = */ yShifting.toPx(),
/* shadowColor = */ android.graphics.Color.WHITE
)
}
Draw Placeholder
This function extends DrawScope
and draws a placeholder for the path with specified parameters.
fun DrawScope.drawPathPlaceholder(
path: Path,
strokeWidth: Dp,
strokeCap: StrokeCap,
placeholderColor: Color
) {
drawPath(
path = path,
color = placeholderColor,
style = Stroke(
width = strokeWidth.toPx(),
cap = strokeCap
)
)
}
Draw Path Segment
This function extends DrawScope
and draws the path segment with specified paint.
fun DrawScope.drawPathSegment(pathSegment: Path, paint: Paint) {
drawIntoCanvas { canvas ->
canvas.drawPath(pathSegment, paint)
}
}
The final function
Alright, now we have everything we need to build the final function.
@Composable
fun InfinityLoader(
// Parameters
) {
// Set up infinite animation
val infiniteTransition = rememberInfiniteTransition("PathTransition")
// Animate path completion
val pathCompletion by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 2f,
animationSpec = infiniteRepeatable(
animation = tween(duration, easing = LinearEasing)
),
label = "PathCompletion"
)
Canvas(modifier) {
// Create path and calculate segment
val path = createPath(size.width, size.height)
val pathSegment = calculatePathSegment(path, pathCompletion)
// Set up paint for drawing
val paint = setupPaint(strokeWidth, strokeCap, brush)
// Apply glow effect, if provided
glow?.applyToPaint(paint, this)
// Draw placeholder, if color is provided
placeholderColor?.let { color ->
drawPathPlaceholder(path, strokeWidth, strokeCap, color)
}
// Draw the path segment
drawPathSegment(pathSegment, paint)
}
}
Congratulations 🥳! We’ve successfully built it 👏. For the complete code implementation, you can access it on GitHub Gist 🧑💻. In the next section, we’ll explore the usage of the function.
Usage
Let’s utilize this function and draw a red-to-blue gradient infinity loader with some glow:
InfinityLoader(
brush = Brush.horizontalGradient(
colors = listOf(Color.Red, Color.Blue)
),
modifier = Modifier
.width(200.dp)
.height(150.dp),
glow = Glow(),
placeholderColor = Color.Black.copy(.16f)
)
Output:
Thank you for reading this article! ❤️ If you found it enjoyable and valuable, show your appreciation by clapping 👏 and following Kappdev for more exciting articles 😊