android Jetpack组成:对滑块更改值作出React

j0pj023g  于 2023-02-17  发布在  Android
关注(0)|答案(5)|浏览(178)

我想显示一个滑块,它可以由用户通过拖放更新,也可以从服务器实时更新。
当用户完成拖动动作时,我想将最终值发送到服务器。
我最初的尝试是:

@Composable
fun LightView(
    channel: UiChannel,
    onDimmerChanged: (Float) -> Unit
) {
    val sliderValue = channel....
    Slider(value = sliderValue, onValueChange = onDimmerChanged)
}

onDimmerChanged方法来自我的ViewModel,它更新服务器值,运行良好,但是每次移动都调用onValueChange,这会用不必要的请求轰炸服务器。
我尝试创建一个自定义滑块:

@Composable
fun LightView(
    channel: UiChannel,
    onDimmerChanged: (Float) -> Unit
) {
    val sliderValue = channel....
    Slider(initialValue = sliderValue, valueSetter = onDimmerChanged)
}

@Composable
fun Slider(initialValue: Float, valueSetter: (Float) -> Unit) {
    var value by remember { mutableStateOf(initialValue) }
    Slider(
        value = value,
        onValueChange = { value = it },
        onValueChangeFinished = { valueSetter(value) }
    )
}

它在应用端运行良好,值只发送一次,当用户停止拖动时。
但是,当服务器有更新时,它无法更新视图。我猜这与remember有关。所以我尝试不使用remember

@Composable
fun Slider(initialValue: Float, valueSetter: (Float) -> Unit) {
    var value by mutableStateOf(initialValue)
    Slider(
        value = value,
        onValueChange = { value = it },
        onValueChangeFinished = { valueSetter(value) }
    )
}

这一次,当服务器更新值时,视图正确更新,但当用户拖动滑块时,视图不再移动。
我肯定我错过了什么与国家提升和所有,但我不知道是什么。

    • 所以我最后一个问题**如何创建一个既可以由ViewModel更新,也可以由用户更新的Slider,并仅在用户完成拖动时通知ViewModel新值?
    • 编辑:**

我也尝试了@CommonsWare的建议:

@Composable
fun LightView(
    channel: UiChannel,
    onDimmerChanged: (Float) -> Unit
) {
    val sliderValue = channel....
    val sliderState = mutableStateOf(sliderValue)
    Slider(state = sliderState, valueSet = { onDimmerChanged(sliderState.value) })
}

@Composable
fun Slider(state: MutableState<Float>, valueSet: () -> Unit) {
    Slider(
        value = state.value,
        onValueChange = { state.value = it },
        onValueChangeFinished = valueSet
    )
}

而且它也不起作用。当使用拖放时,sliderState被正确地更新,并且onDimmerChanged()被用正确的值调用。但是由于某种原因,当点击滑动(而不是滑动)时,valueSet被调用,并且sliderState.value不包含正确的值。我不明白这个值从哪里来的。

kzmpq1sx

kzmpq1sx1#

关于本地&服务器(或视图模型)状态相互冲突的原始问题:
我通过检测我们是否与滑块交互来解决这个问题:

  • 如果是,则显示并更新 * 本地状态值 *
  • 或者如果没有-则 * 显示视图模型值 *。

正如你所说的,我们应该永远不要onValueChange更新视图模型-因为这只是为了在本地更新滑块值(文档),而onValueChangeFinished是用来在我们完成交互后立即将***当前本地状态***发送到视图模型的。
关于电流相互作用的检测,我们可以利用InteractionSource

工作示例:

@Composable
fun SliderDemo() {
    // In this demo, ViewModel updates its progress periodically from 0f..1f
    val viewModel by remember { mutableStateOf(SliderDemoViewModel()) }

    Column(
        verticalArrangement = Arrangement.spacedBy(16.dp)
    ) {

        // local slider value state
        var sliderValueRaw by remember { mutableStateOf(viewModel.progress) }

        // getting current interaction with slider - are we pressing or dragging?
        val interactionSource = remember { MutableInteractionSource() }
        val isPressed by interactionSource.collectIsPressedAsState()
        val isDragged by interactionSource.collectIsDraggedAsState()
        val isInteracting = isPressed || isDragged

        // calculating actual slider value to display
        // depending on wether we are interacting or not
        // using either the local value, or the ViewModels / server one
        val sliderValue by derivedStateOf {
            if (isInteracting) {
                sliderValueRaw
            } else {
                viewModel.progress
            }
        }

        Slider(
            value = sliderValue, // using calculated sliderValue here from above
            onValueChange = {
                sliderValueRaw = it
            },
            onValueChangeFinished = {
                viewModel.updateProgress(sliderValue)
            },
            interactionSource = interactionSource
        )

        // Debug interaction info
        Text("isPressed: ${isPressed} | isDragged: ${isDragged}")
    }
}

