Shimmer animations in Jetpack Compose without extra dependencies

In modern app development, user experience is paramount. Gone are the days when a simple loader at the center of the screen sufficed. Today, users expect more refined loading indicators, with shimmer animations being popular. While many libraries are available for creating shimmer effects, we've found that it's quite simple to implement this animation "manually" in Jetpack Compose without adding another dependency to our project.

Modifier extension approach

Our preferred method for implementing shimmer animations is through a Modifier extension. This approach allows for easy reuse across your app and keeps your code clean and modular.

When we need to display shimmers, we create a loading component where we design a Box similar to the content we expect to see on the screen. By attaching our custom extension to this Box, it transforms into a shimmer view.

The extension

fun Modifier.shimmerEffect(
    widthOfShadowBrush: Int = 300,
    endOfOffsetY: Float = 80f,
    durationMillis: Int = 1000,
): Modifier = composed {
    val shimmerColors = listOf( // 1.
        Color.Transparent,
        Color.Transparent,
        Color.White.withAlpha(0.5f),
        Color.White,
        Color.White.withAlpha(0.5f),
        Color.Transparent,
        Color.Transparent,
    )

    val transition = rememberInfiniteTransition(label = "shimmerLoadingAnimation")

    val configuration = LocalConfiguration.current
    val density = LocalDensity.current
    val width = with(density) { configuration.screenWidthDp.dp.toPx() }

    val translateAnimation = transition.animateFloat( // 2.
        initialValue = 0f,
        targetValue = width + 100f,
        animationSpec = infiniteRepeatable(
            animation = tween(
                durationMillis = durationMillis,
                easing = LinearEasing,
            ),
            repeatMode = RepeatMode.Restart,
        ),
        label = "Shimmer loading animation",
    )

    this.background( // 3.
        brush = Brush.linearGradient(
            colors = shimmerColors,
            start = Offset(x = translateAnimation.value - widthOfShadowBrush, y = 0.0f),
            end = Offset(x = translateAnimation.value, y = endOfOffsetY),
        ),
    )
}
  1. These colors define the look of the moving "white" line on top of the placeholder Box.
  2. This is the animation that moves the "white" line. The animation moved from zero to the width of the screen (and a bit further).
  3. The brush draws the "white" line. The variables here control the slope and the width of the "white" line.

This custom approach allows for complete customization of how the shimmers look. For instance, changing the endOfOffsetY variable changes the slope of the "white" moving line as follows:

endOfOffsetY=20f
endOfOffsetY=80f

Usage

Using this shimmer effect in your Compose UI is straightforward. Here's an example of how to apply it:

@Composable
fun LoadingShimmerEffect() {
    Column(modifier = Modifier.fillMaxSize()) {
        repeat(5) {
            Box(
                modifier = Modifier
                    .fillMaxWidth()
                    .height(100.dp)
                    .padding(16.dp)
                    .shimmerEffect()
            )
        }
    }
}

In this example, we're creating a column with five shimmering boxes. Each box represents a placeholder for content that's loading. The shimmerEffect() modifier is applied to each box, giving it an animated shimmer appearance.

By implementing shimmer animations this way, we've achieved a modern, polished loading indicator without the need for additional libraries. This approach keeps our app's dependency list lean while providing a great user experience.

Happy coding!