Luke
Luke

Reputation: 2599

Handle scroll in Jetpack Compose `LazyLayout`

Recently LazyLayout for compose was shared as experimental to try things out. It's used by other lazy implementations for list (column / row) and grid. I wanted to try this out and implement a simple custom lazy layout to consume scrolling and display items lazily (similarly to LazyList). The code below is dummy, but the idea is to do something on scrolling offset (in this example I'm placing 3 items with a space of scroll offset).

The problem is that on scroll all composables disappear with layout block being called but it doesn't place anything (only initial composition). The answer probably lies somewhere in LazyList or LazyGrid, but I wanted to avoid copy paste and do something incrementally. I'd appreciate if someone could shed some light on this issue, how to actually handle scrolling with LazyLayout.

I tried replacing scrollable with verticalScroll but it gives same results.

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun CustomLazyLayout(
    modifier: Modifier = Modifier,
    content: @Composable (String) -> Unit
) {
    val scrollState = rememberScrollState()
    var firstId by remember { mutableStateOf(0) }
    var firstOffset by remember { mutableStateOf(0) }
    LazyLayout(
        modifier = modifier
            .clipScrollableContainer(Orientation.Vertical)
            .scrollable(
                orientation = Orientation.Vertical,
                reverseDirection = false,
                interactionSource = null,
                flingBehavior = ScrollableDefaults.flingBehavior(),
                state = scrollState,
                overscrollEffect = ScrollableDefaults.overscrollEffect(),
                enabled = true
            ),
        itemProvider = object : LazyLayoutItemProvider {
            override val itemCount: Int
                get() = 10

            @Composable
            override fun Item(index: Int) {
                content("$index")
            }

        },
        measurePolicy = { constraints ->
            val list = mutableListOf<Placeable>()

            var i = firstId
            while(i < firstId+3) {
                val m = measure(i, constraints)
                if (m.isNotEmpty()) {
                    list.add(m[0])
                }
                ++i
            }
            val height = list.first().measuredHeight
            var positionY = 0
            layout(constraints.maxWidth, constraints.maxHeight) {
                for (placeable in list) {
                    placeable.place(0, positionY + scrollState.value)
                    positionY += height
                }
            }
        }
    )

Upvotes: 4

Views: 1536

Answers (1)

Luke
Luke

Reputation: 2599

Problem is solved if I move scrollableState outside of layout block to measurePolicy. To be precise this code will actually mimic regular scroll effect:

        var positionY = -scrollState.value
        layout(constraints.maxWidth, constraints.maxHeight) {
            for (placeable in list) {
                placeable.place(0, positionY)
                positionY += height
            }
        }

Previously on scroll only layout block was called. Now both are (measure and layout). I'm still quite unsure if I'm doing it the right way, but items are composed only once so I guess it's working.

Upvotes: 2

Related Questions