1

I have a CustomGrid that is inside a Column. That column is inside a HorizontalPager and the HorizontalPager is inside another column where there are other elements. I want the Grid to grow in height as much as necessary, but whenever I add fillMaxHeight() or wrapContentSize() or another equivalent method, the application crashes with the error you can see in the title. Is there anything I can do to fix this error and have the Grid take up as much space as I need? I leave you the prints of my Grid, which is custom, and the respective components.

Custom Grid code:

import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.ParentDataModifier
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import kotlin.math.max

interface GridScope {
    @Stable
    fun Modifier.span(columns: Int = 1, rows: Int = 1) = this.then(
        GridData(columns, rows)
    )

    companion object : GridScope
}

private class GridData(
    val columnSpan: Int,
    val rowSpan: Int,
) : ParentDataModifier {

    override fun Density.modifyParentData(parentData: Any?): Any = this@GridData

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false

        other as GridData

        if (columnSpan != other.columnSpan) return false
        if (rowSpan != other.rowSpan) return false

        return true
    }

    override fun hashCode(): Int {
        var result = columnSpan
        result = 31 * result + rowSpan
        return result
    }
}

private val Measurable.gridData: GridData?
    get() = parentData as? GridData

private val Measurable.columnSpan: Int
    get() = gridData?.columnSpan ?: 1

private val Measurable.rowSpan: Int
    get() = gridData?.rowSpan ?: 1

data class GridInfo(
    val numChildren: Int,
    val columnSpan: Int,
    val rowSpan: Int,
)

@Composable
fun Grid(
    columns: Int,
    modifier: Modifier = Modifier,
    content: @Composable GridScope.() -> Unit,
) {
    check(columns > 0) { "Columns must be greater than 0" }
    Layout(
        content = { GridScope.content() },
        modifier = modifier,
    ) { measurables, constraints ->
        // calculate how many rows we need
        val standardGrid = GridData(1, 1)
        val spans = measurables.map { measurable -> measurable.gridData ?: standardGrid }
        val gridInfo = calculateGridInfo(spans, columns)
        val rows = gridInfo.sumOf { it.rowSpan }

        // build constraints
        val baseConstraints = Constraints.fixed(
            width = constraints.maxWidth / columns,
            height = constraints.maxHeight / rows,
        )
        val cellConstraints = measurables.map { measurable ->
            val columnSpan = measurable.columnSpan
            val rowSpan = measurable.rowSpan
            Constraints.fixed(
                width = baseConstraints.maxWidth * columnSpan,
                height = baseConstraints.maxHeight * rowSpan
            )
        }

        // measure children
        val placeables = measurables.mapIndexed { index, measurable ->
            measurable.measure(cellConstraints[index])
        }

        // place children
        layout(
            width = constraints.maxWidth,
            height = constraints.maxHeight,
        ) {
            var x = 0
            var y = 0
            var childIndex = 0
            gridInfo.forEach { info ->
                repeat(info.numChildren) {
                    val placeable = placeables[childIndex++]
                    placeable.placeRelative(
                        x = x,
                        y = y,
                    )
                    x += placeable.width
                }
                x = 0
                y += info.rowSpan * baseConstraints.maxHeight
            }
        }
    }
}

private fun calculateGridInfo(
    spans: List<GridData>,
    columns: Int,
): List<GridInfo> {
    var currentColumnSpan = 0
    var currentRowSpan = 0
    var numChildren = 0
    return buildList {
        spans.forEach { span ->
            val columnSpan = span.columnSpan.coerceAtMost(columns)
            val rowSpan = span.rowSpan
            if (currentColumnSpan + columnSpan <= columns) {
                currentColumnSpan += columnSpan
                currentRowSpan = max(currentRowSpan, rowSpan)
                ++numChildren
            } else {
                add(
                    GridInfo(
                        numChildren = numChildren,
                        columnSpan = currentColumnSpan,
                        rowSpan = currentRowSpan
                    )
                )
                currentColumnSpan = columnSpan
                currentRowSpan = rowSpan
                numChildren = 1
            }
        }
        add(
            GridInfo(
                numChildren = numChildren,
                columnSpan = currentColumnSpan,
                rowSpan = currentRowSpan,
            )
        )
    }
}

Code where Grid will be inserted or other components, it is generated based on data that comes from API:

Box(
        modifier = Modifier
            .fillMaxSize()
            .background(backgroundColor.value)
    ) {

        Column(
            horizontalAlignment = Alignment.CenterHorizontally,
            modifier = Modifier
                .fillMaxSize()
                .verticalScroll(rememberScrollState())
        ) {

            // We only consume nested flings in the main-axis, allowing cross-axis flings to propagate
            // as normal
            val consumeFlingNestedScrollConnection =
                remember { ConsumeFlingNestedScrollConnection(consumeHorizontal = true) }

            Column(
                modifier = Modifier
                    .background(
                        backgroundColor.value,
                        RoundedCornerShape(topStart = Dimen30, topEnd = Dimen30)
                    )
                    .nestedScroll(connection = consumeFlingNestedScrollConnection)
                    .fillMaxWidth()
            ) {
                HorizontalPager(
                    count = size,
                    state = pagerState,
                    itemSpacing = Dimen20,
                    modifier = Modifier.padding(top = Dimen33),
                    userScrollEnabled = false
                ) { page ->
                    Column(
                        modifier = Modifier
                            // We don't any nested flings to continue in the pager, so we add a
                            // connection which consumes them.
                            .nestedScroll(connection = consumeFlingNestedScrollConnection)
                            // Constraint the content width to be <= than the width of the pager.
                            .fillParentMaxWidth()
                            .wrapContentSize()
                    ) {
                        // content (where grid could be, content is generated dinamically based on data that comes from api
                    }
                }
            }
        }
    }

