Given a `LazyColumn` where several items start ~`G...
# compose-android
k
Given a
LazyColumn
where several items start
GONE
out of composition and eventually are animated to
VISIBLE
be in composition, is there any way to make item n + 1 start to appear not at the same time as item n? I need to have item n start to animate, and after a delay n + 1 should animate, and after the same delay n + 2 begins to appear, and so on. If the question’s not clear, happy to clarify in 🧵
solved 1
Basically, things are a bit “*too* performant” for the use case, and when triggering N number of items to appear on a single button press, they all appear at once (great for most use cases) but I need to make them appear one after another, like the list itself is growing in an animated fashion (rather than all N of them animating simultaneously).
It would even be ok for n + 1 to start appearing only after item n has completely finished its appearance animation
The only thing I’ve come up with so far is to make the animation times differ such that - for instance - it takes x times longer for n + 1 to animate than n (which still wouldn’t be quite right for getting n + 1 to start only after n has finished). But it feels like there should be a less manual way. Am I just missing something in the APIs?
s
Using
GONE
and
VISIBLE
to describe composable visibility is kinda trippy , I thought you're talking about View interop 😅
k
@Skaldebane yeah, sorry. In composition and out of composition, would perhaps be the better way of putting it. Corrected now.
👌 1
s
But yeah I don't think there's a built-in way to do this, you'll have to do it manually. Not sure if there's a library that helps abstract it a bit, but it shouldn't be too hard either way.
k
It’s a shame because our iOS devs were able to get the animation just right with seemingly no difficulty. I always hate when that happens (though it’s gotten much much better with the introduction and progression of Compose, of course)
s
Yeah, but it's just different priorities in these frameworks. SwiftUI is more about "here's a bunch of ready components that work as God Apple intended" (but you can customize if you wish), while Compose is more like "here's a bunch of powerful primitives to do whatever, good luck" (but here's some ready stuff for your convenience). So sometimes it's Compose that feels bad, and other times it's SwiftUI 😅
💯 1
😂 3
j
Are you only thinking about the initial load? Or do you also expect these animations to happen when the user scrolls down the list?
1
k
@jolo just the initial load. Think of an accordion that opens up, revealing just one item at a time. After that, the accordion just stays open while you scroll up and down.
@Colton Idle any thoughts? (noticed your reaction)
t
What if you emit with some small delay between the items in the first batch of items? I’m not sure it would work but could worth a try I guess 😅
🤔 1
d
LazyLists don't have a staggered enter transition. So this unfortunately I don't see a way around a manual animation for it. Please feel free to file a feature request. Also, I'm curious in your use case, do you expect animation for the new items brought into the view port if there's scrolling up or down during the initial composition?
gratitude thank you 1
k
@Doris Liu no. Think of a list with N accordion groups. If you click on collapsed accordion 1, then all of the rows which are a part of group 1 will expand one at a time, so that it gives the feel of the accordion being revealed 1 row at a time. Now that the accordion is expanded, it simply stays open until it is clicked closed. Multiple accordions can be open at a time.
As it is now, when each row uses its own
AnimatedVisibility
, the list “suddenly” makes room for the first x number of rows in the expanding accordion(I believe from the look-ahead), and you see the first X rows all animate simultaneously. This causes the feel of jank because the title of accordion 2 “suddenly” disappears (scrolled off the bottom of the screen) rather than sliding down at a velocity that matches the accordion expansion.
Oh wait, you said “initial composition”. Sorry. If any accordion starts expanded then at the initial composition they should appear with no animation (which is fine because initial visible value of each
AnimatedVisibility
starts out as true - it does not transition from false to true).
Here's a few "frames" of the animation we don't prefer
And here's what we would prefer
s
@Kevin Worth Since every single item has its own AnimatedVisibility, you're basically on step away from solving this. It's wonky, a bit manual and hacky, but the idea is to delay the animation by a little bit for every item more than the one before it. There's a couple ways to do this: • Change the visibility from
false
to
true
for each item with a little delay. This is a little complicated, as it'll require you to maintain a "delayed" state (probably a
List<Boolean>
) that reacts to the original state (the one that determines if the accordion is open), and changes with a delay (e.g. at first it's
[true, false, false]
, then after the delay we switch one more
[true, true, false]
, and keep going until everything is
true
, where each accordion item takes its visibility from this list/map based on its index in the accordion or something like that). I don't like this approach, because of how verbose and complicated it is, its effect on cancelling the animation mid-way (you'll have to handle it yourself, otherwise it might look broken), and just the general extra overhead it may have.
The second way, which I think is probably the best is to simply delay the animation from the
animationSpec
of the
EnterTransition
and
ExitTransition
of
AnimatedVisibility
. You just have to get the "index" of each item in the accordion (however you wish), and then multiply that by some amount of delay. For exit animation, you'll wanna reverse those delays. You'll have to use
tween
since it's the only way to specify a delay afaik. (@Doris Liu Compose seems to have an internal
delayed
function that looks super useful to delay e.g. a
spring
or any other spec, any idea why it's
internal
? Or is there a catch?) Given the total number of items is called
maxIndex
this is how that would look like:
Copy code
val enterSpec = tween(delayMillis = index * 200)
val exitSpec = tween(delayMillis = (maxIndex - index) * 200)
AnimatedVisibility(
    visible = ...,
    enter = fadeIn(enterSpec) + expandVertically(enterSpec),
    exit = fadeOut(exitSpec) + shrinkVertically(exitSpec)
)
Note that I haven't tried any of this myself, so take it with a grain of salt. Try it out and see how it goes.
k
@Skaldebane a dead simple use of
delayMillis
! How did I miss that?! Thank you so much! It totally works. Fantastic.
K 2
🦜 1
d
Re: why isn't delay supported on spring, Great question. It's conceptually odd to have a delayed spring, particularly when there's an initial velocity that the spring takes some time (i.e. delay) to start reacting to. it's also quite unclear what should happen when such a spring with initial velocity gets interrupted during the delay. Out of curiosity, do you have other cases where you'd find a delayed spring helpful? We may add it but with some caveats if there's compelling use cases. 🙂
k
I can't speak for Houssam, but I would say my use case we just worked through here may be a good example. I know I want 10 things to happen, and I want each of them to happen one after the other. I need a delay set such that the delay is about as "long" (as stiff?) as the running "time" of the previous ones. Also, for those playing along at home, here's how the code ended up:
Copy code
AnimatedVisibility(
    visible = expandedGroups.contains(groupIndex),
    enter =
      fadeIn(tween(200, easing = LinearEasing)) +
        expandVertically(
          tween(
            durationMillis = if (itemIndex < 10) 30 else 0,
            delayMillis = if (modifierIndex < 10) modifierIndex * 30 else 300,
            easing = LinearEasing
          ),
          expandFrom = Alignment.Top,
        ),
    exit =
      fadeOut(tween(500, easing = LinearEasing)) +
        shrinkVertically(
          tween(
            30,
            delayMillis = if (itemIndex > 10) 0 else 300 - (modifierIndex * 30),
            easing = LinearEasing,
          ),
          shrinkTowards = Alignment.Top,
        ),
  ...
  )
This gives us a "total" animation about 300 milliseconds long, and the user sees the first 10 items animate. That way, if the list is really long, the user doesn't have to wait for all of the lower (likely off screen) items to shrink before they see the animation start. The
LinearEasing
was chosen so that it's all one fluid motion (you don't have each row accelerating and decelerating itself). If anyone sees room for improvement/correction, do let me know. Thanks everyone!
🙌 1
s
@Kevin Worth I was a bit skeptical of my own idea, so glad that worked for you! @Doris Liu I also can't think of a good reason for
spring
in particular to have a delay (maybe this use case, where I'd want the more physics-like animations? bounciness?), but I was just wondering about the
delayed
function that's private in Compose... If we use a spring inside of that, would it even work? And how would they even play together? The idea for a generic delaying mechanism still sounds attractive technically, if it works well in a way that's intuitive to us, but yeah there might not be a use-case for such a thing to begin with 🤷‍♂️