androidkotlinandroid-jetpack-compose

How to ignore content padding/spacing between items in LazyVerticalGrid for certain items?


I have this design where there's a header that fills the max width with grid items underneath.

The whole screen is meant to be scrollable (the heading is meant to scroll up with the content)

So the code might look like:

LazyVerticalGrid(
    columns = GridCells.Fixed(count = 2),
    contentPadding = PaddingValues(horizontal = 16.dp, vertical = 24.dp),
    verticalArrangement = Arrangement.spacedBy(24.dp),
    horizontalArrangement = Arrangement.spacedBy(24.dp) {
            item(span = {
                GridItemSpan(maxLineSpan)
            }) {
                Text("Heading")
            }

            items(contents) {
                Text("${it.name}")
            }
}

So we have a heading that fills the max span for the first item.

We also want to have a grid of items, with 24.dp between them and content padding of 16.dp horizontal and 24.dp vertical.

The issue is how since the LazyVerticalGrid is going to see the first heading item and apply the same padding to it.

Does anyone have any ideas on how to get around this?

You could remove those padding modifier from the LazyVerticalGrid and apply it to the grid items themselves, but you'll have the classic issue back with recycler views where the items double up on padding etc.


Solution

  • You can do it in many ways.

    enter image description here

    First one is changing how you measure a Composable if you don't want it to be limited by parent width - minus horizontal padding. This is also used in source code of some Composables.

    val density = LocalDensity.current
    val offsetPx = with(density) {
        16.dp.roundToPx()
    }
    
    Text(
        modifier = Modifier
            .layout { measurable, constraints ->
                val looseConstraints = constraints.offset(offsetPx * 2, 0)
                val placeable = measurable.measure(looseConstraints)
                layout(placeable.width, placeable.height) {
                    placeable.placeRelative(0, 0)
                }
            }
            .fillMaxWidth()
            .border(2.dp, Color.Magenta),
        text = "Heading"
    )
    

    layout modifier lets you change how you measure and position a Composable. By adding 16.dp in pixel value to both sides using constraints.offset(), this is also used for creating Padding Modifier, You can measure with full size of parent without padding

    Second way is simply using Modifier.offset()

    Text(
        modifier = Modifier
            .offset((-16).dp, 0.dp)
            .fillMaxWidth()
            .border(2.dp, Color.Black),
        text = "Heading"
    )
    

    which is also using placeable positioning under the hood

    private class OffsetModifier(
        val x: Dp,
        val y: Dp,
        val rtlAware: Boolean,
        inspectorInfo: InspectorInfo.() -> Unit
    ) : LayoutModifier, InspectorValueInfo(inspectorInfo) {
        override fun MeasureScope.measure(
            measurable: Measurable,
            constraints: Constraints
        ): MeasureResult {
            val placeable = measurable.measure(constraints)
            return layout(placeable.width, placeable.height) {
                if (rtlAware) {
                    placeable.placeRelative(x.roundToPx(), y.roundToPx())
                } else {
                    placeable.place(x.roundToPx(), y.roundToPx())
                }
            }
        }
    }
    

    3rd option would be using Modifier.graphicsLayer{}

    Text(
        modifier = Modifier
            .graphicsLayer { 
                translationX = -16.dp.toPx()
            }
            .fillMaxWidth()
            .border(2.dp, Color.Yellow),
        text = "Heading"
    )
    

    And full code to try each one out. You can also create a custom layout as well.

    @Preview
    @Composable
    private fun Test() {
        Column(
            Modifier
                .fillMaxSize()
                .padding(20.dp)
        ) {
    
    
            val density = LocalDensity.current
            val offsetPx = with(density) {
                16.dp.roundToPx()
            }
            LazyVerticalGrid(
                modifier = Modifier.border(2.dp, Color.Red),
                columns = GridCells.Fixed(count = 2),
                contentPadding = PaddingValues(horizontal = 16.dp, vertical = 24.dp),
                verticalArrangement = Arrangement.spacedBy(24.dp),
                horizontalArrangement = Arrangement.spacedBy(24.dp)
            ) {
                item(span = {
                    GridItemSpan(
    
                        maxLineSpan
                    )
                }) {
                    Text(
                        modifier = Modifier
                            .layout { measurable, constraints ->
                                val looseConstraints = constraints.offset(offsetPx * 2, 0)
                                val placeable = measurable.measure(looseConstraints)
                                layout(placeable.width, placeable.height) {
                                    placeable.placeRelative(0, 0)
                                }
                            }
                            .fillMaxWidth()
                            .border(2.dp, Color.Magenta),
                        text = "Heading"
                    )
                }
    
                item(span = {
                    GridItemSpan(
    
                        maxLineSpan
                    )
                }) {
                    Text(
                        modifier = Modifier
                            .offset((-16).dp, 0.dp)
                            .fillMaxWidth()
                            .border(2.dp, Color.Black),
                        text = "Heading"
                    )
                }
    
                item(span = {
                    GridItemSpan(
    
                        maxLineSpan
                    )
                }) {
                    Text(
                        modifier = Modifier
                            .graphicsLayer {
                                translationX = -16.dp.toPx()
                            }
                            .fillMaxWidth()
                            .border(2.dp, Color.Yellow),
                        text = "Heading"
                    )
                }
    
                items(10) {
                    Text(
                        modifier = Modifier.border(2.dp, Color.Green),
                        text = "Text $it"
                    )
                }
            }
        }
    }
    

    And if you wonder Padding Modifier is as

    private class PaddingModifier(
        val start: Dp = 0.dp,
        val top: Dp = 0.dp,
        val end: Dp = 0.dp,
        val bottom: Dp = 0.dp,
        val rtlAware: Boolean,
        inspectorInfo: InspectorInfo.() -> Unit
    ) : LayoutModifier, InspectorValueInfo(inspectorInfo) {
        init {
            require(
                (start.value >= 0f || start == Dp.Unspecified) &&
                    (top.value >= 0f || top == Dp.Unspecified) &&
                    (end.value >= 0f || end == Dp.Unspecified) &&
                    (bottom.value >= 0f || bottom == Dp.Unspecified)
            ) {
                "Padding must be non-negative"
            }
        }
    
        override fun MeasureScope.measure(
            measurable: Measurable,
            constraints: Constraints
        ): MeasureResult {
    
            val horizontal = start.roundToPx() + end.roundToPx()
            val vertical = top.roundToPx() + bottom.roundToPx()
    
            val placeable = measurable.measure(constraints.offset(-horizontal, -vertical))
    
            val width = constraints.constrainWidth(placeable.width + horizontal)
            val height = constraints.constrainHeight(placeable.height + vertical)
            return layout(width, height) {
                if (rtlAware) {
                    placeable.placeRelative(start.roundToPx(), top.roundToPx())
                } else {
                    placeable.place(start.roundToPx(), top.roundToPx())
                }
            }
        }
    }