what is the best way to achieve item snapping in a...
# compose
z
what is the best way to achieve item snapping in a custom scrollable layout?
👀 2
This is what I currently have:
I've looked into accompanist pager, but all that code looks confusing, and I'm not sure which part does what
e
You can try to use the nestedScroll modifier callbacks. It can be attached to the parent or directly to the scrollable view.
postFling
method is called when scroll is ended, so you can animate there to some anchor.
z
@ekursakov is there a way to react on scroll or something?
I'm also not sure if nested scroll will work with this, as I'm already using .verticalScroll() modifier
I've finally solved it via FlingBehavior:
Copy code
@Composable
fun IndentedColumn(
    items: List<String> = listOf(
        "One",
        "Two",
        "Three",
        "Four",
        "Five",
        "Six"
    ),
) {
    val scrollState = rememberScrollState()
    val scope = rememberCoroutineScope()
    var size by remember { mutableStateOf(IntSize.Zero) }
    val indices = remember { IntArray(items.size) { 0 } }

    val flingBehavior = object : FlingBehavior {
        override suspend fun ScrollScope.performFling(initialVelocity: Float): Float {
            val value = scrollState.value
            indices.minByOrNull { abs(it - value) }?.let {
                scope.launch {
                    scrollState.animateScrollTo(it)
                }
            }
            return initialVelocity
        }
    }

    Box(modifier = Modifier.onSizeChanged { size = it }) {
        Layout(
            modifier = Modifier
                .verticalScroll(
                    scrollState,
                    flingBehavior = flingBehavior
                )
                .background(Color.Gray),
            content = {
                items.forEach {
                    Text(text = it, color = Color.White)
                }
            }
        ) { measurables, constraints ->
            val itemSpacing = 16.dp.roundToPx()
            var contentHeight = (items.count() - 1) * itemSpacing

            val placeables = measurables.mapIndexed { index, measurable ->
                val placeable = measurable.measure(constraints.copy())
                contentHeight += if (index == 0 || index == measurables.lastIndex) placeable.height / 2 else placeable.height
                placeable
            }

            layout(constraints.maxWidth, size.height + contentHeight) {
                val startOffset = size.height / 2 - placeables[0].height / 2
                var yPosition = startOffset

                val scrollPercent = scrollState.value.toFloat() / scrollState.maxValue

                placeables.forEachIndexed { index, placeable ->
                    val ratio = index.toFloat() / placeables.lastIndex
                    val indent = cos((scrollPercent - ratio) * PI / 2) * size.width / 2

                    placeable.placeRelative(x = indent.toInt(), y = yPosition)
                    indices[index] = yPosition - startOffset
                    yPosition += placeable.height + itemSpacing
                }
            }
        }
    }
}

@Preview
@Composable
fun IndentedColumnPreview() {
    ComposePatternTheme {
        Box(
            Modifier
                .fillMaxWidth()
                .height(300.dp),
            contentAlignment = Alignment.Center
        ) {
            IndentedColumn()
            Divider(color = Color.Red)
            Divider(
                Modifier
                    .fillMaxHeight()
                    .width(1.dp), color = Color.Red
            )
        }
    }
}
🙏 1
🙏🏽 1
a
@Zan Skamljic love this effect. Do you mind if we use it?
z
go ahead
🙏🏽 1