Hello! Is my understanding wrong of what should ha...
# compose
z
Hello! Is my understanding wrong of what should happen or is this a bug? When running an
updateTransition
, any updates to the
targetState
seem to not respect the passed
transitionSpec
but to create a new one with default values. E.g.: 1. Have an
updateTransition.animateFloat
with
transitionSpec = { tween(4000, 0, LinearEasing) }
2. Start a transition between states A → B 3. While the transition is running, set a new
targetState
for the transition: C ER: The transition towards the new state finishes in
4000ms
with
LinearEasing
AR: The transition towards the new state finishes in
300ms
with
FastOutSlowIn
easing (default values of
TweenSpec
)
Snippet:
Copy code
private enum class SomeState {
    State1,
    State2,
    State3,
}

@Composable
private fun SpecTest() {
    val currentState = remember { MutableTransitionState(SomeState.State1) }
    val animatedValue = DoStuff(currentState)
    Log.d("Spec test", animatedValue.value.toString())

    LaunchedEffect(Unit) {
        delay(50)
        Log.d("Spec test", "New target: State 2")
        currentState.targetState = SomeState.State2
    }

    LaunchedEffect(Unit) {
        delay(1250)
        Log.d("Spec test", "New target: State 3")
        currentState.targetState = SomeState.State3
    }
}

