На этой странице рассказывается, как создавать анимацию на основе значений в 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
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, и со временем замедляется.
Рекомендуем
- Примечание. Текст ссылки показывается, когда JavaScript отключен.
- Как настроить анимацию
- Анимация в Compose
- Модификаторы анимации и функции, поддерживающие композицию