android-jetpack-composematerial-components-androidjetpack-compose-animation

How to interrupt and restart a Compose AnimatedVisibility animation


Given an animation showing an Int value where the composable starts small and gets bigger (using scaleIn) how can the animation be immediately interrupted and restarted?

In the following example we want to show a Badge showing a total or sum when it is greater than zero. Each time the user hits a plus button, the total will increment by one, and each time a minus button is hit, the total will decrement by one.

@Composable
fun Counter(total: Int) {
    var visible = remember { MutableTransitionState(false) }
    LaunchedEffect(total) {
        visible.targetState = total > 0
    }
    AnimatedVisibility(visibleState = visible, enter = scaleIn(), exit = None) {
        Badge(backgroundColor = primaryRed) {
            Text(text = total)
        }
    }

The key is, we need the animation to completely reanimate each time either button is pressed. So if you rapidly hit the plus button then you should see the badge immediately disappear and then animatedly re-appear with the new value. Whether the animation is finished or in progress, the badge should immediately be gone and then re-animate each time either button is pressed.

With the code as is, it seems the only thing that works is to put a delay in the LaunchedEffect, like this:

    LaunchedEffect(total) {
        visible.targetState = false
        delay(100)
        visible.targetState = total > 0
    }

For what should be obvious reasons, that doesn't feel right. How can this be done without using delay()?


Solution

  • The crucial thing is to use a key in the remember statement:

    var visible = remember(total) { MutableTransitionState(false) }
    

    That way, as soon as total changes, the remember initialization block is re-evaluated which sets visible to false. This immediately triggers the "exit" animation (set to None, so the badge disappears) and then the LaunchedEffect executes, sets the targetState, and the "enter" animation starts.

        var visible = remember(total) { MutableTransitionState(false) }
        LaunchedEffect(total) {
            visible.targetState = total > 0
        }