How to retrieve the scrolling direction for LazyRow
Asked Answered
I

5

14

For a LazyRow, or Column, how to I know whether the user has scrolled left or right ( or up or... you know). We do not need callbacks in compose for stuff like that, since mutableStateOf objects always anyway trigger recompositions so I just wish to know a way to store it in a variable. Okay so there's lazyRowState.firstVisibleItemScrollOffset, which can be used to mesaure it in a way, but I can't find a way to store its value first, and then subtract the current value to retrieve the direction (based on positive or negative change). Any ideas on how to do that, thanks

Isocrates answered 10/8, 2021 at 13:37 Comment(0)
M
39

Currently there is no built-in function to get this info from LazyListState.

You can use something like:

@Composable
private fun LazyListState.isScrollingUp(): Boolean {
    var previousIndex by remember(this) { mutableStateOf(firstVisibleItemIndex) }
    var previousScrollOffset by remember(this) { mutableStateOf(firstVisibleItemScrollOffset) }
    return remember(this) {
        derivedStateOf {
            if (previousIndex != firstVisibleItemIndex) {
                previousIndex > firstVisibleItemIndex
            } else {
                previousScrollOffset >= firstVisibleItemScrollOffset
            }.also {
                previousIndex = firstVisibleItemIndex
                previousScrollOffset = firstVisibleItemScrollOffset
            }
        }
    }.value
}

Then just use listState.isScrollingUp() to get the info about the scroll.

This snippet is used in a google codelab.

Mertiemerton answered 10/8, 2021 at 17:58 Comment(3)
Would that not be triggered only upon reaching a specific offset? I mean when the scroll offset reaches a specific portion, then only does the firstVisibleItemIndex change (I think it does when the current first item is scrolled completely off the screen.) I mean if the padding between the items themselves is around 15 DPs, and I scroll by 5 dp to the right, it won't be recorded here. Check out my answer above, it uses the offset property instead of index, so it should give continuos updates, and hey isn't the latest version 1.0.1?Isocrates
Oh wait! It's perfect! Didn't see the conditional logic. Thanks!Isocrates
Anything wrong with my implementation below by the way? Seems kinda simplerIsocrates
M
2

This extensions functions can be very handy for you:

fun LazyListState.isFirstItemVisible() = firstVisibleItemIndex == 0

@Composable
fun LazyListState.isScrollingDown(): Boolean {
    val offset by remember(this) { mutableStateOf(firstVisibleItemScrollOffset) }
    return remember(this) { derivedStateOf { (firstVisibleItemScrollOffset - offset) > 0 } }.value
}

@Composable
fun LazyListState.isScrollingUp(): Boolean {
    val offset by remember(this) { mutableStateOf(firstVisibleItemScrollOffset) }
    return remember(this) { derivedStateOf { (firstVisibleItemScrollOffset - offset) < 0 } }.value
}
Menace answered 4/4, 2023 at 16:18 Comment(0)
I
1

Got it

{ //Composable Scope
val lazyRowState = rememberLazyListState()
    val pOffset = remember { lazyRowState.firstVisibleItemScrollOffset }
    val direc = lazyRowState.firstVisibleItemScrollOffset - pOffset
    val scrollingRight /*or Down*/ = direc > 0 // Tad'aa
}
Isocrates answered 10/8, 2021 at 13:42 Comment(2)
I was playing with this a bit. I was not sure, but now I know that the scrolloffset is a per-item value, so it resets when you cross to boundary to the next item. Which means that this will give false reads when crossing the item boundary. Means you have to take both into acccount, item and offset.Chaetognath
Thanks for the insight, seems reasonable (TLDR tho, sorry).Isocrates
T
0

For LazyColumn with specifying the minimum detection step:

import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.State
import androidx.compose.runtime.produceState
import kotlinx.coroutines.delay
import kotlin.math.absoluteValue

    @Composable
    fun LazyListState.verticalDebouncedScrollState(
        minOffsetForDetection: Int = 10
    ): State<VerticalScrollDirection> = produceState(
        initialValue = VerticalScrollDirection.None
    ) {
        var previousScrollOffset = firstVisibleItemScrollOffset
        var previousItemIndex = firstVisibleItemIndex
        while (true) {
            delay(300L)
            val capturedItemIndex = firstVisibleItemIndex
            val capturedScrollOffset = firstVisibleItemScrollOffset
            val offsetDelta = capturedScrollOffset - previousScrollOffset
            val itemIndexDelta = capturedItemIndex - previousItemIndex
            value = when {
                offsetDelta.absoluteValue < minOffsetForDetection && itemIndexDelta == 0 -> VerticalScrollDirection.None
                itemIndexDelta > 0 -> VerticalScrollDirection.Up
                itemIndexDelta == 0 && offsetDelta > 0 -> VerticalScrollDirection.Up
                else -> VerticalScrollDirection.Down
            }
            previousScrollOffset = capturedScrollOffset
            previousItemIndex = capturedItemIndex
        }
    }
    
    enum class VerticalScrollDirection {
        None, Up, Down
    }
Tellurian answered 17/7 at 10:37 Comment(0)
P
-1

Due to false positives when the firstVisibleItemIndex is changed, I ended up with the following version:

@Composable
fun LazyListState.isScrollingUp(): Boolean {
    var previousItemIndex by remember(this) { mutableIntStateOf(firstVisibleItemIndex) }
    var previousScrollOffset by remember(this) { mutableIntStateOf(firstVisibleItemScrollOffset) }
    var scrollingUp by remember(this) { mutableStateOf(true) }

    return remember(this) {
        derivedStateOf {
            if (previousItemIndex == firstVisibleItemIndex) {
                scrollingUp = firstVisibleItemScrollOffset - previousScrollOffset <= 0
            } else {
                previousItemIndex = firstVisibleItemIndex
            }

            previousScrollOffset = firstVisibleItemScrollOffset

            scrollingUp
        }
    }.value
}

The scroll state is cached and preserved while firstVisibleItemIndex is changed.

Parceling answered 17/10, 2023 at 15:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.