Snap to an index Lazyrow

5.4k views Asked by At

I am making a calendar with the help of a lazyRow. I now have the problem that I want the row to snap to the index after a certain scroll amount so it shouldn't be possible to be stuck in between indexes. Is there a way to do that?

    LazyRow(state = calendarViewModel.listState, modifier = Modifier.fillMaxWidth()) {
        calendarYears.forEach {
            items(it.months.count()) { index ->
                calendarViewModel.onEvent(CalendarEvent.ClickedMenuItem(index))
                CalendarRowItem(
                    modifier = Modifier.fillParentMaxWidth(),
                    calendarSize = it.months[index].amountOfDays,
                    initWeekday = it.months[index].startDayOfMonth.ordinal,
                    textColor = MaterialTheme.colors.secondaryVariant,
                    clickedColor = MaterialTheme.colors.primary,
                    textStyle = MaterialTheme.typography.body1
                )
            }
        }
    }
4

There are 4 answers

2
nglauber On BEST ANSWER

You can use the HorizontalPager from accompanist library which provides this fling behavior out-of-the-box and it uses LazyRow internally.

Another option could be use the Snapper library created by @chris-banes

Add the dependency in your build.gradle.

dependencies {
    implementation "dev.chrisbanes.snapper:snapper:<version>"
}

and use it in your LazyRow.

val lazyListState = rememberLazyListState()

LazyRow(
    state = lazyListState,
    flingBehavior = rememberSnapperFlingBehavior(lazyListState),
) {
    // content
}

Result:

enter image description here

3
Gabriele Mariotti On

Starting with compose 1.3.0 you can use the FlingBehavior that performs snapping of items to a given position:

val state = rememberLazyListState()

LazyRow(
    modifier = Modifier.fillMaxSize(),
    verticalAlignment = Alignment.CenterVertically,
    state = state,
    flingBehavior = rememberSnapFlingBehavior(lazyListState = state)
) {
  //item content
}

enter image description here

1
HoltChas On
val centerItemIndex by remember {
    derivedStateOf {
        val layoutInfo = listState.layoutInfo
        val visibleItems = layoutInfo.visibleItemsInfo
        val viewportCenter = (layoutInfo.viewportStartOffset + layoutInfo.viewportEndOffset) / 2

        visibleItems.minByOrNull { abs((it.offset + it.size / 2) - viewportCenter) }?.index ?: 0
    }
}
1
Nimrod Dayan On

Building on top of Gabriele's answer, for snapping based on first visible item (side snapping to the start) we need to do the following:

val scrollState = rememberLazyListState()
val positionInLayout: Density.(Float, Float) -> Float = { _, _ ->
    // This value tells where to snap on the x axis within the viewport
    // Setting it to 0 results in snapping of the first visible item to the left side (or right side if RTL)
    0f
}
val snappingLayout = remember(scrollState) { SnapLayoutInfoProvider(scrollState, positionInLayout) }
val flingBehavior = rememberSnapFlingBehavior(snappingLayout)
LazyRow(
    modifier = modifier,
    state = scrollState,
    contentPadding = PaddingValues(horizontal = 1.5f.u()),
    flingBehavior = flingBehavior,
    content = {}
)