You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Jetpack Compose中重组时Text为何无法重新居中?

Android Compose自适应列宽表格文本居中异常问题

我在Android应用中用Composable实现了一个表格,需求是每列宽度根据列内最宽内容自适应(有最大宽度限制)。目前代码基本能工作:

正常表格

但当加载到比之前行更宽的内容时,已加载行的单元格宽度会更新为正确值(通过背景色可确认),但单元格内的Text组件没有重新居中,仍然按照之前的宽度对齐:

文本未居中的异常表格

相关代码如下:

@Composable
fun TableCell(
    text: String,
    style: TextStyle = MaterialTheme.typography.bodySmall,
    width: Dp = 0.dp,
    onWidthSet: (Dp) -> Unit = {},
    maxColumnWidth: Dp = 100.dp
) {
    val density = LocalDensity.current
    fun Int.asDp() = density.run {
        this@asDp.toDp()
    }

    Text(
        text = text,
        style = style,
        modifier = Modifier
            .padding(4.dp)
            .widthIn(width, maxColumnWidth)
            .onSizeChanged { onWidthSet(it.width.asDp()) }
            .background(MaterialTheme.colorScheme.secondary),
        textAlign = TextAlign.Center
    )
}

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun DataTable(
    headers: List<String>,
    rows: List<List<String>> = emptyList(),
    maxColumnWidth: Dp = 90.dp
) {
    Card(
        shape = RoundedCornerShape(8.dp),
        colors = CardDefaults.cardColors(
            containerColor = MaterialTheme.colorScheme.surface,
        ),
        elevation = CardDefaults.cardElevation(
            defaultElevation = 6.dp
        )
    ) {
        val columnWidths = remember { mutableStateMapOf<Int, Dp>() }
        val onWidthSet: (Int, Dp) -> Unit = { index, width ->
            if (!columnWidths.containsKey(index)) {
                columnWidths[index] = width
            }
            else if (columnWidths[index]!! < width) {
                columnWidths[index] = width
            }
        }
        LazyColumn {
            stickyHeader {
                Row(
                    modifier = Modifier.background(MaterialTheme.colorScheme.primary)
                ) {
                    headers.forEachIndexed { index, title ->
                        TableCell(
                            text = title,
                            style = MaterialTheme.typography.titleSmall.copy(
                                color = MaterialTheme.colorScheme.onPrimary
                            ),
                            width = columnWidths[index] ?: 0.dp,
                            onWidthSet = { width ->
                                onWidthSet(index, width)
                            },
                            maxColumnWidth = maxColumnWidth
                        )
                    }
                }
            }
            items(rows.size) { index ->
                val row = rows[index]
                Row {
                    for (i in headers.indices) {
                        TableCell(
                            text = if (i < row.size) row[i] else "",
                            width = columnWidths[i] ?: 0.dp,
                            onWidthSet = { width ->
                                onWidthSet(index, width)
                            },
                            maxColumnWidth = maxColumnWidth
                        )
                    }
                }
            }
        }
    }
}

@Preview(
    apiLevel = 33,
    showBackground = true,
    device = Devices.TABLET
)
@Composable
private fun DataTablePreview() {
        DataTable(
            headers = listOf("Header 1", "Header 2", "Header 3", "Header 4"),
            rows = listOf(
                listOf("Row 1", "Row 1", "Row 1", ""),
                listOf("Row 2", "-- Wider Row 2 --", "", ""),
                listOf("Row 3", "Row 3", "Row 3", "")
            ),
            maxColumnWidth = 200.dp
        )
}

问题原因

  1. widthIn的局限性:widthIn(min=width, max=maxColumnWidth)仅限制宽度范围,当columnWidths更新导致min值增大时,Text组件的布局约束虽变化,但内部对齐逻辑未触发重新计算——Compose可能缓存了之前的布局状态,未感知到宽度的实质性变化。
  2. 状态更新时机问题:onSizeChanged回调在布局完成后触发,此时修改columnWidths会触发后续重组,但Text组件可能未完全响应这次宽度变化。
  3. 代码笔误:DataTable中循环行内单元格时,onWidthSet(index, width)传的是行索引而非列索引i,导致列宽度更新逻辑混乱。

