Compose имеет множество встроенных механизмов анимации, и может быть сложно понять, какой из них выбрать. Ниже приведен список распространенных случаев использования анимации. Более подробную информацию о полном наборе различных доступных вам опций API можно найти в полной документации Compose Animation .
Анимация общих составных свойств
Compose предоставляет удобные API, которые позволяют решать многие распространенные случаи использования анимации. В этом разделе показано, как можно анимировать общие свойства составного объекта.
Анимировать появление/исчезновение
![Зеленый составной элемент, показывающий и прячущийся](https://developer.android.com/static/develop/ui/compose/images/animations/animated_visibility_column.gif?authuser=2&hl=ru)
Используйте AnimatedVisibility
, чтобы скрыть или отобразить составной объект. Дети внутри AnimatedVisibility
могут использовать Modifier.animateEnterExit()
для собственного перехода входа или выхода.
var visible by remember { mutableStateOf(true) } // Animated visibility will eventually remove the item from the composition once the animation has finished. AnimatedVisibility(visible) { // your composable here // ... }
Параметры входа и выхода AnimatedVisibility
позволяют настроить поведение составного объекта при его появлении и исчезновении. Прочтите полную документацию для получения дополнительной информации.
Другой вариант анимации видимости составного объекта — анимировать альфу с течением времени с помощью animateFloatAsState
:
var visible by remember { mutableStateOf(true) } val animatedAlpha by animateFloatAsState( targetValue = if (visible) 1.0f else 0f, label = "alpha" ) Box( modifier = Modifier .size(200.dp) .graphicsLayer { alpha = animatedAlpha } .clip(RoundedCornerShape(8.dp)) .background(colorGreen) .align(Alignment.TopCenter) ) { }
Однако изменение альфа-канала связано с оговоркой, что составной объект остается в композиции и продолжает занимать пространство, в котором он расположен. Это может привести к тому, что программы чтения с экрана и другие механизмы доступности по-прежнему будут учитывать элемент на экране. С другой стороны, AnimatedVisibility
в конечном итоге удаляет элемент из композиции.
![Анимация альфа компонуемого объекта](https://developer.android.com/static/develop/ui/compose/images/animations/animated_visibility_alpha.gif?authuser=2&hl=ru)
Анимировать цвет фона
![Можно компоновать с изменением цвета фона со временем в виде анимации, где цвета плавно переходят друг в друга.](https://developer.android.com/static/develop/ui/compose/images/animations/animated_forever.gif?authuser=2&hl=ru)
val animatedColor by animateColorAsState( if (animateBackgroundColor) colorGreen else colorBlue, label = "color" ) Column( modifier = Modifier.drawBehind { drawRect(animatedColor) } ) { // your composable here }
Этот вариант более эффективен, чем использование Modifier.background()
. Modifier.background()
приемлем для однократной настройки цвета, но при анимации цвета с течением времени это может привести к большему количеству рекомпозиций, чем необходимо.
Сведения о бесконечной анимации цвета фона см. в разделе «Повторение анимации» .
Анимация размера составного объекта
![Зеленый составной элемент, плавно анимирующий изменение его размера.](https://developer.android.com/static/develop/ui/compose/images/animations/animated_content_size.gif?authuser=2&hl=ru)
Compose позволяет анимировать размер составных элементов несколькими различными способами. Используйте animateContentSize()
для анимации между изменениями компонуемого размера.
Например, если у вас есть поле, содержащее текст, который может расширяться от одной до нескольких строк, вы можете использовать Modifier.animateContentSize()
для достижения более плавного перехода:
var expanded by remember { mutableStateOf(false) } Box( modifier = Modifier .background(colorBlue) .animateContentSize() .height(if (expanded) 400.dp else 200.dp) .fillMaxWidth() .clickable( interactionSource = remember { MutableInteractionSource() }, indication = null ) { expanded = !expanded } ) { }
Вы также можете использовать AnimatedContent
с SizeTransform
, чтобы описать, как должны происходить изменения размера.
Анимировать положение составных элементов
![Зеленый составной элемент плавно анимируется вниз и вправо.](https://developer.android.com/static/develop/ui/compose/images/animations/animated_offset.gif?authuser=2&hl=ru)
Чтобы анимировать положение составного объекта, используйте Modifier.offset{ }
в сочетании с animateIntOffsetAsState()
.
var moved by remember { mutableStateOf(false) } val pxToMove = with(LocalDensity.current) { 100.dp.toPx().roundToInt() } val offset by animateIntOffsetAsState( targetValue = if (moved) { IntOffset(pxToMove, pxToMove) } else { IntOffset.Zero }, label = "offset" ) Box( modifier = Modifier .offset { offset } .background(colorBlue) .size(100.dp) .clickable( interactionSource = remember { MutableInteractionSource() }, indication = null ) { moved = !moved } )
Если вы хотите гарантировать, что составные объекты не будут отображаться поверх или под другими составными объектами при анимации положения или размера, используйте Modifier.layout{ }
. Этот модификатор передает изменения размера и положения родительскому элементу, что затем влияет на других дочерних элементов.
Например, если вы перемещаете Box
внутри Column
, а другим дочерним элементам необходимо перемещаться при перемещении Box
, включите информацию о смещении с помощью Modifier.layout{ }
следующим образом:
var toggled by remember { mutableStateOf(false) } val interactionSource = remember { MutableInteractionSource() } Column( modifier = Modifier .padding(16.dp) .fillMaxSize() .clickable(indication = null, interactionSource = interactionSource) { toggled = !toggled } ) { val offsetTarget = if (toggled) { IntOffset(150, 150) } else { IntOffset.Zero } val offset = animateIntOffsetAsState( targetValue = offsetTarget, label = "offset" ) Box( modifier = Modifier .size(100.dp) .background(colorBlue) ) Box( modifier = Modifier .layout { measurable, constraints -> val offsetValue = if (isLookingAhead) offsetTarget else offset.value val placeable = measurable.measure(constraints) layout(placeable.width + offsetValue.x, placeable.height + offsetValue.y) { placeable.placeRelative(offsetValue) } } .size(100.dp) .background(colorGreen) ) Box( modifier = Modifier .size(100.dp) .background(colorBlue) ) }
![2 блока, второй блок анимирует свое положение X, Y, третий блок реагирует, также перемещаясь на величину Y.](https://developer.android.com/static/develop/ui/compose/images/animations/animated_layout_modifier.gif?authuser=2&hl=ru)
Modifier.layout{ }
Анимация заполнения составного элемента
![Зеленый составной элемент становится меньше и больше при нажатии, а отступы анимируются.](https://developer.android.com/static/develop/ui/compose/images/animations/animated_padding.gif?authuser=2&hl=ru)
Чтобы анимировать заполнение составного объекта, используйте animateDpAsState
в сочетании с Modifier.padding()
:
var toggled by remember { mutableStateOf(false) } val animatedPadding by animateDpAsState( if (toggled) { 0.dp } else { 20.dp }, label = "padding" ) Box( modifier = Modifier .aspectRatio(1f) .fillMaxSize() .padding(animatedPadding) .background(Color(0xff53D9A1)) .clickable( interactionSource = remember { MutableInteractionSource() }, indication = null ) { toggled = !toggled } )
Анимация возвышения составного объекта
Чтобы анимировать подъем составного объекта, используйте animateDpAsState
в сочетании с Modifier.graphicsLayer{ }
. Для однократных изменений высоты используйте Modifier.shadow()
. Если вы анимируете тень, использование модификатора Modifier.graphicsLayer{ }
является более эффективным вариантом.
val mutableInteractionSource = remember { MutableInteractionSource() } val pressed = mutableInteractionSource.collectIsPressedAsState() val elevation = animateDpAsState( targetValue = if (pressed.value) { 32.dp } else { 8.dp }, label = "elevation" ) Box( modifier = Modifier .size(100.dp) .align(Alignment.Center) .graphicsLayer { this.shadowElevation = elevation.value.toPx() } .clickable(interactionSource = mutableInteractionSource, indication = null) { } .background(colorGreen) ) { }
Альтернативно можно использовать составную Card
и установить для свойства возвышения разные значения для каждого состояния.
Анимация масштабирования, перевода или вращения текста
![Текстовое высказывание](https://developer.android.com/static/develop/ui/compose/images/animations/animated_text.gif?authuser=2&hl=ru)
При анимации масштабирования, перевода или поворота текста установите для параметра textMotion
в TextStyle
значение TextMotion.Animated
. Это обеспечивает более плавные переходы между текстовыми анимациями. Используйте Modifier.graphicsLayer{ }
для перевода, поворота или масштабирования текста.
val infiniteTransition = rememberInfiniteTransition(label = "infinite transition") val scale by infiniteTransition.animateFloat( initialValue = 1f, targetValue = 8f, animationSpec = infiniteRepeatable(tween(1000), RepeatMode.Reverse), label = "scale" ) Box(modifier = Modifier.fillMaxSize()) { Text( text = "Hello", modifier = Modifier .graphicsLayer { scaleX = scale scaleY = scale transformOrigin = TransformOrigin.Center } .align(Alignment.Center), // Text composable does not take TextMotion as a parameter. // Provide it via style argument but make sure that we are copying from current theme style = LocalTextStyle.current.copy(textMotion = TextMotion.Animated) ) }
Анимировать цвет текста
![Слова](https://developer.android.com/static/develop/ui/compose/images/animations/animated_text_color.gif?authuser=2&hl=ru)
Чтобы анимировать цвет текста, используйте лямбда color
в составном элементе BasicText
:
val infiniteTransition = rememberInfiniteTransition(label = "infinite transition") val animatedColor by infiniteTransition.animateColor( initialValue = Color(0xFF60DDAD), targetValue = Color(0xFF4285F4), animationSpec = infiniteRepeatable(tween(1000), RepeatMode.Reverse), label = "color" ) BasicText( text = "Hello Compose", color = { animatedColor }, // ... )
Переключайтесь между различными типами контента
![Зеленый экран говорит](https://developer.android.com/static/develop/ui/compose/images/animations/animated_content_slower.gif?authuser=2&hl=ru)
Используйте AnimatedContent
для анимации между различными составными объектами. Если вам просто нужно стандартное затухание между составными объектами, используйте Crossfade
.
var state by remember { mutableStateOf(UiState.Loading) } AnimatedContent( state, transitionSpec = { fadeIn( animationSpec = tween(3000) ) togetherWith fadeOut(animationSpec = tween(3000)) }, modifier = Modifier.clickable( interactionSource = remember { MutableInteractionSource() }, indication = null ) { state = when (state) { UiState.Loading -> UiState.Loaded UiState.Loaded -> UiState.Error UiState.Error -> UiState.Loading } }, label = "Animated Content" ) { targetState -> when (targetState) { UiState.Loading -> { LoadingScreen() } UiState.Loaded -> { LoadedScreen() } UiState.Error -> { ErrorScreen() } } }
AnimatedContent
можно настроить для отображения множества различных типов переходов входа и выхода. Для получения дополнительной информации прочтите документацию по AnimatedContent
или прочитайте эту запись в блоге по AnimatedContent
.
Анимация во время навигации к разным пунктам назначения
![Два составных объекта, один зеленый с надписью «Приземление» и один синий с надписью «Детали», анимация осуществляется путем сдвига составного элемента детали над составным элементом приземления.](https://developer.android.com/static/develop/ui/compose/images/animations/navigation_compose_animation.gif?authuser=2&hl=ru)
Чтобы анимировать переходы между составными объектами при использовании артефакта навигации-компонования , укажите enterTransition
и exitTransition
для составного объекта. Вы также можете установить анимацию по умолчанию, которая будет использоваться для всех пунктов назначения на верхнем уровне NavHost
:
val navController = rememberNavController() NavHost( navController = navController, startDestination = "landing", enterTransition = { EnterTransition.None }, exitTransition = { ExitTransition.None } ) { composable("landing") { ScreenLanding( // ... ) } composable( "detail/{photoUrl}", arguments = listOf(navArgument("photoUrl") { type = NavType.StringType }), enterTransition = { fadeIn( animationSpec = tween( 300, easing = LinearEasing ) ) + slideIntoContainer( animationSpec = tween(300, easing = EaseIn), towards = AnimatedContentTransitionScope.SlideDirection.Start ) }, exitTransition = { fadeOut( animationSpec = tween( 300, easing = LinearEasing ) ) + slideOutOfContainer( animationSpec = tween(300, easing = EaseOut), towards = AnimatedContentTransitionScope.SlideDirection.End ) } ) { backStackEntry -> ScreenDetails( // ... ) } }
Существует множество различных типов входных и выходных переходов, которые применяют различные эффекты к входящему и исходящему содержимому. Дополнительную информацию см. в документации .
Повтор анимации
![Зеленый фон, который бесконечно трансформируется в синий за счет анимации между двумя цветами.](https://developer.android.com/static/develop/ui/compose/images/animations/animated_forever.gif?authuser=2&hl=ru)
Используйте rememberInfiniteTransition
с animationSpec
infiniteRepeatable
, чтобы постоянно повторять анимацию. Измените RepeatModes
, чтобы указать, как он должен двигаться вперед и назад.
Используйте finiteRepeatable
для повторения заданного количества раз.
val infiniteTransition = rememberInfiniteTransition(label = "infinite") val color by infiniteTransition.animateColor( initialValue = Color.Green, targetValue = Color.Blue, animationSpec = infiniteRepeatable( animation = tween(1000, easing = LinearEasing), repeatMode = RepeatMode.Reverse ), label = "color" ) Column( modifier = Modifier.drawBehind { drawRect(color) } ) { // your composable here }
Запустить анимацию при запуске составного элемента
LaunchedEffect
запускается, когда компонуемый объект входит в композицию. Он запускает анимацию при запуске составного объекта, вы можете использовать это для изменения состояния анимации. Использование Animatable
с методом animateTo
для запуска анимации при запуске:
val alphaAnimation = remember { Animatable(0f) } LaunchedEffect(Unit) { alphaAnimation.animateTo(1f) } Box( modifier = Modifier.graphicsLayer { alpha = alphaAnimation.value } )
Создание последовательных анимаций
![Четыре круга с зелеными стрелками, которые анимируются один за другим.](https://developer.android.com/static/develop/ui/compose/images/animations/multiple_properties_sequential.gif?authuser=2&hl=ru)
Используйте API-интерфейсы сопрограмм Animatable
для выполнения последовательной или параллельной анимации. Вызов animateTo
в Animatable
один за другим приводит к тому, что каждая анимация ожидает завершения предыдущей анимации, прежде чем продолжить. Это потому, что это функция приостановки.
val alphaAnimation = remember { Animatable(0f) } val yAnimation = remember { Animatable(0f) } LaunchedEffect("animationKey") { alphaAnimation.animateTo(1f) yAnimation.animateTo(100f) yAnimation.animateTo(500f, animationSpec = tween(100)) }
Создание параллельных анимаций
![Три круга с зелеными стрелками, анимирующими каждый из них, анимирующими все вместе одновременно.](https://developer.android.com/static/develop/ui/compose/images/animations/multiple_properties.gif?authuser=2&hl=ru)
Используйте API-интерфейсы сопрограмм ( Animatable#animateTo()
или animate
) или API- Transition
для достижения одновременной анимации. Если вы используете несколько функций запуска в контексте сопрограммы, анимация запускается одновременно:
val alphaAnimation = remember { Animatable(0f) } val yAnimation = remember { Animatable(0f) } LaunchedEffect("animationKey") { launch { alphaAnimation.animateTo(1f) } launch { yAnimation.animateTo(100f) } }
Вы можете использовать API updateTransition
, чтобы использовать одно и то же состояние для одновременного управления множеством различных анимаций свойств. В приведенном ниже примере анимируются два свойства, управляемые изменением состояния: rect
и borderWidth
:
var currentState by remember { mutableStateOf(BoxState.Collapsed) } val transition = updateTransition(currentState, label = "transition") val rect by transition.animateRect(label = "rect") { state -> when (state) { BoxState.Collapsed -> Rect(0f, 0f, 100f, 100f) BoxState.Expanded -> Rect(100f, 100f, 300f, 300f) } } val borderWidth by transition.animateDp(label = "borderWidth") { state -> when (state) { BoxState.Collapsed -> 1.dp BoxState.Expanded -> 0.dp } }
Оптимизация производительности анимации
Анимации в Compose могут вызвать проблемы с производительностью. Это связано с природой анимации: быстрое перемещение или изменение пикселей на экране, покадрово, чтобы создать иллюзию движения.
Рассмотрим различные этапы Compose : композицию, макет и рисование. Если ваша анимация меняет фазу макета, она требует, чтобы все затронутые составные элементы были перерисованы и перерисованы. Если ваша анимация происходит на этапе рисования, она по умолчанию будет более производительной, чем если бы вы запускали анимацию на этапе макета, поскольку в целом ей придется выполнить меньше работы.
Чтобы ваше приложение выполняло как можно меньше действий при анимации, по возможности выбирайте лямбда-версию Modifier
. Это пропускает рекомпозицию и выполняет анимацию вне фазы композиции, в противном случае используйте Modifier.graphicsLayer{ }
, поскольку этот модификатор всегда выполняется на этапе рисования. Дополнительные сведения об этом см. в разделе «Отложенное чтение» документации по производительности.
Изменить время анимации
Compose по умолчанию использует пружинную анимацию для большинства анимаций. Пружины или анимация, основанная на физике, кажутся более естественными. Их также можно прерывать, поскольку они учитывают текущую скорость объекта, а не фиксированное время. Если вы хотите переопределить значение по умолчанию, все API-интерфейсы анимации, показанные выше, имеют возможность установить animationSpec
, чтобы настроить способ запуска анимации, хотите ли вы, чтобы она выполнялась в течение определенной продолжительности или была более динамичной.
Ниже приводится сводка различных параметров animationSpec
:
-
spring
: анимация, основанная на физике, используется по умолчанию для всех анимаций. Вы можете изменить жесткость или DampingRatio, чтобы добиться другого внешнего вида анимации. -
tween
(сокращение от Between ): анимация на основе продолжительности, анимация между двумя значениями с помощью функцииEasing
. -
keyframes
: спецификация для указания значений в определенных ключевых точках анимации. -
repeatable
: спецификация на основе продолжительности, которая запускается определенное количество раз, указанное вRepeatMode
. -
infiniteRepeatable
: спецификация на основе продолжительности, которая работает вечно. -
snap
: мгновенно привязывается к конечному значению без какой-либо анимации.
![Напишите здесь свой альтернативный текст](https://developer.android.com/static/develop/ui/compose/images/animations/animated_spec_set.gif?authuser=2&hl=ru)
Прочтите полную документацию для получения дополнительной информации об анимационных спецификациях .
Дополнительные ресурсы
Дополнительные примеры забавной анимации в Compose можно найти здесь:
- 5 быстрых анимаций в Compose
- Заставить медузу двигаться в Compose
- Настройка
AnimatedContent
в Compose - Функции замедления в Compose