我的Android应用程序中有一个名为HeightPicker的用户界面组件,允许用户使用厘米(厘米)或英尺(英尺) Select 身高. 当用户在"厘米"和"英尺"单位之间切换时,问题就出现了:所选的高度值意外变化. 具体地说,如果用户 Select 以厘米为单位的高度,切换到英尺,然后返回到厘米,则以前 Select 的高度值将递增1.

复制: 启动应用程序并导航至包含高度 Select 器的屏幕. 最初,高度以厘米为单位显示,默认值为185. 点击"ft"按钮切换到英尺. Select 以英尺为单位的高度,然后切换回厘米. 请注意,先前选定的高度(单位为厘米)增加了1.

预期行为: 当在"cm"和"ft"之间切换时,选定的高度值应保持不变. 例如,如果用户 Select 186 cm,切换到英尺,然后返回到厘米,那么高度应该仍然是186 cm,而不是187 cm..我已经添加了一个链接到我的驱动器,所以你可以看到演示视频这个问题Demo Video

我try 过的: try 使用布尔标志和字符串标识符('cm'和'ft')来跟踪所选单元并相应地处理值. try 使用ViewModel中的可变状态存储数据,但问题仍然存在. 相关代码片段:

提供了HeightPicker、UserModel、DigitPicker和ListItemPicker的代码片段. 提到了 Select ‘cm’和‘ft’按钮的实现,以及更新所选高度值的逻辑.

UserScreen

    @Composable
    fun HeightPicker() {
    var heightType by remember { mutableStateOf(true) }
    val range = if (heightType) 50..300 else 2..12
    val text = if (heightType) "cm" else "ft"
    var pickerValue by remember { mutableStateOf(if (heightType) 185 else 5) }

    Column {
        Row(modifier = Modifier.padding(top = 20.sdp)) {
            // Button for "cm"
            Button(
                modifier = Modifier,
                colors = if (heightType) ButtonDefaults.buttonColors(containerColor = AppColor) else ButtonDefaults.buttonColors(
                    containerColor = LightestAppColor
                ),
                onClick = { heightType = true }
            ) {
                SimpleTextComponent(
                    modifier = Modifier,
                    text = "cm",
                    textColor = if (heightType) Color.White else AppColor,
                    textSize = 12.ssp,
                    fontFamily = TitleTextFont.fontFamily
                )
            }

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

            // Button for "ft"
            Button(
                modifier = Modifier,
                colors = if (!heightType) ButtonDefaults.buttonColors(containerColor = AppColor) else       ButtonDefaults.buttonColors(
                    containerColor = LightestAppColor
                ),
                onClick = {
                    heightType = false

                }
            ) {
                SimpleTextComponent(
                    modifier = Modifier,
                    text = "ft",
                    textColor = if (!heightType) 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 = pickerValue,
                range = range,
                onValueChange = {
                    pickerValue = it
                    model.height = it
                    model.heightType = text},
            )
            SimpleTextComponent(
                modifier = Modifier.align(Alignment.CenterEnd),
                text = text,
                fontFamily = TitleTextFont.fontFamily,
                textSize = 12.ssp
            )
        }
    }
}

UserModel

    data class UserModel(

    var gender : String = "",
    var sedentary : String = "",
    var age : Int = 18,
    var height : Int = 185,
    var heightType : String = "cm",
    var weight : Int = 76,
    var weightType : String = "kg",
    var step : Int = 6000,
)

Digit Picker

    @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,
) {
    ListItemPicker(
        modifier = modifier,
        label = label,
        value = value,
        onValueChange = onValueChange,
        dividersColor = dividersColor,
        list = range.toList(),
        textStyle = textStyle,
    )
}

