На этой странице описывается, как создавать анимацию на основе значений в 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") // ……
Для более сложных переходов, включающих несколько composable-функций, можно использовать 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, являются composable-функциями, то есть вы можете создавать анимации вне композиции.
Все эти API основаны на более фундаментальном API Animation. Хотя большинство приложений не взаимодействуют с Animation напрямую, вы можете получить доступ к некоторым возможностям настройки через API более высокого уровня. Подробнее о том, как настроить анимацию, используя AnimationVector и AnimationSpec…
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 – это 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, и со временем снижается.
Рекомендуем
- Примечание. Текст ссылки показывается, когда JavaScript отключен.
- Как настроить анимацию
- Анимации в Compose
- Модификаторы анимации и composable-функции