How to Create an Animated Infinity Loader in Jetpack Compose

Kappdev
5 min readDec 29, 2023

--

Welcome 👋

In this article, we’ll create a stunning Animated Infinity Loader with Jetpack Compose.

Let’s dive in! 🚀

Created by Kappdev

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 widthand 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)
)
}
}
Created by Kappdev

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:

Created by Kappdev

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:

Custom Loaders | Jetpack Compose

7 stories

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 😊

--

--

Kappdev

💡 Curious Explorer 🧭 Kotlin and Compose enthusiast 👨‍💻 Passionate about self-development and growth ❤️‍🔥 Push your boundaries 🚀