How to animate elements of a dynamic list in Jetpack Compose?

2k Views Asked by At

How can I transition elements of a list to the new list (possibly different size) with animation?

I have a pie chart and when its slices (fractions) change, I want to animate the previous fractions to the new fractions. The thing is, number of slices can be different each time.

If number of new slices is less than the current ones, the current extra slices should animate from their current fraction to 0.
If number of new slices is greater than the current ones, new extra slices should animate from 0 to their fractions.

@Composable fun PieChartCompose(slices: List<Float>) {
    val transitionData = updateTransitionData(slices)
    val fractions = transitionData.fractions
    // Draw the pie with Canvas using the fractions
}

I have currently implemented this with a list of constant size (10, so slices cannot be more than 10)
(note that the initial animation for chart appearance can be different from subsequent animations):

data class TransitionData(val slices: List<State<Float>>)

enum class ChartState { INITIALIZED, CHANGED }

@Composable fun updateTransitionData(
    targetFractions: List<Float>
): TransitionData {
    val mutableState = remember { MutableTransitionState(ChartState.INITIALIZED) }
    mutableState.targetState = ChartState.CHANGED
    val transition = updateTransition(mutableState, label = "main-animation")
    
    val fractions = listOf(
        transition.animateFloat(label = "fraction-0-animation") {
            if (it == ChartState.INITIALIZED) 0f
            else targetSlices.getOrNull(0)?.fraction ?: 0f
        },
        // ...
        transition.animateFloat(label = "fraction-10-animation") {
            if (it == ChartState.INITIALIZED) 0f
            else targetSlices.getOrNull(10)?.fraction ?: 0f
        }
    )
    return remember(transition) { TransitionData(fractions) }
}

Below is an example chart that initially has two slices and then animates to one slice
(the first slice animates to the single new fraction and the second slice animates to 0
they are a little inconsistent probably because of interpolations and animation specs):

Example desired animation

var slices by mutableStateOf(listOf(0.3f, 0.7f))
PieChartCompose(slices)
slices = listOf(1f)
1

There are 1 best solutions below

0
On

You can try having a dynamic amount of animateFloat.

Since we want to animate fractions that disappeared, we need to know the old fractions list (in case it's bigger than new one).
That's why I've changed the transition state to operate on fractions list. We can access the "old" state and find the "max" size (comparing old and new fractions list sizes).
The initial state is an empty list, so initially there will be animation from zero for the first fractions.

In animateFloat we try to take the fraction from the target state and if the fraction at that position is no longer there - then make it zero, so it will disappear.

I've also added remember(values) { } around updating values in animatedFractions which is not needed to work, but it's there rather for optimisation. If the count of values would not change then all existing objects would be reused and values list should be the same - then we do not need to update animatedFractions with new State objects.

From updateTransitionData a stable object is returned, with stable list inside. We only modify objects inside of that list. Because it's a SnapshotStateList it will take care of refreshing all Composables that iterate over it.

@Composable
fun updateTransitionData(
    targetFractions: List<Float>
): TransitionData {
    val mutableState = remember { MutableTransitionState(emptyList<Float>()) }
    mutableState.targetState = targetFractions
    val transition = updateTransition(mutableState, label = "main-animation")

    val maxFractionsSize = max(transition.currentState.size, targetFractions.size)
    val values = (0 until maxFractionsSize).map { index ->
        transition.animateFloat(label = "fraction-$index-animation") { state ->
            state.getOrNull(index) ?: 0f
        }
    }

    val animatedFractions = remember(transition) { SnapshotStateList<State<Float>>() }
    remember(values) {
        animatedFractions.clear()
        animatedFractions.addAll(values)
    }
    return remember(transition) { TransitionData(animatedFractions) }
}

Here is a quick "linear" demo, with slowed down animations, going through 4 different fractions lists:
enter image description here