Jetpack Compose中HeightPicker单位切换数值异常问题
Android Compose HeightPicker切换单位后数值自动加1问题解决
问题描述
实现了支持厘米(cm)/英尺(ft)切换的HeightPicker组件,但存在异常:选择厘米数值后切换到英尺,再切回厘米时,原厘米数值会自动加1(例如185cm切换后变回186cm)。
核心原因
- 本地状态与Model未同步且无单位转换:
pickerValue是本地remember状态,切换单位时未将Model中存储的数值转换为新单位对应值,导致新单位Picker接收到超出范围的数值,触发自动校正逻辑错误修改数值。 - ListItemPicker索引计算异常:当
value不在当前单位的数值范围内时,list.indexOf(value)返回-1,后续索引计算逻辑出错,最终触发onValueChange错误更新数值。 - 状态未监听单位变化:
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
相关产品推荐
相关产品推荐

