Welcome to this series of articles that covers Jetpack Compose animations. Along the way, we’ll be using the most common layout. You guessed it, the list!
In this article, we’ll try to create a cool animation for when an item is added to a list. Without further ado, let’s start with a preview of what we’re going to build:

If you want to follow along or want the code of the UI, check out this repository:

Note that compose version is 1.1.1

Let’s summarize what is happening so we can split this implementation into smaller lucid pieces.

  1. On add button click event, we will randomly pick a ShoesArticle object from an already initialized array of articles.
  2. Then, a particle, which is tinted with a passed color from the chosen article, will be fired from underneath the top bar.
  3. When the particle is in position, the item’s background will expand from the particle to the slot size.
  4. Finally, we animate the content alpha to make it visible.

1. Particle

1.1. UI

So, how to draw a circle in Compose? We will use a canvas to do that. It is pretty straightforward.

@Composable
fun Particle(modifier: Modifier, color: Color) {
val radiusDp = dimensionResource(id = R.dimen.particle_radius)
val radius: Float
with(LocalDensity.current) {
radius = radiusDp.toPx()
}
Canvas(modifier.size(radiusDp * 2)) {
drawCircle(
color = color,
radius = radius
)
}
}
view raw Particle.kt hosted with ❤ by GitHub

Now, we will head to MainActivity to place it under the top bar. So, we need to wrap the top bar and the Particle into a Box. Then, we generate a random color to update the circle tint every time we click the AddCircle icon.

@ExperimentalAnimationApi
@Composable
fun Home() {
val colorsArray = arrayOf(Purple, Blue, Red)
var particleColor by remember { mutableStateOf(Color.White) }
/**/
Scaffold(
topBar = {
Box {
Particle(
modifier = Modifier
.align(Alignment.BottomCenter),
color = particleColor
)
TopAppBar(
/**/
actions = {
IconButton(onClick = {
particleColor = colorsArray.random()
}) {
Icon(Icons.Filled.AddCircle, contentDescription = null)
}
},
/**/
)
}
}
) { /**/ }
}
view raw MainActivity.kt hosted with ❤ by GitHub

1.2. Animation

What exactly are we going to animate? That’ll be the top translation of the particle. Let’s put that in a state, and translate the circle according to its value.

var topTranslation by remember { mutableStateOf(0f) }
Canvas(modifier.size(radiusDp * 2)) {
translate(top = topTranslation) {
drawCircle(
color = color,
radius = radius
)
}
}
view raw Particle.kt hosted with ❤ by GitHub

We need to swing the particle between its first position and the position of the first item in our LazyColumn. So, we animate the top translation between zero and the distance between the center of the particle and the first item. We can calculate the latter with the addition of the particle radius, the top padding of the LazyColumn layout, and the height’s half of an item.
For such a use case, we would normally use animate*AsState.

animate*AsState is a fire-and-forget animation function. This Composable function is overloaded for different parameter types such as Float, Color, Dp, etc. When the provided targetValue is changed, the animation will run automatically.


We can use a different targetValue basing on a boolean state.

val topTranslation by animateFloatAsState(
targetValue = if (isFired) radius + topPadding + itemHeight / 2 else 0f
)
view raw Particle.kt hosted with ❤ by GitHub

However, we want to reset the topTransition to 0f after the animation to make it ready for the next item adding process. So, we need another API for our animation. We will use Animateable.

Animateable is a value holder that can animate the value as it is changed via animateTo.


animateTo is a suspend function. So, we need to wrap it into a coroutine.

LaunchedEffect launches a block into the composition’s CoroutineScope. The coroutine will be canceled and re-launched when LaunchedEffect is recomposed with a different key or keys.


So, we would use isFired as a key, and execute the block only when that key is true. Then, we animate our Animateable while updating topTranslation. We can spice it up with a physics-based animation using the spring function to produce a specification of an animation. Subsequently, we would reset our top translations. And finally, we execute a callback that will be used to notify that the newly added item should be animated.

val animatedTopTranslation = remember { Animatable(0f) }
LaunchedEffect(isFired) {
if (isFired) {
animatedTopTranslation.animateTo(
targetValue = radius + topPadding + itemHeight / 2,
animationSpec = spring(
stiffness = Spring.StiffnessLow
)
) {
topTranslation = value
}
animatedTopTranslation.snapTo(0f)
topTranslation = 0f
onCompleteAnim()
}
}
view raw Particle.kt hosted with ❤ by GitHub

2. Item

2.1. UI

Now, we need to play with the background. So, we can use Modifier.drawBehind() to draw into a canvas behind the modified content. And replace the background color with a transparent one. We want to draw a circle with a dynamic radius that changes between the particle’s equivalent and a max radius that we have to calculate. That’s the value that we are looking for:

