is there a straightforward way to track the decay ...
# compose
j
is there a straightforward way to track the decay of velocity when scrolling LazyColumn? what I’d like is a way to track the decay as follows when I fling on my LazyColumn and it starts scrolling. let’s say the total scroll time for this example will take 2500ms: • at 0ms elapsed, the velocity would be 100 • at 500ms elapsed, the velocity would be 75 • at 1000ms elapsed, the velocity would be 25 • at 2000ms elapsed, the velocity would be 10 • at 2500ms elapsed, the velocity would be 0 (all example data of course, I’m just trying to illustrate the value I’d like to be able to track)
c
I’m not sure there’s a specific LazyColumn way to do it, but you might take a look at a custom modifier with pointerInput and track things with VelocityTracker. Example usage in a snippet: https://gist.github.com/c5inco/6bbf7dd139a6f719d4ba1ba4d174c777#file-swipeablecards-kt-L160
j
thanks for that @Chris Sinco [G], I actually came up with another solution which works but I don’t know if it’s a bad practice or not.. would love your input! I stole the code from LazyColumn’s default flingBehavior which uses
DefaultFlingBehavior()
from Scrollable.kt.. I simply created my own copy of it and I added a parameter which can be updated from within:
fun velocityTrackingFlingBehavior(currentFlingVelocity: MutableState<Float>): FlingBehavior
then where I declare my LazyColumn, I keep the state hoisted above it, something like:
Copy code
val currentFlingVelocity = remember { mutableStateOf(0f) }

LazyColumn(
    ...
    flingBehavior = velocityTrackingFlingBehavior(currentFlingVelocity)
    ...
)
I should add: my reason for doing all of this is I want to use the velocity as a modifier to some behaviour within individual LazyColumn items now I think my next step will be to derive some state from that currentFlingVelocity to debounce and normalise it into values within a small group of ranges, and pass that derived state into my LazyColumn items.. does that sound about right in terms of reducing the impact to performance?
hey @Chris Sinco [G] apologies for the tag, but any chance you can let me know if my idea above is sane or bad? I’ve got it working and it seems performant, but I’m looking for some validation that it’s not a terrible idea 😄
c
Hey James sorry for the delay. Was traveling for work and had some holidays. I’m not the best person to speak to LazyColumn performance, so adding @Andrey Kulikov and @Ben Trengrove [G] who have deeper insights.
🙏 1
🙏 1
b
Andrey is definitely the one to ask. Passing a mutablestate like that to use as an observer is normally not a great idea though, you'll probably tie yourself up in a backwards write eventually https://developer.android.com/jetpack/compose/performance#avoid-backwards Just having a quick read of the DefaultFling code, I wonder if you could instead hoist the AnimationState out so you could read it where you need it. So your constructor would become something like
MyFlingBehaviour(flingDecay, animationState)
. I have no idea if this would work, but that would be what I would have tried first if I was trying to do this
🙏 1
j
cheers Ben! I think the main problem with
MyFlingBehaviour(flingDecay, animationState)
would be that the fling behaviour is on the LazyColumn itself, whereas the
animationState
lives inside individual `item`s, so there are many animation states (which are lazily created as items come into scope)
b
I quickly hacked this up, seems to work
Copy code
@Composable
private fun rememberMyFlingBehavior(): MyFlingBehavior {
    val flingSpec = rememberSplineBasedDecay<Float>()
    return remember(flingSpec) {
        MyFlingBehavior(flingSpec)
    }
}
private class MyFlingBehavior(
    private val flingDecay: DecayAnimationSpec<Float>
) : FlingBehavior {
    private var _lastValue = mutableStateOf(0f)
    val lastValue: State<Float> = _lastValue

    override suspend fun ScrollScope.performFling(initialVelocity: Float): Float {
        // come up with the better threshold, but we need it since spline curve gives us NaNs
        return if (abs(initialVelocity) > 1f) {
            var velocityLeft = initialVelocity
            _lastValue.value = 0f
            AnimationState(
                initialValue = 0f,
                initialVelocity = initialVelocity,
            ).animateDecay(flingDecay) {
                val delta = value - lastValue.value
                val consumed = scrollBy(delta)
                _lastValue.value = value
                velocityLeft = this.velocity
                // avoid rounding errors and stop if anything is unconsumed
                if (abs(delta - consumed) > 0.5f) this.cancelAnimation()
            }
            velocityLeft
        } else {
            initialVelocity
        }
    }
}

val fling = rememberMyFlingBehavior()
println("Fling: ${fling.lastValue.value}")
LazyColumn(
   contentPadding = padding,
   flingBehavior = fling
)
Pretty much the same as what you are doing, but without passing a mutable state around. Be warned, Andrey might still come and say this is a bad idea, he is the lazy list expert
Although it does raise the question of what you are trying to achieve. If it's some sort of animated entry of items perhaps just using the animateEntryExit modifier on the item would be sufficient.
Copy code
itemsIndexed(friends) { index, friend ->
                    Friend(
                        friend = friend,
                        onFriendClick = onFriendClick,
                        modifier = Modifier
                            .padding(horizontal = 16.dp, vertical = 4.dp)
                            .animateEnterExit(
                                enter = slideInVertically(
                                    animationSpec = spring(...)
                                ) { it * (index + 1) } // staggered entrance
                            )
                    )
                }
j
ah nice, your
FlingBehavior
is an improved version of mine! at the very least I will stop passing in the mutable state as a parameter and just expose it, as you’ve shown
as for what I’m trying to achieve here.. I was waiting for someone to ask that! when we use an animated fade in for individual items, it looks great when you scroll at a slow or normal speed.. the problem comes in when you start to scroll very fast.. when you scroll very fast, the fade in animation doesn’t have time to set the alpha level to a level that is visible, and the effect is that the list items look blank if you scroll too fast it’s not the worst thing in the world, but IMO it looks a little silly and we can do better.. so I set out to build this mechanism
so what I built takes that
currentFlingVelocity
which I’m now tracking, and does this:
Copy code
val normalizedVelocity by remember {
    derivedStateOf {
        normalizeVelocity(currentFlingVelocity.value)
    }
}
my
normalizedVelocity()
function does some calculations and ensures it will only ever return one of 6 possible values (so there’s 6 condensed “scroll speeds”) now, I pass the
normalizedVelocity
value down into each
item
(remembering this is still performant because I’ve used derivedStateOf), and then where I have my fade in animation, I provide the following animationSpec:
Copy code
animationSpec = tween(velocityBasedFadeInDuration(normalizedScrollVelocity))
that
velocityBasedFadeInDuration()
function simply returns a particular value in millis, depending on the normalized velocity that was passed in.. so you get 6 possible fade in durations, which change depending on how fast or slow you scroll
it might sound outrageous but it looks really good (in my eyes at least 😅)
hmm, and as I typed this out to you it occurred to me that I would actually be better off passing in a lambda to each item which can be invoked and return
normalizedScrollVelocity
, instead of passing
normalizedScrollVelocity
down and causing re-composition
cheers for rubber ducking for me Ben 😄 lunch break’s over, I’ve gotta get back to work things but I’ll come back to this tonight and make the change you suggested for the custom FlingBehavior, plus the change I just mentioned regarding passing a function into the items rather than the normalised velocity
b
That all sounds very fair and cool!
j
didn’t get back to this last night due to my newborn daughter being too bloody cute for me to resist giving 100% of my attention, but I jumped into it early this morning and made those changes.. all works great! and even though I didn’t feel like performance was an issue before, it’s certainly more performant now with the lambda rather than the normalised velocity going into the individual items 🎉