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

Jetpack Compose中HeightPicker单位切换数值异常问题

Android Compose HeightPicker切换单位后数值自动加1问题解决

问题描述

实现了支持厘米(cm)/英尺(ft)切换的HeightPicker组件,但存在异常:选择厘米数值后切换到英尺,再切回厘米时,原厘米数值会自动加1(例如185cm切换后变回186cm)。

核心原因

  1. 本地状态与Model未同步且无单位转换:pickerValue是本地remember状态,切换单位时未将Model中存储的数值转换为新单位对应值,导致新单位Picker接收到超出范围的数值,触发自动校正逻辑错误修改数值。
  2. ListItemPicker索引计算异常:当value不在当前单位的数值范围内时,list.indexOf(value)返回-1,后续索引计算逻辑出错,最终触发onValueChange错误更新数值。
  3. 状态未监听单位变化:pickerValue的remember未绑定heightType,切换单位时不会重新计算显示值,始终保留旧单位的数值。

解决方案

1. 统一Model的高度存储逻辑

将Model改为存储基准单位(如cm)的高度值,切换单位时仅转换显示数值,避免单位切换时的数值混乱:

data class UserModel(
    var gender: String = "",
    var sedentary: String = "",
    var age: Int = 18,
    // 存储基准单位cm的高度,避免单位切换时数值混乱
    var heightCm: Int = 185,
    var heightType: String = "cm",
    var weight: Int = 76,
    var weightType: String = "kg",
    var step: Int = 6000,
)

2. 重构HeightPicker的状态与单位转换逻辑

移除本地pickerValue,直接根据Model的基准高度和当前单位计算显示值,切换单位时完成数值转换:

@Composable
fun HeightPicker(model: UserModel) {
    var isCmUnit by remember { mutableStateOf(model.heightType == "cm") }
    val currentUnit = if (isCmUnit) "cm" else "ft"
    
    // 根据当前单位计算显示的数值
    val displayHeight = remember(model.heightCm, isCmUnit) {
        if (isCmUnit) {
            model.heightCm
        } else {
            // cm转ft:1ft=30.48cm,取整处理
            (model.heightCm / 30.48).toInt()
        }
    }
    
    val valueRange = if (isCmUnit) 50..300 else 2..12

    Column {
        Row(modifier = Modifier.padding(top = 20.sdp)) {
            // cm切换按钮
            Button(
                modifier = Modifier,
                colors = if (isCmUnit) ButtonDefaults.buttonColors(containerColor = AppColor) 
                         else ButtonDefaults.buttonColors(containerColor = LightestAppColor),
                onClick = {
                    if (!isCmUnit) {
                        // 从ft切回cm,将当前ft数值转换为cm
                        model.heightCm = (displayHeight * 30.48).toInt()
                        model.heightType = "cm"
                    }
                    isCmUnit = true
                }
            ) {
                SimpleTextComponent(
                    text = "cm",
                    textColor = if (isCmUnit) Color.White else AppColor,
                    textSize = 12.ssp,
                    fontFamily = TitleTextFont.fontFamily
                )
            }

            Spacer(modifier = Modifier.width(20.sdp))

            // ft切换按钮
            Button(
                modifier = Modifier,
                colors = if (!isCmUnit) ButtonDefaults.buttonColors(containerColor = AppColor) 
                         else ButtonDefaults.buttonColors(containerColor = LightestAppColor),
                onClick = {
                    if (isCmUnit) {
                        model.heightType = "ft"
                    }
                    isCmUnit = false
                }
            ) {
                SimpleTextComponent(
                    text = "ft",
                    textColor = if (!isCmUnit) Color.White else AppColor,
                    textSize = 12.ssp,
                    fontFamily = TitleTextFont.fontFamily
                )
            }
        }

        Box(
            modifier = Modifier
                .fillMaxHeight()
                .wrapContentWidth()
                .align(Alignment.CenterHorizontally)
                .padding(bottom = 120.sdp),
            contentAlignment = Alignment.Center
        ) {
            DigitPicker(
                modifier = Modifier.width(100.sdp),
                value = displayHeight,
                range = valueRange,
                onValueChange = { newDisplayVal ->
                    if (isCmUnit) {
                        // 当前是cm,直接更新基准数值
                        model.heightCm = newDisplayVal
                    } else {
                        // 当前是ft,转换为cm后更新基准数值
                        model.heightCm = (newDisplayVal * 30.48).toInt()
                    }
                    model.heightType = currentUnit
                },
            )
            SimpleTextComponent(
                modifier = Modifier.align(Alignment.CenterEnd),
                text = currentUnit,
                fontFamily = TitleTextFont.fontFamily,
                textSize = 12.ssp
            )
        }
    }
}

3. 修复ListItemPicker的索引容错逻辑

当传入的value不在当前范围内时,默认取范围的有效值,避免索引计算错误:

private fun <T> getItemIndexForOffset(
    range: List<T>,
    value: T,
    offset: Float,
    halfNumbersColumnHeightPx: Float
): Int {
    // 若value不在列表中,默认取列表中间位置
    val baseIndex = range.indexOf(value).takeIf { it != -1 } ?: range.size / 2
    val calculatedIndex = baseIndex - (offset / halfNumbersColumnHeightPx).toInt()
    return maxOf(0, minOf(calculatedIndex, range.count() - 1))
}

同时在DigitPicker中增加有效值校验:

@Composable
fun DigitPicker(
    modifier: Modifier = Modifier,
    label: (Int) -> String = { it.toString() },
    value: Int,
    range: Iterable<Int>,
    onValueChange: (Int) -> Unit,
    dividersColor: Color = AppColor,
    textStyle: TextStyle = LocalTextStyle.current,
) {
    val rangeList = range.toList()
    // 确保传入的value在范围内,否则取第一个有效值
    val validValue = rangeList.firstOrNull { it == value } ?: rangeList.first()
    ListItemPicker(
        modifier = modifier,
        label = label,
        value = validValue,
        onValueChange = onValueChange,
        dividersColor = dividersColor,
        list = rangeList,
        textStyle = textStyle,
    )
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 17:24:50