На этой странице описывается, как создавать анимации на основе значений в 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, чтобы создать дочерний переход. Этот метод полезен для разделения задач между несколькими подкомпонентами в сложной composable-функции. Родительский переход знает обо всех значениях анимации в дочерних переходах.
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. Эти функции расширения позволяют поднять все анимации входа, выхода и 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-функции