Richard Onslow Roper
Richard Onslow Roper

Reputation: 6817

How to retrieve the scrolling direction for LazyRow

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

Upvotes: 15

Views: 8734

Answers (6)

Ben Trengrove
Ben Trengrove

Reputation: 8719

There are now APIs for this in Compose.

lazyListState.lastScrolledBackward & lazyListState.lastScrolledForward

See https://developer.android.com/reference/kotlin/androidx/compose/foundation/lazy/LazyListState#lastScrolledBackward()

Upvotes: 1

Raevski Anatoly
Raevski Anatoly

Reputation: 11

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
    }

Upvotes: 1

Adrian
Adrian

Reputation: 735

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.

Upvotes: -1

Vansuita Jr.
Vansuita Jr.

Reputation: 2099

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
}

Upvotes: 2

Gabriele Mariotti
Gabriele Mariotti

Reputation: 363439

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.

Upvotes: 41

Richard Onslow Roper
Richard Onslow Roper

Reputation: 6817

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
}

Upvotes: 1

Related Questions