ListItemPicker

    private fun <T> getItemIndexForOffset(
    range: List<T>,
    value: T,
    offset: Float,
    halfNumbersColumnHeightPx: Float
    ): Int {
    val indexOf = range.indexOf(value) - (offset / halfNumbersColumnHeightPx).toInt()
    return maxOf(0, minOf(indexOf, range.count() - 1))
    }

    @Composable
    fun <T> ListItemPicker(
    modifier: Modifier = Modifier,
    label: (T) -> String = { it.toString() },
    value: T,
    onValueChange: (T) -> Unit,
    dividersColor: Color = AppColor,
    list: List<T>,
    textStyle: TextStyle = LocalTextStyle.current,
    dividerHeight: Dp = 2.dp
    ) {
    val minimumAlpha = 0.3f
    val verticalMargin = 15.dp
    val numbersColumnHeight = 150.dp
    val halfNumbersColumnHeight = numbersColumnHeight / 2
    val halfNumbersColumnHeightPx = with(LocalDensity.current) { halfNumbersColumnHeight.toPx() }

    val coroutineScope = rememberCoroutineScope()

    val animatedOffset = remember { Animatable(0f) }
        .apply {
            val index = list.indexOf(value)
            val offsetRange = remember(value, list) {
                -((list.count() - 1) - index) * halfNumbersColumnHeightPx to
                        index * halfNumbersColumnHeightPx
            }
            updateBounds(offsetRange.first, offsetRange.second)
        }

    val coercedAnimatedOffset = animatedOffset.value % halfNumbersColumnHeightPx

    val indexOfElement =
        getItemIndexForOffset(list, value, animatedOffset.value, halfNumbersColumnHeightPx)

    var dividersWidth by remember { mutableStateOf(0.dp) }

    Layout(
        modifier = modifier
            .draggable(
                orientation = Orientation.Vertical,
                state = rememberDraggableState { deltaY ->
                    coroutineScope.launch {
                        animatedOffset.snapTo(animatedOffset.value + deltaY)
                    }
                },
                onDragStopped = { velocity ->
                    coroutineScope.launch {
                        val endValue = animatedOffset.fling(
                            initialVelocity = velocity,
                            animationSpec = exponentialDecay(frictionMultiplier = 4f), 
                            adjustTarget = { target ->
                                val coercedTarget = target % halfNumbersColumnHeightPx
                                val coercedAnchors =
                                    listOf(
                                        -halfNumbersColumnHeightPx,
                                        0f,
                                        halfNumbersColumnHeightPx
                                    )
                                val coercedPoint =
                                    coercedAnchors.minByOrNull { abs(it - coercedTarget) }!!
                                val base =
                                    halfNumbersColumnHeightPx * (target /  halfNumbersColumnHeightPx).toInt()
                                coercedPoint + base
                            }
                        ).endState.value

                        val result = list.elementAt(
                            getItemIndexForOffset(list, value, endValue, halfNumbersColumnHeightPx)
                        )
                        onValueChange(result)
                        animatedOffset.snapTo(0f)
                    }
                }
            )
            .padding(vertical = numbersColumnHeight / 3 + verticalMargin * 2),
        content = {
            Box(
                modifier
                    .height(dividerHeight)
                    .background(color = dividersColor)
            )
            Box(
                modifier = Modifier
                    .padding(vertical = verticalMargin, horizontal = 20.dp)
                    .offset { IntOffset(x = 0, y = coercedAnimatedOffset.roundToInt()) }
            ) {
                val baseLabelModifier = Modifier.align(Alignment.Center)
                ProvideTextStyle(textStyle) {
                    if (indexOfElement > 0)
                        Label(
                            text = label(list.elementAt(indexOfElement - 1)),
                            modifier = baseLabelModifier
                                .offset(y = -halfNumbersColumnHeight)
                                .alpha(
                                    maxOf(
                                        minimumAlpha,
                                        coercedAnimatedOffset / halfNumbersColumnHeightPx
                                    )
                                )
                        )
                    Label(
                        text = label(list.elementAt(indexOfElement)),
                        modifier = baseLabelModifier
                            .alpha(
                                (maxOf(
                                    minimumAlpha,
                                    1 - abs(coercedAnimatedOffset) / halfNumbersColumnHeightPx
                                ))
                            )
                    )
                    if (indexOfElement < list.count() - 1)
                        Label(
                            text = label(list.elementAt(indexOfElement + 1)),
                            modifier = baseLabelModifier
                                .offset(y = halfNumbersColumnHeight)
                                .alpha(
                                    maxOf(
                                        minimumAlpha,
                                        -coercedAnimatedOffset / halfNumbersColumnHeightPx
                                    )
                                )
                        )
                }
            }
            Box(
                modifier
                    .height(dividerHeight)
                    .background(color = dividersColor)
            )
        }
     ) { measurables, constraints ->
        // Don't constrain child views further, measure them with given constraints
        // List of measured children
        val placeables = measurables.map { measurable ->
            // Measure each children
            measurable.measure(constraints)
        }

        dividersWidth = placeables
            .drop(1)
            .first()
            .width
            .toDp()

        // Set the size of the layout as big as it can
        layout(dividersWidth.toPx().toInt(), placeables
            .sumOf {
                it.height
            }
        ) {
            // Track the y co-ord we have placed children up to
            var yPosition = 0

            // Place children in the parent layout
            placeables.forEach {
                // Position item on the screen
                it.placeRelative(x = 0, y = yPosition)

                // Record the y co-ord placed up to
                yPosition += it.height
            }
        }
    }
}


    @Composable
    private fun Label(text: String, modifier: Modifier) {
    Text(
        modifier = modifier.pointerInput(Unit) {
            detectTapGestures(onLongPress = {
                // FIXME: Empty to disable text selection
            })
        },
        text = text,
        textAlign = TextAlign.Center,
        fontSize = 24.ssp,
        fontFamily = TitleTextFont.fontFamily,
        color = AppColor
        )
    }

    private suspend fun Animatable<Float, AnimationVector1D>.fling(
    initialVelocity: Float,
    animationSpec: DecayAnimationSpec<Float>,
    adjustTarget: ((Float) -> Float)?,
    block: (Animatable<Float, AnimationVector1D>.() -> Unit)? = null,
    ): AnimationResult<Float, AnimationVector1D> {
    val targetValue = animationSpec.calculateTargetValue(value, initialVelocity)
    val adjustedTarget = adjustTarget?.invoke(targetValue)
    return if (adjustedTarget != null) {
        animateTo(
            targetValue = adjustedTarget,
            initialVelocity = initialVelocity,
            block = block
        )
    } else {
        animateDecay(
            initialVelocity = initialVelocity,
            animationSpec = animationSpec,
            block = block,
        )
    }
}

