I’ve been trying to create a table view in compose...
# compose
n
I’ve been trying to create a table view in compose using SubcomposeLayout where the rowHeaders are static but the table data should be scrollable horizontally but I just can’t seem to get it right. Can anyone point out what I’m doing wrong? Code in thread
Copy code
@Composable
fun Table(
    modifier: Modifier = Modifier,
    rows: Int,
    columns: Int,
    extraHeaders: Int,
    rowHeaders: @Composable (Int) -> Unit,
    columnHeaders: @Composable (Int) -> Unit,
    tableData: @Composable RowScope.(Int, Int) -> Unit,
    columnTitle: @Composable () -> Unit? = { null },
    rowExtraHeaders: @Composable () -> Unit? = { null },
) {
    val scrollState = rememberScrollState()

    SubcomposeLayout(
        modifier = modifier
    ) { constraints ->

        // Measure table data for max width and height
        val measurablesTableAll = subcompose(TableSlots.TableDataMeasurePass) {
            (0 until rows).forEach { rowIndex ->
                (0 until columns).forEach { columnIndex ->
                    Row(
                        verticalAlignment = Alignment.CenterVertically,
                        horizontalArrangement = Arrangement.SpaceEvenly,
                        modifier = Modifier.horizontalScroll(scrollState)
                    ) {
                        tableData(rowIndex, columnIndex)
                    }
                }
            }
        }
        val contentMaxHeight = measurablesTableAll.maxOf { it.maxIntrinsicHeight(Int.MAX_VALUE) }
        val contentMaxWidth = constraints.maxWidth

        // Measure Row Headers
        val measurablesHeaders = subcompose(TableSlots.RowHeaders) {
            (0 until rows).forEach { rowIndex -> rowHeaders(rowIndex) }
        }
        val headerMaxWidth = measurablesHeaders.maxOf { it.maxIntrinsicWidth(Int.MAX_VALUE) }
        val maxRowHeight = contentMaxHeight
        val headerPlaceables = measurablesHeaders.map {
            it.measure(Constraints.fixed(width = headerMaxWidth, height = contentMaxHeight))
        }

        // Measure Column Headers
        val measurablesColumnHeaders = subcompose(TableSlots.ColumHeaders) {
            (0 until columns).forEach { columnIndex -> columnHeaders(columnIndex) }
        }
        val headerColumnMaxWidth =
            measurablesColumnHeaders.maxOf { it.maxIntrinsicWidth(Int.MAX_VALUE) }
        val headerColumnMaxHeight =
            measurablesColumnHeaders.maxOf { it.maxIntrinsicHeight(headerMaxWidth) }
        val columnMaxWidth = max(headerColumnMaxWidth, contentMaxWidth)

        // Measure Column Title
        val columnTitlePlaceable = subcompose(TableSlots.ColumnTitle) {
            columnTitle()
        }.first().measure(Constraints.fixed(width = columnMaxWidth, height = maxRowHeight))

        // Measure Extra Headers
        val measurablesExtraHeaders = subcompose(TableSlots.ExtraHeaders) {
            (0 until extraHeaders).forEach { _ -> rowExtraHeaders() }
        }
        val extraHeadersMaxWidth =
            measurablesExtraHeaders.maxOf { it.maxIntrinsicWidth(Int.MAX_VALUE) }
        val extraHeadersMaxHeight =
            measurablesExtraHeaders.maxOf { it.maxIntrinsicHeight(Int.MAX_VALUE) }

        // Measure Table Data
        val measurablesTableData = subcompose(TableSlots.TableData) {
            (0 until rows).forEach { rowIndex ->
                Row(
                    horizontalArrangement = Arrangement.SpaceEvenly,
                    verticalAlignment = Alignment.CenterVertically,
                    modifier = Modifier.horizontalScroll(scrollState)
                        .fillMaxWidth()
                        .background(Color.Red)
                ) {
                    (0 until columns).forEach { columnIndex -> tableData(rowIndex, columnIndex) }
                }
            }
        }
        val dataMaxWidth = measurablesTableData.maxOf { it.maxIntrinsicWidth(Int.MAX_VALUE) }
        val headerWidth = maxOf(dataMaxWidth, columnMaxWidth, extraHeadersMaxWidth)
        val measurablesTableDataPlaceable = measurablesTableData.map {
            it.measure(Constraints.fixed(width = headerWidth, height = maxRowHeight))
        }

        // Measure Divider
        val divider = subcompose(TableSlots.Divider) {
            repeat(11) { PublicDivider() }
        }.map {
            it.measure(Constraints.fixed(width = contentMaxWidth, height = 1.dp.roundToPx()))
        }

        // Measure Column Header Placeables
        val headerColumnPlaceables = measurablesColumnHeaders.map {
            it.measure(Constraints.fixed(width = headerWidth / 2, height = headerColumnMaxHeight))
        }

        // Measure Extra Headers Placeables
        val extraHeadersPlaceable = measurablesExtraHeaders.map {
            it.measure(Constraints.fixed(width = headerWidth / 2, height = extraHeadersMaxHeight))
        }

        // Calculate Overall Height
        val height = headerColumnMaxHeight +
                headerPlaceables.sumOf { it.height } +
                columnTitlePlaceable.height +
                extraHeadersPlaceable.sumOf { it.height }


        layout(width = constraints.maxWidth, height = height) {
            val dividerPositions = mutableListOf<Int>() // List to hold y-positions for dividers

            // Place column headers below the title
            var x = headerMaxWidth
            headerColumnPlaceables.forEach {
                // Position each column header horizontally
                it.placeRelative(x, 0)
                x += it.width
            }
            // Place column title at the top-left corner
            columnTitlePlaceable.placeRelative(0, 0)

            // Place extra headers below column title and above table data
            x = headerMaxWidth
            extraHeadersPlaceable.forEach { extraHeaderPlaceable ->
                // Position extra headers horizontally starting after the row headers
                extraHeaderPlaceable.placeRelative(
                    x,
                    headerColumnMaxHeight + headerColumnMaxHeight / 2
                )
                x += extraHeaderPlaceable.width
            }
            // Add positions for dividers below extra headers
            dividerPositions.add(headerColumnMaxHeight + 8.dp.roundToPx())
            dividerPositions.add(extraHeadersMaxHeight + headerColumnMaxHeight + 8.dp.roundToPx())

            // Calculate initial y-position for placing row headers
            val y: Int = headerColumnMaxHeight + extraHeadersMaxHeight + headerColumnMaxHeight / 2

            // Place row headers
            headerPlaceables.forEachIndexed { index, headerPlaceable ->
                // Position each row header vertically
                headerPlaceable.placeRelative(0, y + index * maxRowHeight)
                if (index > 0) {
                    // Add divider position after each row header
                    dividerPositions.add(y + index * maxRowHeight)
                }
            }

            // Place data rows corresponding to row headers
            measurablesTableDataPlaceable.forEachIndexed { index, dataPlaceable ->
                x = headerMaxWidth // Start placing data after the row headers
                dataPlaceable.placeRelative(x, y + index * maxRowHeight)
            }

            // Place dividers at calculated positions
            dividerPositions.forEachIndexed { index, position ->
                // Position each divider at the calculated y-coordinate
                divider[index].placeRelative(0, position)
            }
        }

    }
}

private enum class TableSlots {
    RowHeaders, TableData, TableDataMeasurePass, ColumHeaders, ColumnTitle, ExtraHeaders, Divider
}
s
Do you have a video/image of what you would like the final result to be like?
n
It doesn’t need to scroll vertically but horizontally but basically something like this: https://www.androidcode.ninja/android-scroll-table-fixed-header-column/
s
Perhaps using https://github.com/oleksandrbalan/lazytable with fixed column/rows might be what you want, without having to do all of this manual work?
n
I would like to do it myself instead of adding a dependency. Wanna learn something too :) but thanks maybe I can be inspired
💯 1
K 1