Анимация на основе значений

Анимируйте отдельное значение с помощью animate*AsState

Функции animate*AsState — это простейшие API анимации в Compose для анимации одного значения. Вы указываете только целевое значение (или конечное значение), а API начинает анимацию от текущего значения до указанного значения.

Ниже приведен пример анимации альфы с использованием этого API. Просто обернув целевое значение в animateFloatAsState , значение альфы теперь является значением анимации между предоставленными значениями ( 1f или 0.5f в данном случае).

var enabled by remember { mutableStateOf(true) }

val animatedAlpha: Float by animateFloatAsState(if (enabled) 1f else 0.5f, label = "alpha")
Box(
    Modifier
        .fillMaxSize()
        .graphicsLayer { alpha = animatedAlpha }
        .background(Color.Red)
)

Обратите внимание, что вам не нужно создавать экземпляр какого-либо класса анимации или обрабатывать прерывание. Под капотом объект анимации (а именно, экземпляр Animatable ) будет создан и сохранен в месте вызова с первым целевым значением в качестве его начального значения. С этого момента, всякий раз, когда вы предоставляете этому компонуемому объекту другое целевое значение, анимация автоматически запускается в направлении этого значения. Если анимация уже находится в полете, анимация начинается с ее текущего значения (и скорости) и анимируется в направлении целевого значения. Во время анимации этот компонуемый объект перекомпоновывается и возвращает обновленное значение анимации каждый кадр.

Из коробки Compose предоставляет функции animate*AsState для Float , Color , Dp , Size , Offset , Rect , Int , IntOffset и IntSize . Вы можете легко добавить поддержку других типов данных, предоставив TwoWayConverter для animateValueAsState , который принимает универсальный тип.

Вы можете настроить характеристики анимации, указав AnimationSpec . Для получения дополнительной информации см. AnimationSpec .

Анимируйте несколько свойств одновременно с помощью перехода

Transition управляет одной или несколькими анимациями как дочерними и запускает их одновременно между несколькими состояниями.

Состояния могут быть любого типа данных. Во многих случаях вы можете использовать пользовательский тип enum для обеспечения безопасности типов, как в этом примере:

enum class BoxState {
    Collapsed,
    Expanded
}

updateTransition создает и запоминает экземпляр Transition и обновляет его состояние.

var currentState by remember { mutableStateOf(BoxState.Collapsed) }
val transition = updateTransition(currentState, label = "box state")

Затем вы можете использовать одну из функций расширения animate* для определения дочерней анимации в этом переходе. Укажите целевые значения для каждого из состояний. Эти функции animate* возвращают значение анимации, которое обновляется каждый кадр во время анимации, когда состояние перехода обновляется с помощью updateTransition .

val rect by transition.animateRect(label = "rectangle") { state ->
    when (state) {
        BoxState.Collapsed -> Rect(0f, 0f, 100f, 100f)
        BoxState.Expanded -> Rect(100f, 100f, 300f, 300f)
    }
}
val borderWidth by transition.animateDp(label = "border width") { state ->
    when (state) {
        BoxState.Collapsed -> 1.dp
        BoxState.Expanded -> 0.dp
    }
}

При желании можно передать параметр transitionSpec , чтобы указать разные AnimationSpec для каждой комбинации изменений состояния перехода. Подробнее см. AnimationSpec .

val color by transition.animateColor(
    transitionSpec = {
        when {
            BoxState.Expanded isTransitioningTo BoxState.Collapsed ->
                spring(stiffness = 50f)

            else ->
                tween(durationMillis = 500)
        }
    }, label = "color"
) { state ->
    when (state) {
        BoxState.Collapsed -> MaterialTheme.colorScheme.primary
        BoxState.Expanded -> MaterialTheme.colorScheme.background
    }
}

После того, как переход достигнет целевого состояния, Transition.currentState будет таким же, как Transition.targetState . Это можно использовать как сигнал того, завершился ли переход.

Иногда мы хотим иметь начальное состояние, отличное от первого целевого состояния. Мы можем использовать updateTransition с MutableTransitionState , чтобы добиться этого. Например, это позволяет нам начать анимацию, как только код входит в композицию.

// Start in collapsed state and immediately animate to expanded
var currentState = remember { MutableTransitionState(BoxState.Collapsed) }
currentState.targetState = BoxState.Expanded
val transition = rememberTransition(currentState, label = "box state")
// ……

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

enum class DialerState { DialerMinimized, NumberPad }

@Composable
fun DialerButton(isVisibleTransition: Transition<Boolean>) {
    // `isVisibleTransition` spares the need for the content to know
    // about other DialerStates. Instead, the content can focus on
    // animating the state change between visible and not visible.
}

@Composable
fun NumberPad(isVisibleTransition: Transition<Boolean>) {
    // `isVisibleTransition` spares the need for the content to know
    // about other DialerStates. Instead, the content can focus on
    // animating the state change between visible and not visible.
}

