How to animate item placements in `FlowRow`?
# compose
m
How to animate item placements in
FlowRow
?
u
Hi Mark, I believe @Rebecca Franks has worked with animating
FlowRow
in the past.
r
What exactly are you looking to do? You can use the standard
AnimatedVisibility
inside FlowRow/FlowColumn, or animate the size of the composables using
animateAsState
values too. Here is an example using
AnimatedVisibility
with `FlowRow`:
Copy code
@OptIn(ExperimentalLayoutApi::class)
@Preview
@Composable
fun FlowLayout_AnimateItemEntrance() {
    var numberItems by remember {
        mutableIntStateOf(1)
    }
    LaunchedEffect(Unit){
        this.launch {
            delay(2000)
            numberItems += 1
            delay(2000)
            numberItems += 1
            delay(2000)
            numberItems += 1
        }
    }
    FlowRow(
        modifier = Modifier.padding(4.dp),
        horizontalArrangement = Arrangement.spacedBy(4.dp),
        maxItemsInEachRow = 3
    ) {
        val itemModifier = Modifier
            .clip(RoundedCornerShape(8.dp))
        if (numberItems >= 1) {
            Box(modifier = itemModifier
                .height(200.dp)
                .width(60.dp)
                .background(Color.Red))
        }
        AnimatedVisibility(numberItems >= 2, enter = fadeIn()) {
            Box(modifier = itemModifier
                .height(200.dp)
                .fillMaxWidth(0.7f)
                .background(Color.Blue)
            )
        }
        AnimatedVisibility(numberItems >= 3, enter = fadeIn(),
            modifier = Modifier.weight(1f)){
            Box(modifier = itemModifier
                .height(200.dp)
                .fillMaxSize(1f)
                .background(Color.Magenta))
        }
    }
}
m
Thanks @Rebecca Franks I’m migrating from a
RecyclerView
that was using a flow layout manager so the animations of insertion/deletion were automatically happening. I guess what I’m really looking for is
LazyFlowRow
. In my case, the items are chips. The user can enter action mode and select any subset of chips and then delete/move the selected ones, so it would be good to somehow get the changes to animate. From your example, I’m not sure I can use the same approach, but feel free to correct me!
s
So you want them to get their position animated, not their content size, that’s what I understand from this. Like you get with
animateItemPlacement
inside LazyX.
m
Yes exactly. The size of the chip would not change, though the placement of the chip or any chip with a greater index than it’s old or new index could be changed accordingly.
LazyFlowRow
would also be useful because potentially the items could be considerably too many to fit on the screen at once.
s
Then maybe at least for now you can try looking into this https://kotlinlang.slack.com/archives/CJLTWPH7S/p1638219497247000?thread_ts=1638218328.244500&cid=CJLTWPH7S discussion which links to this sample https://developer.android.com/reference/kotlin/androidx/compose/ui/layout/package-summary#(androidx.compose.ui.Modifier).onPlaced(kotlin.Function1) It may really already be what you need for this case, not sure, I’d give it a try.
m
Oh nice, yes the
Modifier.animatePlacement()
works perfectly! Almost too easy 🙂 I also added the
key(item) { }
although it seems unnecessary and I’m not clear why it should be used here.
s
When you say you “also” added it, what are you referring to now? I sent a bunch of links so I am confused myself now too 😄 But it’s a good idea regardless, since you don’t get a key as you do with lazy lists in the Flow APIs afaik. It’s in general used so that if the items change, and something gets added, compose doesn’t confuse what state is used for which item, so you key to make sure that each item gets it’s own “slot” so to speak to store stuff to the slot table and for sure not accidentally get something from the item next to it when it gets removed.
m
I copied the
animatePlacement
given here: https://developer.android.com/reference/kotlin/androidx/compose/ui/layout/package-summary#(androidx.compose.ui.Modifier).onPlaced(kotlin.Function1) Strangely it’s working on one of my
FlowRow
s but not the other. Investigating…
It’s okay, solved it. For some reason, I’d not implemented equals on the item class!
Actually, that wouldn’t have been a problem if I had used a better key than the item itself. So now I use an id, which means the animation also works for things like renames (note: the chips are sorted alphabetically).
s
Yeah, that's optimally what you want, if the item has a nice stable ID to use
m
Yeah, it makes sense. The only reason I went for
key(item)
by default was because that’s what examples I saw were using.
I’m getting some strange behavior where renaming a chip (so that it has a different index in the list) works in terms of animating to the new placement, but sometimes the chip label does not update. Could this be because it is keyed on the ID which does not change?
s
Hmm, even if you key on something which does not change, as far as I am aware, if you are reading some state from a state object that should still update regardless. If you shared some code maybe it’d help deduce what is going on? My first thought (which is not based on anything, since I haven’t used
key
a lot to see if it happens there too) is that you are reading this state from a parameter passed into the composable, so it’s not from inside a State object, so it’s somehow referencing the old value which was captured in the key lambda. And if you do a
rememberUpdatedState
outside of that
key
lambda and then inside read from the result of
rememberUpdatedState
then it would read the most fresh value. This is something that happens with functions that capture such parameters, here’s one example https://kotlinlang.slack.com/archives/CJLTWPH7S/p1678750942883689?thread_ts=1678732457.665529&cid=CJLTWPH7S so I don’t know, maybe you’re experiencing something similar?
m
Thanks @Stylianos Gakis. It seems like a good guess because the label is obtained from the item via a property which needs to be invoked. However this still does not work:
Copy code
items.forEach { item ->
    val updatedLambda by rememberUpdatedState(item.labelGetter)
    key(item.id) {
        val label = updatedLambda()
        TagChip(
            label = label,
s
Damn. I really don’t know then, and not sure what else I’d try tbh 😵‍💫 I’d for sure first check that removing the
key
fixes this issue, and if yes there must be something with the
key
function that I do not properly understand.
m
I tried removing the key, removing the animatedPlacement, and removing both, but still get the same problem. Although when removing the key, the problem seems to happen less often. When I stick a println right before the
Text
Composable , it’s logging the new value. So it’s definitely not a problem with the item itself. How can it be that the Text is not showing the argument that it’s receiving? Perhaps an issue with
FlowRow
?
The best workaround I have is to do this, though I don’t understand why it’s necessary
Copy code
val label = item.labelGetter()
key(item.id, label) {
s
Glad it works at least 😄 Maybe someone else can come in and give an explanation
m
Do I still need the
rememberUpdatedState
?
s
I don’t know at this point, test and act accordingly is what I’d suggest 😄
s
@Mark were you able to add exit animation when an item was removed?
u
@Shivam Dhuria Were you able to figure out both enter and exit? Apparently
animatePlacement()
isn't working with
FlowRow
for me.
Adding the solution here in case someone stumbles upon this thread like me.
Copy code
@Composable
@OptIn(ExperimentalLayoutApi::class)
private fun PotentialTripsBlock(spacing: Dp, itemsPerRow: Int) {
    Column(verticalArrangement = Arrangement.spacedBy(spacing)) {
        EGDSText(
            text = "Flow Row (Animating items)",
            egdsTextStyle = EGDSTextStyle.Text500(weight = EGDSTextWeight.MEDIUM)
        )
        val removedList = remember { mutableStateListOf<Int>() }
        val items = List(10) { it }

        FlowRow(
            modifier = Modifier
                .fillMaxWidth()
                .animateContentSize(),
            maxItemsInEachRow = itemsPerRow,
            horizontalArrangement = Arrangement.spacedBy(mediumSpacing),
            verticalArrangement = Arrangement.spacedBy(spacing),
        ) {
            items.forEachIndexed { index, item ->
                key(index) {
                    AnimatedVisibility(
                        modifier = Modifier.animatePlacement(),
                        visible = removedList.contains(index).not(),
                        enter = fadeIn(),
                        exit = fadeOut(),
                    ) {
                        Box(
                            modifier = Modifier
                                .widthIn(min = 290.dp)
                                .height(100.dp)
                                .background(color = Color.Red)
                                .clickable {
                                    removedList.add(index)
                                }
                        ) {}
                    }
                }
            }
        }
    }

}
s
So
animatePlacement
does seem to work after all right?
👌 1
u
But I noticed that it won't work if you try to use it in
Row
or
LazyRow
. It slows down the scrolling. So it only works with FlowRow/Column apparently.
1290 Views