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

小屏设备上如何让Compose NumberPicker文本保持单行显示?

解决Jetpack Compose DurationPicker小屏数字换行问题

针对三星Galaxy S20这类小屏设备上DurationPicker的NumberPicker两位数字换行问题,下面提供几种自适应调整方案,既能保证小屏上数字单行显示,又能让大屏保持合理的间距:

方案一:基于ConstraintLayout动态计算单位文本padding

通过监听父容器尺寸,计算NumberPicker所需的最小宽度,动态调整"h/m/s"文本的padding,避免挤压数字显示空间:

@Composable
fun DurationPicker(
    label: String,
    onSelectTime: (FullHourWithSec) -> Unit,
    value: FullHourWithSec,
) {
    OutlinedBox(label = label) {
        val density = LocalDensity.current
        val constraints = remember { mutableStateOf(IntSize.Zero) }
        val unitPadding = remember { mutableStateOf(8.dp) }

        ConstraintLayout(
            modifier = Modifier.fillMaxWidth()
                .onSizeChanged { size ->
                    constraints.value = size
                    // 计算两位数字+内部边距的最小宽度
                    val minPickerWidth = with(density) {
                        val textWidth = TextMeasurer.measure(
                            text = "00",
                            style = MaterialTheme.typography.bodyLarge
                        ).size.width.toDp()
                        textWidth + 16.dp
                    }
                    // 计算布局所需的最小总宽度
                    val totalMinWidth = 3 * minPickerWidth + 3 * 8.dp
                    // 空间不足时按比例缩减单位padding
                    if (size.width.toDp() < totalMinWidth) {
                        val extraGap = totalMinWidth - size.width.toDp()
                        unitPadding.value = max(0.dp, 8.dp - extraGap / 3)
                    } else {
                        unitPadding.value = 8.dp
                    }
                }
        ) {
            val (hourPicker, hourText, minutePicker, minuteText, secondPicker, secondText) = createRefs()

            NumberPicker(
                modifier = Modifier.constrainAs(hourPicker) {
                    start.linkTo(parent.start)
                    top.linkTo(parent.top)
                    bottom.linkTo(parent.bottom)
                    end.linkTo(hourText.start)
                    width = Dimension.fillToConstraints
                },
                label = { "%02d".format(it) },
                value = value.hours,
                onValueChange = { onSelectTime(value.copy(hours = it)) },
                dividersColor = MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.disabled),
                range = 0..1000
            )

            Text(
                text = "h",
                modifier = Modifier.constrainAs(hourText) {
                    start.linkTo(hourPicker.end)
                    top.linkTo(parent.top)
                    bottom.linkTo(parent.bottom)
                }.padding(horizontal = unitPadding.value)
            )

            NumberPicker(
                modifier = Modifier.constrainAs(minutePicker) {
                    start.linkTo(hourText.end)
                    top.linkTo(parent.top)
                    bottom.linkTo(parent.bottom)
                    end.linkTo(minuteText.start)
                    width = Dimension.fillToConstraints
                },
                label = { "%02d".format(it) },
                value = value.minutes,
                onValueChange = { onSelectTime(value.copy(minutes = it)) },
                dividersColor = MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.disabled),
                range = 0..59
            )

            Text(
                text = "m",
                modifier = Modifier.constrainAs(minuteText) {
                    start.linkTo(minutePicker.end)
                    top.linkTo(parent.top)
                    bottom.linkTo(parent.bottom)
                }.padding(horizontal = unitPadding.value)
            )

            NumberPicker(
                modifier = Modifier.constrainAs(secondPicker) {
                    start.linkTo(minuteText.end)
                    top.linkTo(parent.top)
                    bottom.linkTo(parent.bottom)
                    end.linkTo(secondText.start)
                    width = Dimension.fillToConstraints
                },
                label = { "%02d".format(it) },
                value = value.seconds,
                onValueChange = { onSelectTime(value.copy(seconds = it)) },
                dividersColor = MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.disabled),
                range = 0..59
            )

            Text(
                text = "s",
                modifier = Modifier.constrainAs(secondText) {
                    start.linkTo(secondPicker.end)
                    top.linkTo(parent.top)
                    bottom.linkTo(parent.bottom)
                    end.linkTo(parent.end)
                }.padding(start = unitPadding.value)
            )
        }
    }
}