修复方案

1. 重构TableCell组件

用明确的width修饰符替代widthIn,提前计算文本宽度确保布局同步:

@Composable
fun TableCell(
    text: String,
    style: TextStyle = MaterialTheme.typography.bodySmall,
    width: Dp = 0.dp,
    onWidthSet: (Dp) -> Unit = {},
    maxColumnWidth: Dp = 100.dp
) {
    val density = LocalDensity.current
    fun Int.asDp() = density.run { this@asDp.toDp() }

    // 提前计算文本的实际宽度
    val textLayoutResult = remember(text, style) {
        val annotatedText = androidx.compose.ui.text.resolveDefaults(AnnotatedString(text), style)
        androidx.compose.ui.text.layout.TextLayoutResult(
            androidx.compose.ui.text.layout.TextLayoutInput(
                text = annotatedText,
                style = style,
                constraints = androidx.compose.ui.layout.Constraints()
            )
        )
    }
    val textWidth = with(density) { textLayoutResult.size.width.toDp() }
    // 确定最终宽度:取当前列宽和文本宽度的较大值,不超过最大限制
    val finalWidth = minOf(maxColumnWidth, max(width, textWidth))

    Text(
        text = text,
        style = style,
        modifier = Modifier
            .padding(4.dp)
            .width(finalWidth) // 明确设置宽度,确保重组时Text重新计算居中
            .background(MaterialTheme.colorScheme.secondary),
        textAlign = TextAlign.Center
    )

    // 在LaunchedEffect中触发宽度回调,避免布局过程中修改状态
    LaunchedEffect(finalWidth) {
        onWidthSet(finalWidth)
    }
}

2. 修复DataTable中的索引错误

将行内单元格的onWidthSet(index, width)改为onWidthSet(i, width),确保更新的是对应列的宽度:

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun DataTable(
    headers: List<String>,
    rows: List<List<String>> = emptyList(),
    maxColumnWidth: Dp = 90.dp
) {
    Card(
        shape = RoundedCornerShape(8.dp),
        colors = CardDefaults.cardColors(
            containerColor = MaterialTheme.colorScheme.surface,
        ),
        elevation = CardDefaults.cardElevation(
            defaultElevation = 6.dp
        )
    ) {
        val columnWidths = remember { mutableStateMapOf<Int, Dp>() }
        val onWidthSet: (Int, Dp) -> Unit = { index, width ->
            val currentWidth = columnWidths[index] ?: 0.dp
            if (width > currentWidth) {
                columnWidths[index] = width
            }
        }
        LazyColumn {
            stickyHeader {
                Row(modifier = Modifier.background(MaterialTheme.colorScheme.primary)) {
                    headers.forEachIndexed { index, title ->
                        TableCell(
                            text = title,
                            style = MaterialTheme.typography.titleSmall.copy(
                                color = MaterialTheme.colorScheme.onPrimary
                            ),
                            width = columnWidths[index] ?: 0.dp,
                            onWidthSet = { width -> onWidthSet(index, width) },
                            maxColumnWidth = maxColumnWidth
                        )
                    }
                }
            }
            items(rows.size) { rowIndex ->
                val row = rows[rowIndex]
                Row {
                    headers.indices.forEach { columnIndex ->
                        TableCell(
                            text = if (columnIndex < row.size) row[columnIndex] else "",
                            width = columnWidths[columnIndex] ?: 0.dp,
                            onWidthSet = { width -> onWidthSet(columnIndex, width) }, // 修复为列索引
                            maxColumnWidth = maxColumnWidth
                        )
                    }
                }
            }
        }
    }
}

关键改进点

  • 用width替代widthIn:强制设置单元格宽度,确保宽度变化时Text组件完全感知,重新计算居中对齐。
  • 提前计算文本宽度:避免依赖onSizeChanged的异步回调,让宽度计算和布局同步进行。
  • 修复索引错误:确保列宽度更新对应正确的列索引,避免逻辑混乱。
  • 使用LaunchedEffect:在布局完成后安全地更新列宽度状态,符合Compose的状态管理规范。

内容的提问来源于stack exchange,提问作者Damon

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.27 22:25:53