commonMain.com.sunnychung.lib.android.composabletable.ux.Table.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of composable-table-desktop Show documentation
Show all versions of composable-table-desktop Show documentation
A multiplaform Jetpack Compose library that provides a `@Composable` table with automatic layouts.
The newest version!
package com.sunnychung.lib.android.composabletable.ux
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Dp
@Composable
fun Table(
modifier: Modifier = Modifier,
rowCount: Int,
columnCount: Int,
stickyRowCount: Int = 0,
stickyColumnCount: Int = 0,
maxCellWidthDp: Dp = Dp.Infinity,
maxCellHeightDp: Dp = Dp.Infinity,
verticalScrollState: ScrollState = rememberScrollState(),
horizontalScrollState: ScrollState = rememberScrollState(),
cellContent: @Composable (rowIndex: Int, columnIndex: Int) -> Unit
) {
val columnWidths = remember { mutableStateMapOf() }
val rowHeights = remember { mutableStateMapOf() }
val maxCellWidth = if (listOf(Dp.Infinity, Dp.Unspecified).contains(maxCellWidthDp)) {
Constraints.Infinity
} else {
with(LocalDensity.current) { maxCellWidthDp.toPx() }.toInt()
}
val maxCellHeight = if (listOf(Dp.Infinity, Dp.Unspecified).contains(maxCellHeightDp)) {
Constraints.Infinity
} else {
with(LocalDensity.current) { maxCellHeightDp.toPx() }.toInt()
}
var accumWidths = mutableListOf()
var accumHeights = mutableListOf()
@Composable
fun StickyCells(modifier: Modifier = Modifier, rowCount: Int, columnCount: Int) {
if (rowCount > 0 && columnCount > 0) {
Box(modifier = modifier) {
Layout(
content = {
(0 until rowCount).forEach { rowIndex ->
(0 until columnCount).forEach { columnIndex ->
cellContent(rowIndex = rowIndex, columnIndex = columnIndex)
}
}
},
) { measurables, constraints ->
val placeables = measurables.mapIndexed { index, it ->
val columnIndex = index % columnCount
val rowIndex = index / columnCount
it.measure(
Constraints(
minWidth = columnWidths[columnIndex] ?: 0,
minHeight = rowHeights[rowIndex] ?: 0,
maxWidth = columnWidths[columnIndex] ?: 0,
maxHeight = rowHeights[rowIndex] ?: 0
)
)
}
val totalWidth = accumWidths[columnCount]
val totalHeight = accumHeights[rowCount]
layout(width = totalWidth, height = totalHeight) {
placeables.forEachIndexed { index, placeable ->
val columnIndex = index % columnCount
val rowIndex = index / columnCount
placeable.placeRelative(
accumWidths[columnIndex],
accumHeights[rowIndex]
)
}
}
}
}
}
}
Box(modifier = modifier) {
Box(
modifier = Modifier
.then(Modifier.horizontalScroll(horizontalScrollState))
.then(Modifier.verticalScroll(verticalScrollState))
) {
Layout(
content = {
(0 until rowCount).forEach { rowIndex ->
(0 until columnCount).forEach { columnIndex ->
cellContent(rowIndex = rowIndex, columnIndex = columnIndex)
}
}
},
) { measurables, constraints ->
val placeables = measurables.mapIndexed { index, it ->
val columnIndex = index % columnCount
val rowIndex = index / columnCount
it.measure(
Constraints(
minWidth = columnWidths[columnIndex] ?: 0,
minHeight = rowHeights[rowIndex] ?: 0,
maxWidth = maxCellWidth,
maxHeight = maxCellHeight
)
)
}
placeables.forEachIndexed { index, placeable ->
val columnIndex = index % columnCount
val rowIndex = index / columnCount
val existingWidth = columnWidths[columnIndex] ?: 0
val maxWidth = maxOf(existingWidth, placeable.width)
if (maxWidth > existingWidth) {
columnWidths[columnIndex] = maxWidth
}
val existingHeight = rowHeights[rowIndex] ?: 0
val maxHeight = maxOf(existingHeight, placeable.height)
if (maxHeight > existingHeight) {
rowHeights[rowIndex] = maxHeight
}
}
accumWidths = mutableListOf(0).apply {
(1..columnWidths.size).forEach { i ->
this += this.last() + columnWidths[i - 1]!!
}
}
accumHeights = mutableListOf(0).apply {
(1..rowHeights.size).forEach { i ->
this += this.last() + rowHeights[i - 1]!!
}
}
val totalWidth = accumWidths.last()
val totalHeight = accumHeights.last()
layout(width = totalWidth, height = totalHeight) {
placeables.forEachIndexed { index, placeable ->
val columnIndex = index % columnCount
val rowIndex = index / columnCount
placeable.placeRelative(accumWidths[columnIndex], accumHeights[rowIndex])
}
}
}
}
StickyCells(
modifier = Modifier.horizontalScroll(horizontalScrollState),
rowCount = stickyRowCount,
columnCount = columnCount
)
StickyCells(
modifier = Modifier.verticalScroll(verticalScrollState),
rowCount = rowCount,
columnCount = stickyColumnCount
)
StickyCells(
rowCount = stickyRowCount,
columnCount = stickyColumnCount
)
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy