feat: implement auto-fit functionality for dynamic column widths in ListDetailScreen
This commit is contained in:
+48
-28
@@ -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 =
|
val headerDp = headerPx.toDp() + 12.dp + 12.dp + 24.dp + 2.dp
|
||||||
with(density) {
|
val contentDp = maxContentPx.toDp() + 8.dp + 8.dp + 2.dp
|
||||||
val headerDp = headerPx.toDp() + 12.dp + 12.dp + 24.dp + 2.dp
|
val base = maxOf(headerDp, contentDp, 100.dp)
|
||||||
val contentDp = maxContentPx.toDp() + 8.dp + 8.dp + 2.dp
|
(base + 6.dp).value
|
||||||
val base = maxOf(headerDp, contentDp, 100.dp)
|
}
|
||||||
(base + 6.dp).value
|
fieldWidths[field.id] = widthDpValue.dp
|
||||||
}
|
autoFitCache[field.id] = signature to widthDpValue
|
||||||
newWidths[field.id] = widthDp
|
|
||||||
}
|
}
|
||||||
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 ->
|
||||||
|
|||||||
Reference in New Issue
Block a user