Android: Shimmer Effect using Jetpack Compose
In this short tutorial, we explore how to create a shimmering effect using Jetpack Compose.
Shimmer effect is mainly used in skeleton loading, and sometimes, it improve the overall user experience to a certain extent... but... let's admit. Skeleton loading is the new way to show loading state to the users!
Let's define the colors for the shimmering effect.
val colors = listOf(
Color(0xFFEAEAEA), // grey
Color(0xFFD34545), // red
Color(0xFFEAEAEA) // grey
)
Since the shimmering effect is an infinite loop, we can make use of the rememberInfiniteTransition (provided by the Compose library) where it returns a InfiniteTransition. We can use the InfiniteTransition to that creates the animation that runs infinitely.
val translateAnim = transition.animateFloat(
initialValue = 0f,
targetValue = 500f,
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = 1000,
easing = FastOutLinearInEasing
),
repeatMode = RepeatMode.Restart
)
)
And let's create a linear gradient brush with colors and translateAnim
val brush = Brush.linearGradient(
colors = colors,
start = Offset(10f, 10f),
end = Offset(translateAnim, translateAnim)
)
Lastly, let's define a Spacer (or you can use a Box as well)
Spacer(
modifier = modifier
.background(brush = brush)
)
Let's put together
@Composable
fun ShimmerSpacer(
modifier: Modifier = Modifier
.fillMaxWidth()
.height(50.dp)
){
val colors = listOf(
Color(0xFFEAEAEA),
Color(0xFFD34545),
Color(0xFFEAEAEA)
)
val transition = rememberInfiniteTransition()
val translateAnim = transition.animateFloat(
initialValue = 0f,
targetValue = 500f,
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = 1000,
easing = FastOutSlowInEasing
),
repeatMode = RepeatMode.Restart
)
)
val brush = Brush.linearGradient(
colors,
start = Offset(10f,10f),
end = Offset(translateAnim.value,translateAnim.value)
)
Spacer(
modifier = modifier
.background(brush = brush)
)
}
And here you go... the shimmer effect.
Clearly, there are a few things went wrong with the shimmer effect
- The colour is red. 💩
- The shimmer effect stops mid way
- The shimmer light expands weirdly (doesn't look good either...)
Let's break it down.
The colour is red. 💩
I think you can just change the colors inside the colors list. 😅
The shimmer effect stops mid way
To allow the shimmer effect travels to the end of the Spacer, we must calculate the width of the Spacer. To do so, we can use the BoxWithConstraints to get the maxWidth and maxHeight.
The shimmer light expands weirdly (doesn't look good either...)
Let's understand how the linearGradient works...
val brush = Brush.linearGradient(
colors,
start = Offset(10f,10f),
end = Offset(translateAnim.value,translateAnim.value)
)
I draw (poorly using Adobe-owned Figma software 🙃) a state illustration as a reference of how those the start and end works in the linearGradient method, with the use of value in translateAnim.
For illustration purpose, the linearGradient will be shown as a whole
If you recalled, the start parameter have an offset of (10f, 10f) where it refers to (x1 and y1) in the illustration above. The start will be at 10f, 10f (x,y) throughout the run of the animation.
As for the end parameter, it also holds an offset but with the transition value at 100f (20% times 500f) for both x and y. Hence, it is expanding weirdly throughout the animation run.
To fix this weirdly expandable shimmer, we have to the translateAnim's value in both start and end parameters of the linearGradient brush. In addition, we also must give a fake width to the shimmer.
With reference to the above code block, we have to make use of the translateAnim.value for both x in the start and end parameter so the shimmer effect will flow from start to end point of the box.
If you noticed, I also deduct the start's x by 30% because this is considered as we the "width" of the shimmer effect. Another reason why I deduct is the shimmer should "start" before ending the visible view of the Spacer.
Final code
Here is the final code for the shimmer spacer.
@Composable
fun NewShimmerSpacer(
modifier: Modifier = Modifier
.fillMaxWidth()
.height(50.dp)
){
val colors = listOf(
Color(0xFFEAEAEA),
Color(0xFFF6F6F6),
Color(0xFFEAEAEA)
)
val transition = rememberInfiniteTransition()
val shimmerWidthPercentage = 0.3f
BoxWithConstraints {
val spaceMaxWidth = with(LocalDensity.current) { maxWidth.toPx() }
val spaceMaxHeight = with(LocalDensity.current) { maxHeight.toPx() }
val translateAnim = transition.animateFloat(
initialValue = 0f,
targetValue = spaceMaxWidth * (1 + shimmerWidthPercentage),
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = 1000,
easing = FastOutSlowInEasing
),
repeatMode = RepeatMode.Restart
)
)
val brush = Brush.linearGradient(
colors,
start = Offset(translateAnim.value - (spaceMaxWidth * shimmerWidthPercentage),spaceMaxHeight),
end = Offset(translateAnim.value,spaceMaxHeight)
)
Spacer(
modifier = modifier
.background(brush = brush)
)
}
}
Column(
Modifier.fillMaxHeight(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
){
NewShimmerSpacer(
Modifier.fillMaxWidth(0.5f)
.height(80.dp)
.clip(RoundedCornerShape(5.dp))
)
Spacer(Modifier.fillMaxWidth().height(15.dp))
NewShimmerSpacer(
Modifier
.fillMaxWidth(0.5f)
.height(50.dp)
)
Spacer(Modifier.fillMaxWidth().height(15.dp))
NewShimmerSpacer(
Modifier
.size(100.dp)
.clip(CircleShape)
)
}
Final result
Feel free to connect with me
I think there are better ways to do it. It can be cleaner as well (using 0f to 1f instead of actual values).
But you know what, it works for me!
I love to hear your thoughts, so please feel free to contact via my linkedIn or twitter 🤟
Please feel free to subscribe for weird blog post in the future. It's free. I promise!