Анимация на основе ценности

На этой странице рассказывается, как создавать анимацию на основе значений в Jetpack Compose, используя API, которые анимируют значения на основе их текущего и целевого состояний.

Анимируйте одно значение с помощью 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), а в качестве его начального значения будет использовано первое целевое значение. После этого при каждом изменении целевого значения в этой composable-функции будет автоматически запускаться анимация. Если анимация уже выполняется, она начинается с текущего значения (и скорости) и продолжается до целевого значения. Во время анимации эта composable-функция перекомпоновывается и возвращает обновленное значение анимации для каждого кадра.

По умолчанию 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 и при необходимости запускает анимацию появления, исчезновения и sizeTransform, когда значение 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")
            }
        }
    }
}

Как создать переход и сделать его многоразовым

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

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

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, animateFloat или animateValue. Вам также нужно указать 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 основаны на более фундаментальном Animation API. Большинство приложений не взаимодействуют с Animation напрямую, но некоторые возможности настройки доступны через API более высокого уровня. Подробнее о том, как настроить анимацию…AnimationVectorAnimationSpec

Взаимосвязь между API анимации низкого уровня
Рисунок 1. Взаимосвязь между API анимации низкого уровня.

Animatable: анимация одного значения на основе сопрограмм

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

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

// 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. При любом последующем изменении логического значения запускается анимация перехода к другому цвету. Если во время изменения значения выполняется анимация, Compose отменяет ее и начинает новую анимацию с текущего моментального снимка и текущей скоростью.

Этот API Animatable является базовой реализацией для 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, и со временем замедляется.