I am extending the Modifier interface, to add my o...
# compose
g
I am extending the Modifier interface, to add my own button bounce on the action down event, I am detecting this via the
pointerInteropFilter
and assigning to a 3 state enum and storing in
Copy code
var buttonActionState by remember { mutableStateOf(ButtonActionState.NOT_PRESSED) }
95% of the time it works fine, but when I slightly move my mouse(via remote screen), then tap again I can see
buttonActionState
being set, it being a different value to the old one, but there is no recomposition. Anyone any ideas? I can provide the full function if required. Cheers
l
Not sure if this is what is happening here, but if you change the state twice in the same frame, e.g from not pressed to pressed and back to not pressed, then recomposition won’t happen
g
i’ve a feeling thats whats happening. atleast the logs reflect it. i see DOWN being emitted, but no recomposition magic happening, then UP occurring and the recomp happening. Is there anyway I can confirm or fix this?
l
You should be able to reproduce by manually changing the state twice in quick succession. For ‘fix’, it depends what you want to do. Showing one state for one frame will just appear as a flicker anyway, and probably looks worse than not showing the intermediate state. If you want a minimum duration for each state, you would need to implement this at the place you change the state for example
g
i was trying to use the
awaitFrame()
but no dice unfortunately, still same issue occurs in an odd way, because now it doesnt even set the value
l
You probably want a delay of some minimum amount of time. Ripples for example have a minimum duration of 225ms
g
Its a very odd bug tbh, because the animation works fine most the time, but if I move the mouse ever so slightly and tap, it doesnt. so it doesn’t seem to related to timing but what do i know
l
Hard to say without more information. In the general case using a pointerInputFilter here is a bit of a hack and seems suspicious
You probably want to use InteractionSource instead to track presses / releases
☝️ 1
g
hmm interesting, ill give that a gander, thanks
lawls its nuts, even with implementing an interaction source the same bug exists. i wonder if what am asking can be done with a modifier extension
l
Can you share some code?
g
sure, the code is currently something like
Copy code
@Composable
fun Modifier.bounceAnimation(
    scale: Float = 1.2f,
    onFinish: () -> Unit,
) : Modifier {
    var buttonActionState by remember { mutableStateOf(ButtonActionState.NOT_PRESSED) }

    val scaleState by animateFloatAsState(
        targetValue = if (buttonActionState == ButtonActionState.PRESSED) scale else ONE,
        label = "scaleState",
        animationSpec = SpringSpec(
            dampingRatio = Spring.DampingRatioMediumBouncy,
            stiffness = Spring.StiffnessMedium,
        ),
        finishedListener = {
            if (buttonActionState == ButtonActionState.NOT_PRESSED) onFinish()
        },
    )

    return this then pointerInteropFilter {
        buttonActionState = when (it.action) {
            MotionEvent.ACTION_DOWN -> ButtonActionState.PRESSED
            MotionEvent.ACTION_UP -> ButtonActionState.NOT_PRESSED
            else -> ButtonActionState.IGNORE
        }
        true
    }.graphicsLayer {
        scaleX = scaleState
        scaleY = scaleState
    }
}
introducing the interaction source means id have to change the contract which i am trying to avoid but that attempt was something like
the enable stuff can be ignored, its just a hang over from me trying not to break the contract
with the interaction source stuff itd be
Copy code
@Composable
fun Modifier.bounceAnimation(
    scale: Float = 1.2f,
    interactionSource: InteractionSource = remember { MutableInteractionSource() },
    onFinish: () -> Unit,
) : Modifier {
    val isPressed by interactionSource.collectIsPressedAsState()

    val scaleState by animateFloatAsState(
        targetValue = if (isPressed) scale else ONE,
        label = "scaleState",
        animationSpec = SpringSpec(
            dampingRatio = Spring.DampingRatioMediumBouncy,
            stiffness = Spring.StiffnessMedium,
        ),
        finishedListener = {
            if (!isPressed) onFinish()
        },
    )

    return this then graphicsLayer {
        scaleX = scaleState
        scaleY = scaleState
    }
}
the problem i think ill have by using the interaction source stuff is, if the user presses down, then scrolls, i need to cancel the action but complete the animation
l
If the user starts to scroll, it will no longer be pressed, so that should be fine? Unless I’m misunderstanding
What is the ‘bug’ you are seeing here? In the second approach it seems like it should work fine, except for as I mentioned the issue with minimum duration for presses, if you quickly become pressed and unpressed
g
thats correct, both snippets animate as they should in most cases, i found that i can listen to the difference from cancel/release in interactionSource which is handy. but going with that approach means i have to change the contract which i am currently trying to avoid
l
Well the current contract is not possible to implement ‘correctly’. At a bare minimum, this modifier will include touch events that are in clipped regions of the button, so it will trigger scales when the button itself is not considered pressed. E.g if you have a rounded corner button, this modifier will trigger in the corner that is outside the rounded corner, but the button won’t
g
i know, it’s not ideal am not gonna lie, just one of those things that needs reimplementing. but currently not feasible on the eve of a release 😄
from a straight up copy and paste and usage, it also sometimes misses the animation, it registers the click event in the logs, but no animation occurs, its maddening
l
There isn’t a ‘correct’ behavior here if you want the animation to start and finish in one frame, you would have to define a meaningful minimum duration
g
how would one define a minimum duration?
l
Basically, instead of using collectIsPressedAsState, just manually collect interactions, and delay changing the state until some minimum time has passed
There’s a (complicated) example in the docs (https://developer.android.com/develop/ui/compose/touch-input/user-interactions/handling-interactions#build-advanced) that does this sort of thing
g
ah i see, i guess thats just become tomorrows headache
thanks for the help
hmm so ive got the scale indication working as i need it, but the
onclick
is always triggered beforehand, or sometimes after the start of the animation but doesnt wait till the end, meaning if for example we are navigating away we never really see it or only partially see it.
I can make it work by passing down a lambda to listen for when we finish animating but would have been nice if the indication is completed then onclick is carried out, ah wells
l
Right, this is pretty typical - you don't usually want to delay clicks to show these kinds of effects. You could add a delay if you want, and invoke in the onClick inside the indication, but it's a bit suspicious
g
I currently invoke the onFinish, so theres no artificial delay but yah no ideal imo, on the plus side it works perfectly now, but it means I need to go updating all the uses of the previous scale not long before a release 😂