Как анимировать элементы динамического списка в Jetpack Compose?

Как я могу перенести элементы списка в новый список (возможно, другого размера) с помощью анимации?

У меня есть круговая диаграмма, и когда ее срезы (фракции) меняются, я хочу анимировать предыдущие дроби в новые дроби. Дело в том, что количество ломтиков каждый раз может быть разным.

Если количество новых срезов меньше, чем текущих, текущие дополнительные срезы должны анимироваться с их текущей доли до.
Если количество новых срезов больше, чем текущих, новые лишние срезы должны анимироваться от до их фракций.

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

В настоящее время я реализовал это со списком постоянного размера (10, поэтому срезы не могут быть больше 10)
(обратите внимание, что начальная анимация для внешнего вида диаграммы может отличаться от последующих анимаций):

      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) }
}

Это пример диаграммы, которая изначально имеет два среза, а затем анимируется с одним срезом
(первый срез анимируется с единственной новой дробью, а второй срез анимируется для 0-
они немного несовместимы, вероятно, из-за спецификаций интерполяции и анимации):

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

1 ответ

Вы можете попробовать иметь динамическое количество.

Поскольку мы хотим оживить исчезнувшие дроби, нам нужно знать старый список дробей (на случай, если он больше нового).
Вот почему я изменил состояние перехода, чтобы работать со списком дробей. Мы можем получить доступ к «старому» состоянию и найти «максимальный» размер (сравнивая старые и новые размеры списков дробей).
Начальное состояние - пустой список, поэтому изначально для первых дробей будет анимация с нуля.

В animateFloat мы пытаемся взять дробь от цели state и если дроби в этой позиции больше нет - сделайте ее равной нулю, и она исчезнет.

Я также добавил remember(values) { }вокруг обновления значений, в которых не нужно работать, а скорее для оптимизации. Если количество не изменится, все существующие объекты будут повторно использованы и values список должен быть таким же - тогда нам не нужно обновлять animatedFractions с новым State объекты.

Из updateTransitionDataвозвращается стабильный объект со стабильным списком внутри. Мы изменяем только объекты внутри этого списка. Потому что это SnapshotStateList он позаботится об обновлении всего Composables которые повторяют его.

      @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) }
}

Вот быстрая «линейная» демонстрация с замедленной анимацией, проходящей через 4 разных списка фракций:

Другие вопросы по тегам