@Composable
fun Dialer(dialerState: DialerState) {
    val transition = updateTransition(dialerState, label = "dialer state")
    Box {
        // Creates separate child transitions of Boolean type for NumberPad
        // and DialerButton for any content animation between visible and
        // not visible
        NumberPad(
            transition.createChildTransition {
                it == DialerState.NumberPad
            }
        )
        DialerButton(
            transition.createChildTransition {
                it == DialerState.DialerMinimized
            }
        )
    }
}

Используйте переход с AnimatedVisibility и AnimatedContent

AnimatedVisibility и AnimatedContent доступны как функции расширения Transition . targetState для Transition.AnimatedVisibility и Transition.AnimatedContent выводится из Transition и запускает переходы входа/выхода по мере необходимости, когда targetState Transition изменяется. Эти функции расширения позволяют поднять все анимации входа/выхода/sizeTransform, которые в противном случае были бы внутренними для AnimatedVisibility / AnimatedContent , в Transition . С помощью этих функций расширения изменение состояния AnimatedVisibility / AnimatedContent можно наблюдать снаружи. Вместо visible логического параметра эта версия AnimatedVisibility принимает лямбду, которая преобразует целевое состояние родительского перехода в логическое значение.

Подробную информацию смотрите в разделах AnimatedVisibility и AnimatedContent .

var selected by remember { mutableStateOf(false) }
// Animates changes when `selected` is changed.
val transition = updateTransition(selected, label = "selected state")
val borderColor by transition.animateColor(label = "border color") { isSelected ->
    if (isSelected) Color.Magenta else Color.White
}
val elevation by transition.animateDp(label = "elevation") { isSelected ->
    if (isSelected) 10.dp else 2.dp
}
Surface(
    onClick = { selected = !selected },
    shape = RoundedCornerShape(8.dp),
    border = BorderStroke(2.dp, borderColor),
    shadowElevation = elevation
) {
    Column(
        modifier = Modifier
            .fillMaxWidth()
            .padding(16.dp)
    ) {
        Text(text = "Hello, world!")
        // AnimatedVisibility as a part of the transition.
        transition.AnimatedVisibility(
            visible = { targetSelected -> targetSelected },
            enter = expandVertically(),
            exit = shrinkVertically()
        ) {
            Text(text = "It is fine today.")
        }
        // AnimatedContent as a part of the transition.
        transition.AnimatedContent { targetState ->
            if (targetState) {
                Text(text = "Selected")
            } else {
                Icon(imageVector = Icons.Default.Phone, contentDescription = "Phone")
            }
        }
    }
}

Инкапсулируйте переход и сделайте его пригодным для повторного использования

Для простых случаев использования определение анимаций перехода в том же компонуемом элементе, что и ваш UI, является вполне допустимым вариантом. Однако, когда вы работаете над сложным компонентом с несколькими анимированными значениями, вам может потребоваться отделить реализацию анимации от компонуемого UI.

Это можно сделать, создав класс, содержащий все значения анимации, и функцию «обновления», которая возвращает экземпляр этого класса. Реализацию перехода можно извлечь в новую отдельную функцию. Этот шаблон полезен, когда необходимо централизовать логику анимации или сделать сложные анимации повторно используемыми.

enum class BoxState { Collapsed, Expanded }

@Composable
fun AnimatingBox(boxState: BoxState) {
    val transitionData = updateTransitionData(boxState)
    // UI tree
    Box(
        modifier = Modifier
            .background(transitionData.color)
            .size(transitionData.size)
    )
}

// Holds the animation values.
private class TransitionData(
    color: State<Color>,
    size: State<Dp>
) {
    val color by color
    val size by size
}

// Create a Transition and return its animation values.
@Composable
private fun updateTransitionData(boxState: BoxState): TransitionData {
    val transition = updateTransition(boxState, label = "box state")
    val color = transition.animateColor(label = "color") { state ->
        when (state) {
            BoxState.Collapsed -> Color.Gray
            BoxState.Expanded -> Color.Red
        }
    }
    val size = transition.animateDp(label = "size") { state ->
        when (state) {
            BoxState.Collapsed -> 64.dp
            BoxState.Expanded -> 128.dp
        }
    }
    return remember(transition) { TransitionData(color, size) }
}

Создайте бесконечно повторяющуюся анимацию с помощью rememberInfiniteTransition

InfiniteTransition содержит одну или несколько дочерних анимаций, таких как Transition , но анимации начинают работать, как только они попадают в композицию, и не останавливаются, пока их не удалят. Вы можете создать экземпляр InfiniteTransition с помощью rememberInfiniteTransition . Дочерние анимации можно добавлять с помощью animateColor , animatedFloat или animatedValue . Вам также нужно указать infiniteRepeatable для указания спецификаций анимации.

val infiniteTransition = rememberInfiniteTransition(label = "infinite")
val color by infiniteTransition.animateColor(
    initialValue = Color.Red,
    targetValue = Color.Green,
    animationSpec = infiniteRepeatable(
        animation = tween(1000, easing = LinearEasing),
        repeatMode = RepeatMode.Reverse
    ),
    label = "color"
)

Box(
    Modifier
        .fillMaxSize()
        .background(color)
)

API анимации низкого уровня

Все API анимации высокого уровня, упомянутые в предыдущем разделе, построены на основе API анимации низкого уровня.

