How to show animate an item view in LazyColumn on Jetpack Compose Android

Viewed 10103

I have a list of items in lazy column view.

How we can show animation when we are removing the one item from the list.

I need to animate the view which is getting deleted. The delete operation is done by pressing the delete icon inside the view.

I tried with AnimationVisibility and its not working as expected.

7 Answers

Update for Compose 1.1.0:

Animating item position changes are now possible but deletion/insertion animations are still not possible and need to be implemented by the method I've explained.

To animate item position changes you just have to provide the item keys in your list by key = { it.id } and use Modifier.animateItemPlacement().

Original Answer

It is not officially supported yet but they are working on it. You can probably achieve it but in a hacky way.

When your list updates, your composable is recreated and it doesn't support animations for items yet, so you have to add a boolean variable on your item and change the value when it's "deleted" instead of removing it from the list. Once the updated list is shown, you can animate the item being removed with a delay and then update the list without it once the animation is over.

I have not personally tested this method so it might not work as expected but that's the only way I can think of with lazy lists not supporting update animations.

Experimental ability to animate Lazy lists item positions was added.

There is a new modifier available within LazyItemScope called Modifier.animateItemPlacement().

Usage example:

var list by remember { mutableStateOf(listOf("A", "B", "C")) }
LazyColumn {
    item {
        Button(onClick = { list = list.shuffled() }) {
            Text("Shuffle")
        }
    }
    items(list, key = { it }) {
        Text("Item $it", Modifier.animateItemPlacement())
    }
}

When you provide a key via LazyListScope.item or LazyListScope.items this modifier will enable item reordering animations. Aside from item reordering all other position changes caused by events like arrangement or alignment changes will also be animated.

Like what YASAN said, I was able to produce 'slideOut' + 'fadeOut' animation on LazyColumn item using AnimatedVisibility by simply adding isVisible properties on DataClass of your item and wrap your item view in AnimatedVisibility Composable. Since that api is still in experimental please be careful.

For you reference if you or someone else might look for it I'm gonna drop my snippet here.

For the LazyColumn

LazyColumn {
    items(
        items = notes,
        key = { item: Note -> item.id }
    ) { item ->
        AnimatedVisibility(
            visible = item.isVisible,
            exit = fadeOut(
                animationSpec = TweenSpec(200, 200, FastOutLinearInEasing)
            )
        ) {
            ItemNote(
                item
            ) {
                notes = changeNoteVisibility(notes, it)
            }
        }
    }
}

And for the Item Composable

@ExperimentalAnimationApi
@ExperimentalMaterialApi
@Composable
fun ItemNote(
    note: Note,
    onSwipeNote: (Note) -> Unit
) {
    val iconSize = (-68).dp
    val swipeableState = rememberSwipeableState(0)
    val iconPx = with(LocalDensity.current) { iconSize.toPx() }
    val anchors = mapOf(0f to 0, iconPx to 1)

    val coroutineScope = rememberCoroutineScope()

    Box(
        modifier = Modifier
            .fillMaxWidth()
            .height(75.dp)
            .swipeable(
                state = swipeableState,
                anchors = anchors,
                thresholds = { _, _ -> FractionalThreshold(0.5f) },
                orientation = Orientation.Horizontal
            )
            .background(Color(0xFFDA5D5D))
    ) {
        Box(
            modifier = Modifier
                .fillMaxHeight()
                .align(Alignment.CenterEnd)
                .padding(end = 10.dp)
        ) {
            IconButton(
                modifier = Modifier.align(Alignment.Center),
                onClick = {
                    Log.d("Note", "Deleted")
                    coroutineScope.launch {
                        onSwipeNote(note)
                    }
                }
            ) {
                Icon(
                    Icons.Default.Delete,
                    contentDescription = "Delete this note",
                    tint = Color.White
                )
            }
        }

        AnimatedVisibility(
            visible = note.isVisible,
            exit = slideOutHorizontally(
                targetOffsetX = { -it },
                animationSpec = TweenSpec(200, 0, FastOutLinearInEasing)
            )
        ) {
            ConstraintLayout(
                modifier = Modifier
                    .offset { IntOffset(swipeableState.offset.value.roundToInt(), 0) }
                    .fillMaxHeight()
                    .fillMaxWidth()
                    .background(Color.White)
            ) {
                val (titleText, contentText, divider) = createRefs()

                Text(
                    modifier = Modifier.constrainAs(titleText) {
                        top.linkTo(parent.top, margin = 12.dp)
                        start.linkTo(parent.start, margin = 18.dp)
                        end.linkTo(parent.end, margin = 18.dp)
                        width = Dimension.fillToConstraints
                    },
                    text = note.title,
                    fontSize = 16.sp,
                    fontWeight = FontWeight(500),
                    textAlign = TextAlign.Start
                )

                Text(
                    modifier = Modifier.constrainAs(contentText) {
                        bottom.linkTo(parent.bottom, margin = 12.dp)
                        start.linkTo(parent.start, margin = 18.dp)
                        end.linkTo(parent.end, margin = 18.dp)
                        width = Dimension.fillToConstraints
                    },
                    text = note.content,
                    textAlign = TextAlign.Start
                )

                Divider(
                    modifier = Modifier.constrainAs(divider) {
                        start.linkTo(parent.start)
                        end.linkTo(parent.end)
                        bottom.linkTo(parent.bottom)
                        width = Dimension.fillToConstraints
                    },
                    thickness = 1.dp,
                    color = Color.DarkGray
                )
            }
        }
    }
}

Also the data class Note

data class Note(
    var id: Int,
    var title: String = "",
    var content: String = "",
    var isVisible: Boolean = true
)

I'm just playing around but maybe something like this will help while they built out the proper solution.

    @Composable
fun <T> T.AnimationBox(
    enter: EnterTransition = expandVertically() + fadeIn(),
    exit: ExitTransition = fadeOut() + shrinkVertically(),
    content: @Composable T.() -> Unit
) {
    val state = remember {
        MutableTransitionState(false).apply {
            // Start the animation immediately.
            targetState = true
        }
    }

    AnimatedVisibility(
        visibleState = state,
        enter = enter,
        exit = exit
    ) { content() }
}


LazyColumn(
  state = lazyState
) {
    item {
        AnimationBox {
            Item(text = "TEST1!")
        }
        AnimationBox {
            Item(text = "TEST2!")
        }
    }
}
Related