Jetpack Compose LazyRow scroll with snap only to start of next or previous element

Viewed 1278
2 Answers

You can check the scrolling direction like so

@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
}

Of course, you will need to create a rememberLazyListState(), and then pass it to the list as a parameter.

Then, based upon the scrolling direction, you can call lazyListState.scrollTo(lazyListState.firstVisibleItemIndex + 1) in a coroutine (if the user is scrolling right), and appropriate calls for the other direction.

(Example for a horizontal LazyRow)

You could do a scroll to the next or previous item to create a snap effect. Check the offset of the first visible item to see which item of the list takes up more screen space and then scroll left or right to the most visible one.

@Composable
fun SnappyLazyRow() {
    val listState = rememberLazyListState()
    val coroutineScope = rememberCoroutineScope()

    LazyRow(
        state = listState,
        modifier = Modifier.fillMaxSize(),
        content = {
            items(/*list of items*/) { index ->
                /* Item view */

                if(!listState.isScrollInProgress){
                    if(listState.isHalfPastItemLeft())
                        coroutineScope.scrollBasic(listState, left = true)
                    else
                        coroutineScope.scrollBasic(listState)

                    if(listState.isHalfPastItemRight())
                        coroutineScope.scrollBasic(listState)
                    else
                        coroutineScope.scrollBasic(listState, left = true)
                }
            }
        })
}

private fun CoroutineScope.scrollBasic(listState: LazyListState, left: Boolean = false){
    launch {
        val pos = if(left) listState.firstVisibleItemIndex else listState.firstVisibleItemIndex+1
        listState.animateScrollToItem(pos)
    }
}

@Composable
private fun LazyListState.isHalfPastItemRight(): Boolean {
    return firstVisibleItemScrollOffset > 500
}

@Composable
private fun LazyListState.isHalfPastItemLeft(): Boolean {
    return firstVisibleItemScrollOffset <= 500
}
Related