Ajay Venugopal
Ajay Venugopal

Reputation: 1700

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

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.

Upvotes: 35

Views: 47464

Answers (10)

Ivan Carracedo
Ivan Carracedo

Reputation: 31

From version 1.7.0-alpha06 on the new non-Experimental way to do that is:

LazyColumn {
    items(...) { 
        Card(
            modifier = Modifier.animateItem(),
            ...
        ) {
          ...
        }
    }
}

Upvotes: 1

Глеб Гутник
Глеб Гутник

Reputation: 58

There have been many solutions provided here, but I would like to add another one. I wrote my custom composable that can be used like this:

val testCases = listOf(
    List(4) { "Item $it" } + listOf("Item 8", "Item 9"),
    List(12) { "Item $it" },
    List(12) { "Item $it" }.shuffled(),
    List(20) { "Item $it" },
    listOf("Item 1")
)

@OptIn(ExperimentalFoundationApi::class)
@Composable
@Preview("AnimatedColumnPreview")
fun AnimatedColumnPreview() {
    var clickCount by remember {
        mutableIntStateOf(0)
    }
    var exampleList by remember {
        mutableStateOf(List(10) { "Item $it" })
    }
    Box(
        modifier = Modifier
            .fillMaxSize()
    ) {
        LazyAnimatedColumn(
            items = exampleList,
            keyProvider = { it },
            modifier = Modifier.heightIn(10.dp, LocalConfiguration.current.screenHeightDp.dp),
            lazyModifier = { Modifier.animateItemPlacement(tween(1000)) },
            enterTransition = defaultEnterTransition * 2,
            exitTransition = defaultExitTransition * 2
        ) { _, item ->
            Box(
                modifier = Modifier
                    .fillMaxWidth()
                    .height(50.dp)
                    .padding(horizontal = 20.dp)
                    .background(
                        Brush.verticalGradient(listOf(Color.LightGray, Color.White))
                    )
            ) {
                Text(
                    text = item,
                    fontSize = 14.sp,
                    modifier = Modifier.align(Alignment.Center)
                )
            }
        }
        Column(
            modifier = Modifier
                .align(Alignment.BottomCenter)
                .fillMaxWidth()
                .background(Color.White)
                .padding(20.dp),
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Button(
                onClick = {
                    exampleList = testCases[clickCount % 5]
                    clickCount++
                }
            ) {
                Text("Change elements")
            }
        }
    }
}

This code renders a column with both appearing and disappearing animations, as well as shuffling animation:

Preview

As you see, you can simply pass a new state to it and it will calculate all animations internally. I'm not happy with the lazyModifier lambda, but I couldn't invent anything better yet. The source code can be found here: https://github.com/gleb-skobinsky/animatedcolumn/blob/main/AnimatedColumn.kt

It's quite long, so I decided not to paste it right here. Feel free to modify it to suit your own needs.

Upvotes: 1

YCuicui
YCuicui

Reputation: 5351

As per the curren Compose version, you have to put an animateItemPlacement in a modifier off the SwipeToDismiss composable like this:

LazyColumn {
    items(...) { 
        val dismissState = rememberDismissState()
        SwipeToDismiss(
            modifier = Modifier.animateItemPlacement(),
            ...
        ) {
          ...
        }
    }
}

At time of writing, this is a experimental. You will have to add @OptIn(ExperimentalFoundationApi::class) to your composable.

Upvotes: 0

Zakir Sheikh
Zakir Sheikh

Reputation: 1038

Experimental ability to animate Lazy lists item positions was added.

There is a new modifier available within LazyItemScope called Modifier.animateItemPlacement(). This works if you provide a unique key per item.

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.

Upvotes: 47

Eudy Contreras
Eudy Contreras

Reputation: 396

I have created a gist where I solve the problem for animating adding items to a LazyColumn. Really easy to add item removal animation too. I hope you find it useful

https://gist.github.com/EudyContreras/f91b20c49552e02607816b3aea6e7f43

Upvotes: 0

Yasan Glass
Yasan Glass

Reputation: 1314

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.

Upvotes: 34

Phill Taylor
Phill Taylor

Reputation: 51

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!")
        }
    }
}

Upvotes: 3

F.Mysir
F.Mysir

Reputation: 4126

If you implement compose Version 1.1.0-beta03. Now we have a new way on how we can animate items.

Working example in this link with swipeToDismiss.

Also you may have a look on this example posted by Google.

Upvotes: 0

Ajay Venugopal
Ajay Venugopal

Reputation: 1700

If you’ve used the RecyclerView widget, you’ll know that it animates item changes automatically. The Lazy layouts do not yet provide that functionality, which means that item changes cause an instant ‘snap’. You can follow this bug to track any changes for this feature.

Source: https://developer.android.com/jetpack/compose/lists#item-animations

Check the status here : https://issuetracker.google.com/issues/150812265

Upvotes: 1

axelbrians
axelbrians

Reputation: 406

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
)

Upvotes: 6

Related Questions