我的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,
)
}
}