'Will UpdateTransition animation maintain its running velocity when if being changed to new animation?

I create the same animation using AnimationAsState, Animatable, and UpdateTransition and try them out.

All the animations are using both moving forward or backward.

    tween(
        durationMillis = 3000,
        easing = LinearOutSlowInEasing
    )

We can see it below

enter image description here

If the animation is performed halfway through, and I click the button again, I would assume the running animation velocity is maintained and passed on to the subsequent animation.

This seems correct for AnimateAsState and Animatable. However, to my surprise, the UpdateTransition doesn't seem to behave that way. I guess it got resumed to the default animation duration (which runs really fast).

We can see as below.

enter image description here

My question is, is it

  1. expected that UpdateTransition won't retain the running animation spec if it get cancel halfway by a subsequent animation?
  2. I miss something on my code (I share the entire code below)?
  3. This is an UpdateTransition bug, and need to be reported to Google?

The entire working code for the above animation as below

import androidx.compose.animation.core.*
import androidx.compose.animation.core.Animatable
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp

@Composable
fun Combination() {
    var enabled by remember { mutableStateOf(false) }

    val dbAnimateAsState: Dp by animateDpAsState(
        targetValue = switch(enabled),
        animationSpec = animationSpec()
    )

    val dbAnimatable = remember { Animatable(0.dp) }

    val transition = updateTransition(enabled, label = "")
    val dbTransition by transition.animateDp(
        transitionSpec = { animationSpec() }, label = "") {
        switch(it)
    }

    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {

        Text("AnimateAsState")
        animateBoxHorizontal(dbAnimateAsState)
        Text("Animatable")
        animateBoxHorizontal(dbAnimatable.value)
        Text("UpdateTransition")
        animateBoxHorizontal(dbTransition)

        Button(onClick = { enabled = !enabled }) {
            Text("Click Me")
        }
    }

    LaunchedEffect(key1 = enabled) {
        dbAnimatable.animateTo(
            targetValue = switch(enabled),
            animationSpec = animationSpec()
        )
    }
}

private fun animationSpec(): TweenSpec<Dp> =
    tween(
        durationMillis = 3000,
        easing = LinearOutSlowInEasing
    )

private fun switch(enabled: Boolean) = if (enabled) 268.dp else 0.dp

fun Animatable(initialValue: Dp) = Animatable(
    initialValue,
    DpToVector,
)

private val DpToVector: TwoWayConverter<Dp, AnimationVector1D> =
    TwoWayConverter({ AnimationVector1D(it.value) }, { it.value.dp })

@Composable
private fun animateBoxHorizontal(dbAnimateAsState: Dp) {
    Box(
        modifier = Modifier
            .height(32.dp)
            .width(300.dp)
            .background(Color.Yellow)
    ) {
        Box(
            modifier = Modifier
                .offset(dbAnimateAsState, 0.dp)
                .size(32.dp)
                .background(Color.Red)
        )
    }
    Spacer(modifier = Modifier.height(16.dp))
}

Note: Updated Info

If I change from tweenSpec to springSpec i.e.

    tween(
        durationMillis = 3000,
        easing = LinearOutSlowInEasing
    )

to

    spring(stiffness =20f, dampingRatio = 0.25f)

Then updateTransition works as usual, where the Spring velocity and animation are preserved and continuous when we interrupt the animation with a new one.



Solution 1:[1]

After more investigation, apparently, this is by design.

If we check the Google Transition class code of class Transition<S> @PublishedApi internal constructor

We can see

 val spec = if (isInterrupted) {
                // When interrupted, use the default spring, unless the spec is also a spring.
                if (animationSpec is SpringSpec<*>) animationSpec else interruptionSpec
            } else {
                animationSpec
            }

And

interruptionSpec = spring(visibilityThreshold = visibilityThreshold)

So the interruptionSpec is set to Spring, and if the user is providing a Spring, it get preserved. If not it will be set to spring(visibilityThreshold = visibilityThreshold).

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Elye