Is it possible to get insert/remove animations sim...
# compose
z
Is it possible to get insert/remove animations similar to
LazyColumn
with a regular
Column
? My layout consists of several cards, each with their own column that grows/shrinks. I get pretty far by just using
Modifier.animateContentSize()
but if I remove the first item in the list, the second one just takes its spot immediately (followed by the card shrinking in size since there are fewer items).
s
I think you'll have to wrap each item with AnimatedVisibility for that, but even then I think you'd still need more work to match animateItem's behavior (maybe the best way is through a custom layout?)
(I also experimented with this in a FlowRow, if you want to have it all backed by a list, you'll have to write some rather hacky code to diff the list (so you know what disappeared), and make the for each rely on a slightly delayed representation of your list (so items don't disappear before finishing the exit animation))
z
I suspected something along those lines. Seems like too much work, for my case at least. It already looks about 95% perfect (just some edge cases that could behave better). I appreciate you taking the time to respond! I think I saw something about the inner workings of LazyColumn being extracted out so that it could be re-used in other scenarios.. but Im not sure it was about the diffing logic. Would be a pretty nice addition to have some kind of diff behavior in a layout 🙂
✌️ 1
k
The only way i could find was to wrap every item in animated visibility did you folks find something better?
nono 1
homer disappear 1
s
AnimatedVisibility seems like the easiest way to do this without having to design an entirely new API Though I'm super interested in building some generic way to do this (at the very least, some helper functions and modifiers to help with the diffing, state, and animation management etc...). It's definitely possible with a custom layout, could even be a made of the built-in layouts with AnimatedVisibility or something similar, in a way where the bulk of the work is abstracted into a nice API.
k
I am also trying to to do the same i need to animate every item in a column sounds like a task for the weeekend
z
This is probably not the way to do it nowadays, but I recall using some functionality from RecyclerViews diff thing back in the day; which was hooked up to a simple LinearLayout. It was pretty involved, but the end result was magical. This is to say that the RecyclerView source code might have some insights around the diffing logic 🙂
k
I read this a while back https://medium.com/google-developer-experts/the-diffing-dilemma-all-about-diffing-with-lazylists-288847307b8a Dont remember exactly what it had but will go through this again
👍🏽 1
z
This is ai generated code, but its quite clever. Maybe you will find it useful, if you do try to adapt it Id love to know! I have some other things that are stealing my focus atm, but when time allows Im gonna experiment with it myself too!
❤️ 1
Copy code
@Composable
fun Today(activity: Activity) {
    val displayItems = remember { mutableStateListOf<DisplayItem>() }
    val coroutineScope = rememberCoroutineScope()

    // React to changes in the immutable list
    LaunchedEffect(activity.entries) {
        val currentEntries = activity.entries
        val currentKeys = currentEntries.map { it.first to it.second }.toSet()

        // Handle removals
        displayItems.filter { it.plan to it.performance !in currentKeys }.forEach { item ->
            coroutineScope.launch {
                // Animate out (shrink and fade)
                item.animation.animateTo(
                    targetValue = 0f,
                    animationSpec = tween(durationMillis = 300, easing = LinearEasing)
                )
                displayItems.remove(item) // Remove after animation
            }
        }

        // Handle additions
        currentEntries.forEach { (plan, performance) ->
            if (displayItems.none { it.plan == plan && it.performance == performance }) {
                val anim = Animatable(0f) // Start invisible
                displayItems.add(DisplayItem(plan, performance, anim))
                coroutineScope.launch {
                    // Animate in
                    anim.animateTo(
                        targetValue = 1f,
                        animationSpec = tween(durationMillis = 300, easing = LinearEasing)
                    )
                }
            }
        }
    }

    Card(
        modifier = Modifier.fillMaxWidth(),
        content = {
            Header {
                Text("Today")
            }

            Column(
                modifier = Modifier.animateContentSize() // Smoothly resize the Column
            ) {
                displayItems.forEach { item ->
                    // Use key for stable identity
                    key(item.plan, item.performance) {
                        val scale by item.animation.asState() // Get animated value
                        if (scale > 0f) { // Only render if not fully animated out
                            Box(
                                modifier = Modifier.graphicsLayer {
                                    scaleX = scale
                                    scaleY = scale
                                    alpha = scale // Fade out as it shrinks
                                }
                            ) {
                                Pending(
                                    plan = item.plan,
                                    performance = item.performance
                                )
                            }
                        }
                    }
                }
            }

            Add() // Hypothetical "Add" composable
        }
    )
}

// Placeholder composables (replace with your actual implementations)
@Composable
fun Card(modifier: Modifier = Modifier, content: @Composable () -> Unit) {
    Column(modifier = modifier.padding(16.dp)) { content() }
}

@Composable
fun Header(content: @Composable () -> Unit) {
    content()
}

@Composable
fun Pending(plan: String, performance: String) {
    Row(modifier = Modifier.padding(8.dp)) {
        Text("$plan - $performance")
    }
}

@Composable
fun Add() {
    Button(onClick = { /* Add logic */ }) {
        Text("Add")
    }
}

data class Activity(val entries: List<Pair<String, String>>)
s
Whoa, that looks nice! Very similar to what I did! My solution was extremely similar in diffing, but the difference is that I used
AnimatedVisibility
, alongside with
DisposableEffect
. The animated visibility uses the original list (not the diffed one), but the for each loop uses a diffed copy, where additions were reflected immediately, while removals were delayed (until
onDispose
was called within the
AnimatedVisibility
, signalling that the animation was over and the composable left the composition). Their usage of
Animatable
is quite nice there, makes the diffing code much simpler and all in one place, but the usage is more manual compared to
AnimatedVisibility
(though it's probably more powerful that way!). Thanks for sharing, I'll definitely experiment with this!
z
Agreed! I went down a different route myself and solved it too! The root of my problem was that I had a card with additional items inside, hence I couldnt use LazyColumn for the card items. I ended up trying to fake the items to just look like theyre part of the same card (different shape for first/middle/last) so that the LazyColumn could do its thing. It turned out amazing, and I really like that the look & feel of the animations are identical to other insert/remove animations in the app, and possibly some performance gains from the items being reusable too (its a very complex screen with a lot of items). The only caveat I can think of is that the background of the removing item is faded out as well (or rather, it has no background, so the card "opens up" in its place); but I actually like it so much that I havent even looked into just having a surface around each item (I dont like the fact that Id need to make sure card & surface colors are synchronized for that).
🙌 1
🦜 1