feat: implement auto-fit functionality for dynamic column widths in ListDetailScreen

This commit is contained in:
2025-11-11 21:54:49 +01:00
parent 2bcc262c9f
commit eef630bbe6
@@ -385,41 +385,52 @@ fun ListDetailScreen(
) )
// Auto-resize button (chip style) // Auto-resize button (chip style)
// Auto-fit cache keyed by fieldId with a compact signature based on timestamps and counts
val autoFitCache = remember { mutableStateMapOf<String, Pair<AutoFitSignature, Float>>() }
FilterChip( FilterChip(
selected = false, selected = false,
onClick = { onClick = {
val newWidths = mutableMapOf<String, Float>()
stableFields.forEach { field -> stableFields.forEach { field ->
val headerPx = // Build a compact signature: field name, field.updatedAt, item count, max updatedAt among values
textMeasurer.measure( var maxValUpdated = 0L
AnnotatedString(field.name), stableItems.forEach { item ->
style = headerTextStyle, val v = item.values.find { it.fieldId == field.id }
).size.width.toFloat() if (v != null && v.updatedAt > maxValUpdated) maxValUpdated = v.updatedAt
}
val signature = AutoFitSignature(
name = field.name,
fieldUpdatedAt = field.updatedAt,
itemCount = stableItems.size,
maxValueUpdatedAt = maxValUpdated,
)
val cached = autoFitCache[field.id]
if (cached != null && cached.first == signature) {
// Use cached width
fieldWidths[field.id] = cached.second.dp
return@forEach
}
// Measure header and max content width
val headerPx = textMeasurer.measure(AnnotatedString(field.name), style = headerTextStyle).size.width.toFloat()
var maxContentPx = 0f var maxContentPx = 0f
stableItems.forEach { itemWithValues -> stableItems.forEach { itemWithValues ->
val v = itemWithValues.values.find { it.fieldId == field.id }?.value val v = itemWithValues.values.find { it.fieldId == field.id }?.value
val display = getDisplayTextForMeasure(field, v) val display = getDisplayTextForMeasure(field, v)
if (display.isNotEmpty()) { if (display.isNotEmpty()) {
val w = val w = textMeasurer.measure(AnnotatedString(display), style = bodyTextStyle).size.width.toFloat()
textMeasurer.measure(
AnnotatedString(display),
style = bodyTextStyle,
).size.width.toFloat()
if (w > maxContentPx) maxContentPx = w if (w > maxContentPx) maxContentPx = w
} }
} }
val widthDpValue = with(density) {
val widthDp =
with(density) {
val headerDp = headerPx.toDp() + 12.dp + 12.dp + 24.dp + 2.dp val headerDp = headerPx.toDp() + 12.dp + 12.dp + 24.dp + 2.dp
val contentDp = maxContentPx.toDp() + 8.dp + 8.dp + 2.dp val contentDp = maxContentPx.toDp() + 8.dp + 8.dp + 2.dp
val base = maxOf(headerDp, contentDp, 100.dp) val base = maxOf(headerDp, contentDp, 100.dp)
(base + 6.dp).value (base + 6.dp).value
} }
newWidths[field.id] = widthDp fieldWidths[field.id] = widthDpValue.dp
autoFitCache[field.id] = signature to widthDpValue
} }
newWidths.forEach { (id, w) -> fieldWidths[id] = w.dp }
prefs.setColumnWidths(listId, fieldWidths.mapValues { it.value.value }) prefs.setColumnWidths(listId, fieldWidths.mapValues { it.value.value })
}, },
label = { Text("Auto-fit") }, label = { Text("Auto-fit") },
@@ -482,14 +493,18 @@ fun ListDetailScreen(
pendingScrollToBottom = false pendingScrollToBottom = false
} }
} }
Column(modifier = Modifier.fillMaxSize()) { Column(
modifier =
Modifier
.fillMaxSize()
.horizontalScroll(horizontalScrollState),
) {
// Fixed header (does not participate in vertical scroll) // Fixed header (does not participate in vertical scroll)
Row( Row(
modifier = modifier =
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
.background(MaterialTheme.colorScheme.surface) .background(MaterialTheme.colorScheme.surface),
.horizontalScroll(horizontalScrollState),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
stableFields.forEach { field -> stableFields.forEach { field ->
@@ -533,7 +548,6 @@ fun ListDetailScreen(
fields = stableFields, fields = stableFields,
fieldWidths = fieldWidths, fieldWidths = fieldWidths,
itemWithValues = itemWithValues, itemWithValues = itemWithValues,
scrollState = horizontalScrollState,
onClick = { itemToEdit = itemWithValues }, onClick = { itemToEdit = itemWithValues },
) )
} }
@@ -735,6 +749,14 @@ private fun getDisplayTextForMeasure(
} }
} }
// Compact signature to detect when a field's content or definition changed without scanning all values deeply
private data class AutoFitSignature(
val name: String,
val fieldUpdatedAt: Long,
val itemCount: Int,
val maxValueUpdatedAt: Long,
)
// Centralized mapping from canonical/legacy field type strings to user-facing labels // Centralized mapping from canonical/legacy field type strings to user-facing labels
private fun fieldTypeToLabel(type: String): String { private fun fieldTypeToLabel(type: String): String {
return when (type.uppercase()) { return when (type.uppercase()) {
@@ -886,7 +908,6 @@ fun ItemRow(
fields: List<Field>, fields: List<Field>,
fieldWidths: Map<String, Dp>, fieldWidths: Map<String, Dp>,
itemWithValues: ItemWithValues, itemWithValues: ItemWithValues,
scrollState: androidx.compose.foundation.ScrollState,
onClick: () -> Unit, onClick: () -> Unit,
) { ) {
// Map values by fieldId to ensure correct value-column alignment regardless of original order // Map values by fieldId to ensure correct value-column alignment regardless of original order
@@ -898,9 +919,8 @@ fun ItemRow(
modifier = modifier =
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
.height(IntrinsicSize.Min)
.clickable(onClick = onClick) .clickable(onClick = onClick)
.horizontalScroll(scrollState), ,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
fields.forEach { field -> fields.forEach { field ->