推荐答案

我认为这条线背后的逻辑有问题:

var pickerValue by remember { mutableStateOf(if (heightType) 185 else 5) }

mutableStateOf()内的if语句将在第一次合成时被执行exactly once.之后,当前值将在重新组合中进行remember次调整.
因此,当您更改heightType时,pickerValue将保持不变.只有rangetext会更新,因为您没有使用remember进行这些更新.

我不能确切解释你观察到的奇怪行为,但我猜发生的是

  • 最初将heightType设置为厘米,将pickerValue设置为185
  • heightType设置为英寸,但pickerValue保持为185
  • 由于185在允许的range之外,该值将被强制.

如果你想在你从cm切换到inch之后恢复以前的值,你需要两个状态变量:

var cmPickerValue by remember { mutableStateOf(185) }
var inchPickerValue by remember { mutableStateOf(5) }

//...

DigitPicker(
    modifier = Modifier.width(100.sdp),
    value = if (heightType) cmPickerValue else inchPickerValue,
    range = range,
    onValueChange = {
        if (heightType) {
            cmPickerValue = it
        } else {
            inchPickerValue = it
        }
        model.height = it
        model.heightType = text
    },
)

或者,您可以只调用DigitPicker Composable两次,一次为厘米,一次为英寸:

if (heightType) {
    DigitPicker(
        // use cmPickerValue in here
    )
} else {
    DigitPicker(
        // use inchPickerValue in here
    )
}

如果有效或至少改进了当前的实施,请报告.

Kotlin相关问答推荐

文本正在被切断在200%的屏幕比例在Jetpack Compose

我可以检测一个函数是否在Kotlin中被递归调用(即,重入)吗?

T和T有什么区别:任何>

如何在Docker中使用Selenium和chromedriver?

为什么记得不将 StateFlow 转换为特定类型?

如何将 `throw` 放置在辅助函数中但仍然具有空安全性?

为什么 KFunction2 在 Kotlin 中不是可表示类型?

Kotlin 从其他类调用成员扩展函数

Kotlin 方法重载

使用 Hilt 注入 CoroutineWorker

片段内的 Kotlin 按钮 onClickListener 事件

Kotlin JVM 和 Kotlin Native 有什么区别?

从片段(fragment)中的点击事件启动协同程序

如何获取Kotlin中变量的名称?

未解决的参考 dagger 2 + kotlin + android gradle

Gradle:无法连接到 Windows 上的 Kotlin 守护程序

在 kotlin 中,如何将主构造函数中的属性设置器设为私有?

在android java类中使用Kotlin扩展

如何在 Gradle Kotlin DSL 中使用来自 gradle.properties 的插件版本?

有没有办法在Kotlin中设置一个私有常量