希望能有所帮助。

pjngdqdw

pjngdqdw2#

这里有几件事要做,所以让我们试着把它分解一下。
最初的尝试是每次更改值都调用onDimmerChanged,这看起来很棒!
看一下第二次尝试,创建一个定制的Slider组件是可行的,但是有一些问题。

@Composable
fun LightView(
    channel: UiChannel,
    onDimmerChanged: (Float) -> Unit
) {
    val sliderValue = channel....
    Slider(initialValue = sliderValue, valueSetter = onDimmerChanged)
}

@Composable
fun Slider(initialValue: Float, valueSetter: (Float) -> Unit) {
    var value by remember { mutableStateOf(initialValue) }
    Slider(
        value = value,
        onValueChange = { value = it },
        onValueChangeFinished = { valueSetter(value) }
    )
}

让我们来讨论一下Slider组合中发生了什么:
1.我们还记得initialValue的状态
1.更新channel值,重新组合LightViewSlider也是如此
1.因为我们记住了状态,所以它仍然设置为以前的initialValue
你认为remember是罪魁祸首的想法是正确的。当记忆一个值时,除非我们告诉Compose,否则它在重新组合时不会更新。但是没有记忆(如第三次尝试所示),每次重组都会创建一个状态模型(var value by mutableStateOf(initialValue))。由于每次value改变时都会触发重新组合,因此我们总是将initialValue而不是更新后的值传递给Slider。使其永远不会根据此Composable中的更改进行更新。相反,我们希望将initialValue作为键传递给remember,告诉Compose在键更改时重新计算值。

@Composable
fun Slider(initialValue: Float, valueSetter: (Float) -> Unit) {
    var value by remember { mutableStateOf(initialValue) }
    Slider(
        value = value,
        onValueChange = { value = it },
        onValueChangeFinished = { valueSetter(value) }
    )
}

您可能也可以将Slider拉入您的LightView

@Composable
fun LightView(
    channel: UiChannel,
    onDimmerChanged: (Float) -> Unit
) {
    var value by remember(channel.sliderValue) { mutableStateOf(channel.sliderValue) }
    Slider(
        value = value,
        onValueChange = { value = it },
        onValueChangeFinished = { onDimmerChanged(value) }
    )
}

最后,关于@ commonsare建议的尝试。

@Composable
fun LightView(
    channel: UiChannel,
    onDimmerChanged: (Float) -> Unit
) {
    val sliderValue = channel....
    val sliderState = mutableStateOf(sliderValue)
    Slider(state = sliderState, valueSet = { onDimmerChanged(sliderState.value) })
}

@Composable
fun Slider(state: MutableState<Float>, valueSet: () -> Unit) {
    Slider(
        value = state.value,
        onValueChange = { state.value = it },
        onValueChangeFinished = valueSet
    )
}

这是另一种实现相同目的的方法,但是传递MutableState s是一种反模式,应该尽可能避免,这就是状态提升的作用所在(您在前面的尝试中尝试做的事情:)
最后,关于SlideronValueChangeFinished使用了错误的值。
而且它也不起作用。当使用拖放时,sliderState被正确更新,并且onDimmerChanged()被用正确的值调用。但是由于某种原因,当点击滑动(而不是滑动)时,valueSet被调用,并且sliderState.value不包含正确的值。我不明白这个值从哪里来。
这是Slider组件中的一个bug。您可以通过查看以下代码的输出来检查:

@Composable
fun Slider(initialValue: Float, valueSetter: (Float) -> Unit) {
    var value by remember { mutableStateOf(initialValue) }
    val key = Random.nextInt()
    Slider(
        value = value,
        onValueChange = { value = it },
        onValueChangeFinished = { 
            valueSetter(value)
            println("Callback invoked. Current key: $key")
        }
    )
}

