I have a difficulties to deal with the `pointerInp...
# compose
o
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)
Copy code
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
a
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:
Copy code
val currentSaturation by rememberUpdatedState(saturation)
and then use
currentSaturation
in your gesture detection code instead of
saturation
o
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))
a
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
1
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
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
Copy code
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)
a
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
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
👍 1
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
Copy code
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 🙇
👍 1