https://kotlinlang.org logo
Title
z

Zan Skamljic

05/28/2021, 6:47 AM
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

ekursakov

05/28/2021, 10:53 AM
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

Zan Skamljic

05/28/2021, 12:31 PM
@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:
@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

Ash

05/28/2021, 2:31 PM
@Zan Skamljic love this effect. Do you mind if we use it?
z

Zan Skamljic

05/28/2021, 3:31 PM
go ahead
🙏🏽 1