o

    Olivier Patry

    1 year ago
    I have a difficulties to deal with the
    pointerInput
    Modifier
    , I try to implement a custom drag behavior. Based on a position on a
    Box
    I compute a color. If I use
    var color by remember { mutableStateOf(initialColor) }
    inside my composable and update color value based on drag position it works. If I extract a state (to allow parent composable to consume it), and hoist it, it doesn't work anymore. My understanding is that the value captured by
    onDrag
    is stale and still consider the initial value used and never update it. I don't understand the difference with state being hoisted or inline within Composable… (code in thread)
    class ColorPickerState(val originalColor: Color) {
        var targetColor by mutableStateOf(originalColor)
    }
    @Composable
    fun MainColorHueControl(
        state: ColorPickerState,
        enabled: Boolean = true,
    ) {
        var origin by remember { mutableStateOf(Offset.Zero) }
    
        val thumbSize = 20.dp
        val wheelSize = 144.dp + thumbSize
        val (hue, saturation, brightness) = state.targetColor.toHSB()
        val hueAngle = Math.toRadians((360 - (hue * 360)).toDouble())
        val halfSize = wheelSize / 2
        val radius = (wheelSize - thumbSize) / 2
    
        val position = Offset(
            (radius.value * cos(hueAngle)).toFloat(),
            (radius.value * sin(hueAngle)).toFloat()
        )
    
        val thumbX = (position.x).dp + halfSize - (thumbSize / 2)
        val thumbY = (position.y).dp + halfSize - (thumbSize / 2)
    
        Box(
            Modifier.size(wheelSize)
                .onSizeChanged { origin = it.center.toOffset() }
                .pointerInput(Unit) {
                    detectDragGestures(
                        onDragStart = { offset ->
                            val next = offset - origin
                            state.targetColor = next.toColor(saturation, brightness)
                        },
                        onDrag = { change, amount ->
                            val next = position + amount
                            change.consumeAllChanges()
                            state.targetColor = next.toColor(saturation, brightness)
                        }
                    )
                }
    The color start to be modified and then is stuck. It's particularly visible if I allow to change the color (on press for instance), then if I start a drag, the considered colored is the initial one…
    (if I log
    position
    in
    onDrag
    we can see the value matches the position of the original color, not the last defined one)
    I don't think I can give the color as key of
    pointerInput(Unit)
    , the drag sequence seems not working either
    Adam Powell

    Adam Powell

    1 year ago
    If you want to have composition update a value out from under a running effect/coroutine,
    rememberUpdatedState
    may be what you're looking for
    the syntax gets a little funny here since you can't use it with destructuring
    but in essence you would have follow-up lines like this:
    val currentSaturation by rememberUpdatedState(saturation)
    and then use
    currentSaturation
    in your gesture detection code instead of
    saturation
    o

    Olivier Patry

    1 year ago
    it's "better" but I still have an issue it seems
    I used
    rememberUpdatedState
    for
    position
    (assimilated to
    hue
    , it's a position on a circle),
    saturation
    and
    brightness
    .
    If I go slowly, it "kind of" work (not super accurate and it seems some recomposition are missing)
    If I go quickly it starts to go crazy
    (initially, the color update is animated, and when animating the change, it's even worse)
    (compared to "working" (but less appropriate for my use case))
    Adam Powell

    Adam Powell

    1 year ago
    hmm, reading this again, yeah the data flow doesn't look right and this is deriving a new value from the same target that is being set
    I'm not sure why the old color is an input to determining the new color here; it looks like the position on the wheel for target colors is always fixed
    o

    Olivier Patry

    1 year ago
    yes, I think there is an issue linked to the flow, I agree but I can't get it
    let me explain my intention
    This composable consumes an original color and a current one. I determine the position of the thumb on the circle based on the current color's
    hue
    . Now, I can change the hue either by press or drag on the circle. When I press, I change the
    state.targetColor
    (based on clicked position on the circle transformed to hue) and my composable is recomposed, it works fine. When I drag, I do the exact same thing but it doesn't work. The difference is that the
    onDrag
    relies on the
    position
    being computed for previous step, because I get a "amount" of drag, not an absolute position so I need something to add.
    Hope my goal is clearer now.
    (I borrowed this logic from Leland's Countdown app)
    On Leland's impl, the position is stored in the state class, maybe, it impacts the behavior?
    My alternative impl wasn't exactly doing the same thing in fact, I was dealing with remembered position, and then computing the color to consider based on such position. Could be linked to this?
    I lost myself now 😅
    (previous "working" impl
    var position by remember { mutableStateOf(Offset.Unspecified) }
        val currentColor = when {
            position != Offset.Unspecified -> Color.fromHSB(position.toHue(), saturation, brightness)
            else -> originalColor
        }
    ...
        val (hue, _, _) = currentColor.toHSB()
    ...
                .pointerInput(Unit) {
                    detectDragGestures(
    ...
                        onDrag = { change, amount ->
                            val prev = if (position == Offset.Unspecified) Offset.Zero else position
                            val next = prev + amount
                            position = next
                            change.consumeAllChanges()
                        }
    But for me the data model is the color stored in the state and the composable should recomputes its whole UI state based on this 🤔 (and updates the state in terms of color)
    Adam Powell

    Adam Powell

    1 year ago
    oh I see, so yeah, it's entirely the working in deltas instead of absolute positions
    in that case I'd probably drop
    detectDragGestures
    altogether and work with the events coming in using absolute coordinates
    o

    Olivier Patry

    1 year ago
    Ok, so I guess I catch was caused my problem thanks to your lead
    hmm, reading this again, yeah the data flow doesn't look right and this is deriving a new value from the same target that is being set
    I separated the "dragPosition" and the "thumbPosition"
    and everything works fine now
    the dragPosition isn't constrained on the circle and is kind of independent of the hue position in fact
    so, in addition to
    position
    , I add
    var dragPosition by remember { mutableStateOf(Offset.Unspecified) }
    and I consume this instead of
    position
    in my
    detectDragGestures
    implementation
    Here is the correct behavior (I still have some bugs to clean near the "undo/press" sequence but that shouldn't be a huge problem I think, also the Saturation & Brightness sliders do not behave properly, not investigated yet)
    Thanks a lot @Adam Powell 🙇