We can compute this with: sqrt( (width/2) ^2 + (height/2)^2). Kotlin offers us an inline function that achieves this, which is hypot().
But now we need to get the slot size after its initial composition. In Android Views, we normally use viewReference.post {}. The Compose counterpart is Modifier.onGloballyPositioned {}.
The last thing, we want to add a visibility alpha for the item’s content to animate it later after the circular expansion of the background.

private var maxRadiusPx = 0f
@ExperimentalAnimationApi
@Composable
fun ShoesCard(shoesArticle: ShoesArticle, isVisible: Boolean) {
val particleRadius: Float
with(LocalDensity.current) {
particleRadius = dimensionResource(id = R.dimen.particle_radius).toPx()
}
var radius by remember { mutableStateOf(particleRadius) }
var visibilityAlpha by remember { mutableStateOf(0f) }
Box(
Modifier.padding(horizontal = 16.dp)
) {
Column(
modifier = Modifier
.clip(RoundedCornerShape(8.dp))
.background(
color = Color.Transparent
)
.onGloballyPositioned { coordinates ->
if (maxRadiusPx == 0f) {
maxRadiusPx = hypot(coordinates.size.width / 2f, coordinates.size.height / 2f)
}
}
.drawBehind {
drawCircle(
color = if (isVisible) shoesArticle.color else Color.Transparent,
radius = radius
)
}
.padding(dimensionResource(id = R.dimen.slot_padding))
.align(Alignment.CenterStart)
.fillMaxWidth(),
) {
Text(
/**/
modifier = Modifier.alpha(visibilityAlpha)
)
/**/
Row(
modifier = Modifier
.height(IntrinsicSize.Min)
.alpha(visibilityAlpha),
/**/
) {
/**/
}
}
Image(
modifier = Modifier
.align(Alignment.CenterEnd)
.size(dimensionResource(id = R.dimen.image_size))
.alpha(visibilityAlpha),
/**/
)
}
}
view raw ShoesCard.kt hosted with ❤ by GitHub

Back to MainActivity now to add an item. Nothing complex here, we create a ShoesArticle basing on the color we randomly picked, and we increment the id for the next article.

/**/
var isFired by remember { mutableStateOf(false) }
val shoesArticles = remember { mutableStateListOf<ShoesArticle>() }
var addedArticle by remember { mutableStateOf(ShoesArticle()) }
var id by remember { mutableStateOf(0) }
Scaffold(
topBar = {
Box {
/**/
TopAppBar(
title = {
Text(text = "List Animations In Compose")
},
actions = {
IconButton(onClick = {
particleColor = colorsArray.random()
addedArticle =
allShoesArticles.first { it.color == particleColor }.copy(id = id)
.also {
id++
}
shoesArticles.add(0, addedArticle)
isFired = true
}) {
Icon(Icons.Filled.AddCircle, contentDescription = null)
}
},
/**/
)
}
}
) { /**/ }
view raw MainActivity.kt hosted with ❤ by GitHub

Now, we handle the items states. We create a map basing on our shoesArticles list, and we change the newly added item state to true when the particle animation is complete. Also, we set isFired to false so that when it is switched to true on the next click, the LaunchedEffect block of the particle will execute.

/**/
val shoesArticles = remember { mutableStateListOf<ShoesArticle>() }
val isVisibleStates = remember {
mutableStateMapOf<ShoesArticle, Boolean>()
.apply {
shoesArticles.map { shoesArticle ->
shoesArticle to false
}.toMap().also {
putAll(it)
}
}
}
Scaffold(
topBar = {
Box {
Particle(
modifier = Modifier
.align(Alignment.BottomCenter),
isFired = isFired,
color = particleColor,
onCompleteAnim = {
isVisibleStates[addedArticle] = true
isFired = false
}
)
/**/
}
}
) { innerPadding ->
ShoesList(
modifier = Modifier.padding(innerPadding),
isVisibleStates = isVisibleStates,
shoesArticles = shoesArticles
)
}
view raw MainActivity.kt hosted with ❤ by GitHub

2.2. Animation

We want to animate the radius in the background, and then the alpha of the content. So, we utilize the same approach used in particle implementation.

val animatedRadius = remember { Animatable(particleRadius) }
val animatedAlpha = remember { Animatable(0f) }
LaunchedEffect(isVisible) {
if (isVisible) {
animatedRadius.animateTo(maxRadiusPx, animationSpec = tween()) {
radius = value
}
animatedAlpha.animateTo(1f, animationSpec = tween()) {
visibilityAlpha = value
}
}
}
view raw ShoesCard.kt hosted with ❤ by GitHub

3. Final thoughts

Since the items will be recomposed with a true isVisible parameter, they will animate. And we will get a scroll animation:

Animation | Jetpack Compose | Android Developers

Here is the full Github repository for this article:

That’s it for this article. See you in the next one of this series.

Here We Go Again : (

if (article == helpful) {
    println("Like and subscribe to blog newsletter.")
} else {
    println("Let me know what i should blog on.")
}

This site uses Akismet to reduce spam. Learn how your comment data is processed.