Reputation: 1700
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
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:
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
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
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
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
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
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
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
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
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