Search code examples
kotlinscrollandroid-jetpack-composeandroid-jetpack-compose-lazy-column

How to detect if user is scrolling up/down LazyColumn - Jetpack Compose


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.


Solution

  • 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
    //            }
    //        }
    //    }
    }