Функции animate*AsState — это простейшие API, которые визуализируют мгновенное изменение значения как значение анимации. Они поддерживаются Animatable , который является API на основе сопрограмм для анимации одного значения. updateTransition создает объект перехода, который может управлять несколькими анимационными значениями и запускать их на основе изменения состояния. rememberInfiniteTransition похож, но он создает бесконечный переход, который может управлять несколькими анимациями, которые продолжают выполняться бесконечно. Все эти API являются компонуемыми, за исключением Animatable , что означает, что эти анимации могут быть созданы вне композиции.

Все эти API основаны на более фундаментальном API Animation . Хотя большинство приложений не будут напрямую взаимодействовать с Animation , некоторые возможности настройки Animation доступны через API более высокого уровня. См. раздел Настройка анимаций для получения дополнительной информации о AnimationVector и AnimationSpec .

Диаграмма, показывающая взаимосвязь между различными низкоуровневыми API анимации

Animatable : анимация отдельных значений на основе сопрограмм

Animatable — это держатель значения, который может анимировать значение по мере его изменения с помощью animateTo . Это API, поддерживающее реализацию animate*AsState . Он обеспечивает последовательное продолжение и взаимоисключаемость, что означает, что изменение значения всегда непрерывно, а любая текущая анимация будет отменена.

Многие функции Animatable , включая animateTo , предоставляются как функции приостановки. Это означает, что их необходимо обернуть в соответствующую область действия сопрограммы. Например, можно использовать LaunchedEffect composable для создания области действия только на время действия указанного значения ключа.

// Start out gray and animate to green/red based on `ok`
val color = remember { Animatable(Color.Gray) }
LaunchedEffect(ok) {
    color.animateTo(if (ok) Color.Green else Color.Red)
}
Box(
    Modifier
        .fillMaxSize()
        .background(color.value)
)

В примере выше мы создаем и запоминаем экземпляр Animatable с начальным значением Color.Gray . В зависимости от значения логического флага ok цвет анимируется либо в Color.Green , либо Color.Red . Любое последующее изменение логического значения запускает анимацию для другого цвета. Если при изменении значения идет текущая анимация, анимация отменяется, и новая анимация начинается с текущего значения снимка с текущей скоростью.

Это реализация анимации, которая поддерживает API animate*AsState , упомянутый в предыдущем разделе. По сравнению с animate*AsState , использование Animatable напрямую дает нам более детальный контроль в нескольких отношениях. Во-первых, Animatable может иметь начальное значение, отличное от его первого целевого значения. Например, приведенный выше пример кода сначала показывает серый ящик, который немедленно начинает анимироваться либо зеленым, либо красным цветом. Во-вторых, Animatable предоставляет больше операций над значением содержимого, а именно snapTo и animateDecay . snapTo немедленно устанавливает текущее значение равным целевому значению. Это полезно, когда сама анимация не является единственным источником истины и должна быть синхронизирована с другими состояниями, такими как события касания. animateDecay запускает анимацию, которая замедляется с заданной скорости. Это полезно для реализации поведения броска. Подробнее см. в разделе Жесты и анимация .

Из коробки Animatable поддерживает Float и Color , но можно использовать любой тип данных, предоставив TwoWayConverter . Для получения дополнительной информации см. AnimationVector .

Вы можете настроить характеристики анимации, указав AnimationSpec . Для получения дополнительной информации см. AnimationSpec .

Animation : Анимация, управляемая вручную

Animation — это API анимации самого низкого уровня из доступных. Многие из анимаций, которые мы видели до сих пор, построены на основе Animation. Существует два подтипа Animation : TargetBasedAnimation и DecayAnimation .

Animation должна использоваться только для ручного управления временем анимации. Animation не имеет состояния и не имеет понятия жизненного цикла. Она служит в качестве движка расчета анимации, который используют API более высокого уровня.

TargetBasedAnimation

Другие API охватывают большинство вариантов использования, но использование TargetBasedAnimation напрямую позволяет вам контролировать время воспроизведения анимации самостоятельно. В примере ниже время воспроизведения TargetAnimation контролируется вручную на основе времени кадра, предоставленного withFrameNanos .

val anim = remember {
    TargetBasedAnimation(
        animationSpec = tween(200),
        typeConverter = Float.VectorConverter,
        initialValue = 200f,
        targetValue = 1000f
    )
}
var playTime by remember { mutableLongStateOf(0L) }

LaunchedEffect(anim) {
    val startTime = withFrameNanos { it }

    do {
        playTime = withFrameNanos { it } - startTime
        val animationValue = anim.getValueFromNanos(playTime)
    } while (someCustomCondition())
}

DecayAnimation

В отличие от TargetBasedAnimation , DecayAnimation не требует предоставления targetValue . Вместо этого он вычисляет targetValue на основе начальных условий, установленных initialVelocity и initialValue и предоставленного DecayAnimationSpec .

Анимации затухания часто используются после жеста рывка, чтобы замедлить элементы до полной остановки. Скорость анимации начинается со значения, установленного initialVelocityVector , и замедляется с течением времени.

{% дословно %} {% endverbatim %} {% дословно %} {% endverbatim %}