5

How can I detect when they user is scrolling up or down in a LazyColumn? I'm trying to hide an element on the screen when the user scrolls down and show it again when the user begins to scroll upwards.

2 Answers 2

17

This can be done by comparing lazyListState.firstVisibleItemIndex and lazyListState.firstVisibleItemScrollOffset with their previous values.

enter image description here

You can encapsulate this logic like LazyListState does

class DirectionalLazyListState(
    private val lazyListState: LazyListState
) {
    private var positionY = lazyListState.firstVisibleItemScrollOffset
    private var visibleItem = lazyListState.firstVisibleItemIndex


    val scrollDirection by derivedStateOf {
        if (lazyListState.isScrollInProgress.not()) {
            ScrollDirection.None
        } else {
            val firstVisibleItemIndex = lazyListState.firstVisibleItemIndex
            val firstVisibleItemScrollOffset =
                lazyListState.firstVisibleItemScrollOffset

            // We are scrolling while first visible item hasn't changed yet
            if (firstVisibleItemIndex == visibleItem) {
                val direction = if (firstVisibleItemScrollOffset > positionY) {
                    ScrollDirection.Down
                } else {
                    ScrollDirection.Up
                }
                positionY = firstVisibleItemScrollOffset

                direction
            } else {

                val direction = if (firstVisibleItemIndex > visibleItem) {
                    ScrollDirection.Down
                } else {
                    ScrollDirection.Up
                }
                positionY = firstVisibleItemScrollOffset
                visibleItem = firstVisibleItemIndex
                direction
            }
        }
    }
}

Call constructor via remember

@Composable
fun rememberDirectionalLazyListState(
    lazyListState: LazyListState,
): DirectionalLazyListState {
    return remember {
        DirectionalLazyListState(lazyListState)
    }
}

And use it

val lazyListState = rememberLazyListState()
val directionalLazyListState = rememberDirectionalLazyListState(
    lazyListState
)

Enum class

enum class ScrollDirection {
    Up, Down, None
}

Full demo

@Preview
@Composable
private fun ScrollDirectionSample() {

    val lazyListState = rememberLazyListState()
    val directionalLazyListState = rememberDirectionalLazyListState(
        lazyListState
    )

    val text by remember {
        derivedStateOf {
            "isScrollInProgress: ${lazyListState.isScrollInProgress}\n" +
                    "firstVisibleItemIndex: ${lazyListState.firstVisibleItemIndex}\n" +
                    "firstVisibleItemScrollOffset: ${lazyListState.firstVisibleItemScrollOffset}"
        }
    }


    val color = when (directionalLazyListState.scrollDirection) {
        ScrollDirection.Up -> Color.Green
        ScrollDirection.Down -> Color.Blue
        else -> Color.Black
    }

    Column {

        Text(text, fontSize = 16.sp)
        Text(
            "Direction: ${directionalLazyListState.scrollDirection}",
            fontSize = 24.sp,
            color = color,
            fontWeight = FontWeight.Bold
        )
        LazyColumn(
            state = lazyListState,
            modifier = Modifier.fillMaxSize()
        ) {
            items(50) {
                Text(
                    text = "Row $it",
                    fontSize = 22.sp,
                    color = Color.White,
                    modifier = Modifier
                        .fillMaxWidth()
                        .background(Color.Red)
                        .padding(8.dp)
                )
            }
        }
    }
}

The one above can't determine whether finger is moving or down or idle while it's pressed because isScrollInProgress returns true when pressed whether pointer is not moving or not. You can add a timeout to set to none if user doesn't move pointer while pointer is pressed or isScrollInProgress is true.

@Stable
class DirectionalLazyListState(
    private val lazyListState: LazyListState,
    private val coroutineScope: CoroutineScope
)  {
    private var positionY = lazyListState.firstVisibleItemScrollOffset
    private var visibleItem = lazyListState.firstVisibleItemIndex

    private var currentTime = System.currentTimeMillis()
    var scrollDirection by mutableStateOf(ScrollDirection.None)

    init {

        coroutineScope.launch {
            while (isActive) {
                delay(120)
                if (System.currentTimeMillis() - currentTime > 120) {
                    scrollDirection = ScrollDirection.None
                }
            }
        }

        snapshotFlow {
            val scrollInt = if (lazyListState.isScrollInProgress) 20000 else 10000
            val visibleItemInt = lazyListState.firstVisibleItemIndex * 10
            scrollInt + visibleItemInt + lazyListState.firstVisibleItemScrollOffset
        }
            .onEach {
                if (lazyListState.isScrollInProgress.not()) {
                    scrollDirection = ScrollDirection.None
                } else {

                    currentTime = System.currentTimeMillis()

                    val firstVisibleItemIndex = lazyListState.firstVisibleItemIndex
                    val firstVisibleItemScrollOffset =
                        lazyListState.firstVisibleItemScrollOffset

                    // We are scrolling while first visible item hasn't changed yet
                    if (firstVisibleItemIndex == visibleItem) {
                        val direction = if (firstVisibleItemScrollOffset > positionY) {
                            ScrollDirection.Down
                        } else {
                            ScrollDirection.Up
                        }
                        positionY = firstVisibleItemScrollOffset

                        scrollDirection = direction
                    } else {

                        val direction = if (firstVisibleItemIndex > visibleItem) {
                            ScrollDirection.Down
                        } else {
                            ScrollDirection.Up
                        }
                        positionY = firstVisibleItemScrollOffset
                        visibleItem = firstVisibleItemIndex
                        scrollDirection = direction
                    }
                }
            }
            .launchIn(coroutineScope)
    }


//    val scrollDirection by derivedStateOf {
//        if (lazyListState.isScrollInProgress.not()) {
//            ScrollDirection.None
//        } else {
//            val firstVisibleItemIndex = lazyListState.firstVisibleItemIndex
//            val firstVisibleItemScrollOffset =
//                lazyListState.firstVisibleItemScrollOffset
//
//            // We are scrolling while first visible item hasn't changed yet
//            if (firstVisibleItemIndex == visibleItem) {
//                val direction = if (firstVisibleItemScrollOffset > positionY) {
//                    ScrollDirection.Down
//                } else {
//                    ScrollDirection.Up
//                }
//                positionY = firstVisibleItemScrollOffset
//
//                direction
//            } else {
//
//                val direction = if (firstVisibleItemIndex > visibleItem) {
//                    ScrollDirection.Down
//                } else {
//                    ScrollDirection.Up
//                }
//                positionY = firstVisibleItemScrollOffset
//                visibleItem = firstVisibleItemIndex
//                direction
//            }
//        }
//    }
}
Sign up to request clarification or add additional context in comments.

1 Comment

Very informative answer.
0

Try to intercept gesture:

val listState = rememberLazyListState()
val isFocused by listState.interactionSource.interactions
            .distinctUntilChanged()
            .filterIsInstance<DragInteraction>()
            .map { dragInteraction -> 
                dragInteraction is DragInteraction.Start 
             }
            .collectAsState(false)
LaunchedEffect(isFocused) {
    println("isFocused: $isFocused")            
}
LazyColumn(
    state = listState,
) {
//...
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.