方案二:给NumberPicker设置最小宽度

预先计算两位数字的显示宽度,给每个NumberPicker设置最小宽度,确保数字有足够空间,剩余空间分配给单位文本的padding:

@Composable
fun DurationPicker(
    label: String,
    onSelectTime: (FullHourWithSec) -> Unit,
    value: FullHourWithSec,
) {
    OutlinedBox(label = label) {
        val density = LocalDensity.current
        val rowWidth = remember { mutableStateOf(0.dp) }
        // 计算NumberPicker的最小宽度
        val minPickerWidth = remember {
            with(density) {
                TextMeasurer.measure(
                    text = "00",
                    style = MaterialTheme.typography.bodyLarge
                ).size.width.toDp() + 16.dp
            }
        }
        // 计算单位文本的自适应padding
        val adaptivePadding = remember(rowWidth.value) {
            val availableExtra = rowWidth.value - 3 * minPickerWidth
            max(0.dp, availableExtra / 6) // 6个padding方向(h左右,m左右,s左)
        }

        Row(
            verticalAlignment = Alignment.CenterVertically,
            modifier = Modifier.fillMaxWidth()
                .onSizeChanged { rowWidth.value = it.width.toDp() }
        ) {
            Box(modifier = Modifier.weight(1f).widthIn(min = minPickerWidth)) {
                NumberPicker(
                    modifier = Modifier.fillMaxWidth(),
                    label = { "%02d".format(it) },
                    value = value.hours,
                    onValueChange = { onSelectTime(value.copy(hours = it)) },
                    dividersColor = MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.disabled),
                    range = 0..1000
                )
            }
            Text(text = "h", modifier = Modifier.padding(horizontal = adaptivePadding))
            
            Box(modifier = Modifier.weight(1f).widthIn(min = minPickerWidth)) {
                NumberPicker(
                    modifier = Modifier.fillMaxWidth(),
                    label = { "%02d".format(it) },
                    value = value.minutes,
                    onValueChange = { onSelectTime(value.copy(minutes = it)) },
                    dividersColor = MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.disabled),
                    range = 0..59
                )
            }
            Text(text = "m", modifier = Modifier.padding(horizontal = adaptivePadding))
            
            Box(modifier = Modifier.weight(1f).widthIn(min = minPickerWidth)) {
                NumberPicker(
                    modifier = Modifier.fillMaxWidth(),
                    label = { "%02d".format(it) },
                    value = value.seconds,
                    onValueChange = { onSelectTime(value.copy(seconds = it)) },
                    dividersColor = MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.disabled),
                    range = 0..59
                )
            }
            Text(text = "s", modifier = Modifier.padding(start = adaptivePadding))
        }
    }
}

方案三:文本自适应缩放(备选)

如果前两种方案无法满足需求,可以让NumberPicker的文本自动缩小以适应空间,但会降低小屏可读性,仅作为兜底方案:

@Composable
fun DurationPicker(
    label: String,
    onSelectTime: (FullHourWithSec) -> Unit,
    value: FullHourWithSec,
) {
    OutlinedBox(label = label) {
        Row(
            verticalAlignment = Alignment.CenterVertically,
        ) {
            NumberPicker(
                modifier = Modifier.weight(1f),
                label = { num ->
                    val text = "%02d".format(num)
                    val textStyle = MaterialTheme.typography.bodyLarge
                    val textLayoutResult = remember(text, textStyle) {
                        TextMeasurer.measure(text = text, style = textStyle)
                    }
                    val textWidth = textLayoutResult.size.width
                    val availableWidth = with(LocalDensity.current) {
                        (LocalConfiguration.current.screenWidthDp - 48.dp).toPx() / 3 // 减去边距后分3份
                    }
                    val scaleFactor = min(1f, availableWidth / textWidth)

                    Text(
                        text = text,
                        style = textStyle,
                        maxLines = 1,
                        softWrap = false,
                        modifier = Modifier.scale(scale = scaleFactor)
                    )
                },
                value = value.hours,
                onValueChange = { onSelectTime(value.copy(hours = it)) },
                dividersColor = MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.disabled),
                range = 0..1000
            )
            Text(text = "h", modifier = Modifier.padding(horizontal = 8.dp))
            // 分钟、秒的NumberPicker和文本组件重复上述逻辑即可
            // ...
        }
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.24 15:36:59