@Composable
private fun DoStuff(currentState: MutableTransitionState<SomeState>): State<Float> {
    val transition = updateTransition(currentState)
    val animatedValue = transition.animateFloat(
        transitionSpec = { tween(4000, 0, LinearEasing) },
        targetValueByState = {
            when (it) {
                SomeState.State1 -> 0f
                SomeState.State2 -> -10f
                SomeState.State3 -> 10f
            }
        }
    )
    return animatedValue
}
Result:
Copy code
22:55:33.183 D/Spec test: New target: State 2
22:55:33.197 D/Spec test: 0.0
22:55:33.213 D/Spec test: 0.0
22:55:33.228 D/Spec test: -0.040000003
22:55:33.245 D/Spec test: -0.080000006
22:55:33.261 D/Spec test: -0.122499995
22:55:33.277 D/Spec test: -0.1625
22:55:33.294 D/Spec test: -0.2025
22:55:33.311 D/Spec test: -0.24499999
22:55:33.326 D/Spec test: -0.285
22:55:33.343 D/Spec test: -0.325
22:55:33.359 D/Spec test: -0.3675
22:55:33.375 D/Spec test: -0.4075
22:55:33.392 D/Spec test: -0.45000002
22:55:33.408 D/Spec test: -0.48999998
22:55:33.425 D/Spec test: -0.53
22:55:33.441 D/Spec test: -0.5725
22:55:33.457 D/Spec test: -0.6125
22:55:33.473 D/Spec test: -0.65250003
22:55:33.489 D/Spec test: -0.695
22:55:33.506 D/Spec test: -0.735
22:55:33.523 D/Spec test: -0.775
22:55:33.539 D/Spec test: -0.8175
22:55:33.555 D/Spec test: -0.85749996
22:55:33.571 D/Spec test: -0.90000004
22:55:33.587 D/Spec test: -0.93999994
22:55:33.604 D/Spec test: -0.97999996
22:55:33.620 D/Spec test: -1.0225
22:55:33.637 D/Spec test: -1.0625
22:55:33.653 D/Spec test: -1.1025001
22:55:33.669 D/Spec test: -1.145
22:55:33.686 D/Spec test: -1.1850001
22:55:33.702 D/Spec test: -1.2275
22:55:33.719 D/Spec test: -1.2675
22:55:33.735 D/Spec test: -1.3075
22:55:33.751 D/Spec test: -1.35
22:55:33.768 D/Spec test: -1.39
22:55:33.784 D/Spec test: -1.4300001
22:55:33.800 D/Spec test: -1.4725
22:55:33.817 D/Spec test: -1.5125
22:55:33.833 D/Spec test: -1.5525
22:55:33.849 D/Spec test: -1.595
22:55:33.866 D/Spec test: -1.635
22:55:33.882 D/Spec test: -1.6775
22:55:33.898 D/Spec test: -1.7175
22:55:33.915 D/Spec test: -1.7575
22:55:33.931 D/Spec test: -1.8000001
22:55:33.947 D/Spec test: -1.84
22:55:33.964 D/Spec test: -1.8799999
22:55:33.980 D/Spec test: -1.9225
22:55:33.996 D/Spec test: -1.9625001
22:55:34.013 D/Spec test: -2.0025
22:55:34.029 D/Spec test: -2.045
22:55:34.045 D/Spec test: -2.085
22:55:34.062 D/Spec test: -2.1275
22:55:34.078 D/Spec test: -2.1675
22:55:34.095 D/Spec test: -2.2075
22:55:34.111 D/Spec test: -2.25
22:55:34.127 D/Spec test: -2.29
22:55:34.144 D/Spec test: -2.33
22:55:34.173 D/Spec test: -2.3725
22:55:34.176 D/Spec test: -2.4125
22:55:34.193 D/Spec test: -2.4525
22:55:34.209 D/Spec test: -2.4950001
22:55:34.226 D/Spec test: -2.535
22:55:34.242 D/Spec test: -2.5775
22:55:34.258 D/Spec test: -2.6175
22:55:34.275 D/Spec test: -2.6574998
22:55:34.291 D/Spec test: -2.7
22:55:34.307 D/Spec test: -2.7399998
22:55:34.323 D/Spec test: -2.78
22:55:34.339 D/Spec test: -2.8224998
22:55:34.356 D/Spec test: -2.8625
22:55:34.372 D/Spec test: -2.9025002
22:55:34.385 D/Spec test: New target: State 3
22:55:34.391 D/Spec test: -2.945
22:55:34.406 D/Spec test: -1.3040949
22:55:34.422 D/Spec test: 1.582622
22:55:34.440 D/Spec test: 4.3585196
22:55:34.457 D/Spec test: 6.3139114
22:55:34.473 D/Spec test: 7.6665077
22:55:34.489 D/Spec test: 8.59953
22:55:34.505 D/Spec test: 9.148899
22:55:34.521 D/Spec test: 9.48955
22:55:34.538 D/Spec test: 9.7069
22:55:34.554 D/Spec test: 9.827664
22:55:34.571 D/Spec test: 9.902748
22:55:34.587 D/Spec test: 9.943596
22:55:34.604 D/Spec test: 9.967458
22:55:34.621 D/Spec test: 9.981949
22:55:34.638 D/Spec test: 10.0
d
The built-in interruption handling gets triggered when target changes (e.g. to C) while the animation (e.g. A -> B) is in flight. A default spring will be used to animate to the new state after interruption unless the spec for B->C uses a spring. The reason why we don't use the spec for B->C for this interruption case is because: 1. B->C spec doesn’t always work well, especially if B->C animationSpec is dependent on values defined in B and C. For example: • snap()  • keyframes() 2. Tween works somewhat well, but can significantly increase/shorten the overall duration. E.g. Interrupted when nearly finished, and therefore start another 500ms long tween.
We have considered making this interruption handling customizable, but we haven't received any feature request until you asked about it here. 😄
z
I see! Thanks for the answer Doris. This behaviour is quite unexpected though, as the default spring finishes the B→C animation so abruptly that it feels almost like dropping it. For a good while we tried to figure out what other mistakes we were making to produce this, as it felt like a bug.
Tween works somewhat well, but can significantly increase/shorten the overall duration. E.g. Interrupted when nearly finished, and therefore start another 500ms long tween.
Actually both these use-cases can make sense to client code in different situations: 1. reusing the remaining amount of time to finish animating towards a new target – adds predictability to carry out an operation on the UI in a given time 2. restarting the timer on every interruption – adds a fluid UX for interacting with the UI when end time is not important It would be great to have this as a configuration parameter. And then I assume the base scenario could work as well?
d
reusing the remaining amount of time to finish animating towards a new target – adds predictability to carry out an operation on the UI in a given time
This has the potential to look very strange - you could be interrupted when there's 30ms remaining from the interrupted animation, and quickly arriving at the new target within 30 ms could look like a jump cut.
restarting the timer on every interruption – adds a fluid UX for interacting with the UI when end time is not important
This is what we ended up doing using springs - the timer is restarted and the value & velocity at the point of interruption is accounted for in the spring animation.
It would be great to have this as a configuration parameter. And then I assume the base scenario could work as well?
We'll look into making this configurable. In the meantime, I'd suggest trying out the idea of reusing the remaining time in a prototype (maybe using Animatable) to see whether that's desirable for your use case. 🙂 Thanks for filing the feature request