How Grid is added to that layout:

Grid(
                columns = 5,
                modifier = Modifier
                    .padding(start = Dimen20, end = Dimen20, top = Dimen16)
                    .fillMaxWidth()
                    .wrapContentSize()
            ) {
// cards content
}

The crash points to the baseConstraints of the Grid code, but I can't figure out why and I can't solve the problem.

R0ck
  • 409
  • 1
  • 15

1 Answers1

4

When you use Constraints.fixed() you need to have Constraints maxWidth and maxHeight that are not infinite.

When you apply Modifier.verticalScroll you transform your maxHeight to Constraints.Infinity which is 2147483647. I explained in detail in this answer about constraints with vertical scroll and Constraints.

And created a sample to show why it crashes

@Composable
private fun Grid(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {

    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->

        val placeables = measurables.map { measurable: Measurable ->
            measurable.measure(
                Constraints.fixed(
                    width = constraints.maxWidth,
                    height = constraints.maxHeight
                )
            )
        }
        layout(constraints.maxWidth, constraints.maxHeight) {
            placeables.forEach {
                it.placeRelative(0, 0)
            }
        }
    }
}

demonstration why it crashes

Column(
    modifier = Modifier
        .fillMaxSize()
        .verticalScroll(rememberScrollState())
) {
    HorizontalPager(count = 3) {
        Column(
            modifier = Modifier.wrapContentSize()
        ) {
            Grid() {
                BoxWithConstraints() {
                    Text("Constraints: ${this.constraints}")
                }
            }
        }
    }
}

What you should be doing is to constrain maxHeight to parent or screen size.

One, and simple way to do is, passing it via Modifier.onSizeChanged to your Layout, but be careful using Modifier.onSizeChanged which you might trigger recomposition continuously when mutableState is used to update or set size of another Composable.

var height by remember {
    mutableStateOf(0)
}
Box(
    modifier = Modifier
        .fillMaxSize()
        .onSizeChanged {
            if (height == 0) {
                height = it.height
            }
        }
){
    Column(
        modifier = Modifier
            .fillMaxSize()
            .verticalScroll(rememberScrollState())
    ) {
        HorizontalPager(count = 3) {
            Column(
                modifier = Modifier.wrapContentSize()
            ) {
                Grid(height = height) {
                    BoxWithConstraints {
                        Text("Constraints: ${this.constraints}")
                    }
                }
            }
        }
    }
}

And use parent height instead of infinite height due to Modifier.verticalScroll

@Composable
private fun Grid(
    modifier: Modifier = Modifier,
    height:Int,
    content: @Composable () -> Unit
) {

    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->

        val placeables = measurables.map { measurable: Measurable ->
            measurable.measure(
                Constraints.fixed(
                    width = constraints.maxWidth,
                    height = constraints.maxHeight.coerceAtMost(height)
                )
            )
        }
        layout(constraints.maxWidth, constraints.maxHeight) {
            placeables.forEach {
                it.placeRelative(0, 0)
            }
        }
    }
}

Changing your Compsable based on sample above you can achieve what you wish to create

Thracian
  • 43,021
  • 16
  • 133
  • 222
  • Thanks for the answer. I was able to understand why it happens, but with the way you suggested, my items on the grid don't appear. I need to have an item per line that occupies 60% of the width and another 40%. How can I do that with your logic? – R0ck Nov 25 '22 at 17:02
  • It's not easy to test from the code you shared but you can either debug or print logs what `cellConstraints`contain as min/max width and height, placable dimensions are and check out `layout(constraints.maxWidth, constraints.MaxHeight)`. one or multiple of these probably needs to be updated – Thracian Nov 25 '22 at 17:06
  • If placeable dimension are correct, 60% of the width and another 40% and heights of your grid items are finite then you might need to update layout height or width – Thracian Nov 25 '22 at 17:09
  • Cell constraint are not used with your logic. On Grid method, I just replaced my placeable with your placeables, it crashes on baseContraints, probably because the calculations will still be made. – R0ck Nov 25 '22 at 17:20
  • This is not a method. You need to limit fixed constraints finite for `Constraints.fixed`. You can update your dimension measurement using finite constraints. `Cell constraint are not used with your logic`. What i posted is an example how to keep your constraints in parent dimensions. You need to debug what are your `baseConstraints`, and `cellConstraints` are. Start with debug and go step by step to check out what constraints and placeables are – Thracian Nov 25 '22 at 17:23
  • I was able to put it working as I pretend. Thank you so much for your help. – R0ck Nov 26 '22 at 01:15