由于key应该随着每次重新组合而改变,您可以看到onValueChangeFinished回调包含了对旧组合的引用(希望我没有说错)。
希望这有助于澄清一些事情!

dgjrabp2

dgjrabp23#

是的,我也可以重现你的问题。我在bug主题中回答了,但是我会复制粘贴到这里,这样其他人在匆忙的时候就可以修复它。不幸的是,这个解决方案需要复制整个Slider类。
问题在于,拖动和单击修改器使用不同的“位置”示例,尽管它们应该始终相同(因为“位置”是在“记住”中创建的)。
问题出在指针输入修饰符上。

val press = if (enabled) {
            Modifier.pointerInput(Unit) {...}

Modifier.pointerInput()更改为接受除Unit之外的任何其他主题,以便该修饰符中的Position示例始终是最新的,并在重新创建Position时进行更新。例如,可以将其更改为Modifier.pointerInput(valueRange)

w6lpcovy

w6lpcovy4#

我遇到了这个问题,正如jossiwolf所指出的,将进度作为key用于remember{}是必要的,以确保Slider进度在重新组合后更新。
我有一个额外的问题,虽然,在那里,如果我更新了进度中途寻找,滑块将重新组成,拇指将恢复到原来的位置。
为了解决这个问题,我使用了一个临时的滑块位置,它只在拖动过程中使用:

@Composable
fun MySlider(
    progress: Float,
    onSeek: (progress: Float) -> Unit,
) {
    val sliderPosition = remember(progress) { mutableStateOf(progress) }
    val tempSliderPosition = remember { mutableStateOf(progress) }
    val interactionSource = remember { MutableInteractionSource() }
    val isDragged = interactionSource.collectIsDraggedAsState()

    Slider(
        value = if (isDragged.value) tempSliderPosition.value else sliderPosition.value,
        onValueChange = { progress ->
            sliderPosition.value = progress
            tempSliderPosition.value = progress
        },
        onValueChangeFinished = {
            sliderPosition.value = tempSliderPosition.value
            onSeek(tempSliderPosition.value)
        },
        interactionSource = interactionSource
    )
}
jgovgodb

jgovgodb5#

我对Steffen的答案进行了一些详细的阐述,这样即使用户当前正在拖动滑块,也可以更新“external”“viewModel”值(因为有许多用例需要这样做)。
用法与合成“本机”Slider相同,不同之处在于onValueChangeFinished参数现在接受结束浮点值。

import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsDraggedAsState
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.material.Slider
import androidx.compose.material.SliderColors
import androidx.compose.material.SliderDefaults
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier

/**
 * "Wrapper" around the Slider solving the issue of overwriting values during drag. See https://stackoverflow.com/questions/66386039/jetpack-compose-react-to-slider-changed-value.
 * Use as normal Slider and feel free to update the underlying value in the onValueChange method.
 */
@Composable
fun ComposeSlider(
        value: Float,
        onValueChange: (Float) -> Unit,
        modifier: Modifier = Modifier,
        enabled: Boolean = true,
        valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
        steps: Int = 0,
        onValueChangeFinished: ((Float) -> Unit)? = null,
        interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
        colors: SliderColors = SliderDefaults.colors()
) {
    // local slider value state with default value (from the "external" source)
    var sliderValueRaw by remember { mutableStateOf(value) }

    // getting current interaction with slider - are we pressing or dragging?
    val isPressed by interactionSource.collectIsPressedAsState()
    val isDragged by interactionSource.collectIsDraggedAsState()
    val isInteracting = isPressed || isDragged

    // calculating actual slider value to display depending on whether we are interacting or not
    // using either the local value, or the provided one
    val determinedValueToShow by remember(isInteracting, sliderValueRaw, value) {
        derivedStateOf {
            if (isInteracting) {
                sliderValueRaw
            } else {
                value
            }
        }
    }

    Slider(
        value = determinedValueToShow,
        onValueChange = {
            sliderValueRaw = it
            onValueChange.invoke(it)
        },
        modifier = modifier,
        enabled = enabled,
        valueRange = valueRange,
        steps = steps,
        onValueChangeFinished = {
            onValueChangeFinished?.invoke(sliderValueRaw)
        },
        interactionSource = interactionSource,
        colors = colors
    )
}

相关问题