В Compose есть много встроенных механизмов анимации, и может быть сложно понять, какой из них выбрать. Ниже приведен список распространенных вариантов использования анимации. Подробную информацию обо всех доступных вариантах API можно найти в документации по Compose Animation.
Анимируйте общие свойства компонуемых объектов.
Compose предоставляет удобные API, которые позволяют решать многие распространенные задачи, связанные с анимацией. В этом разделе показано, как анимировать общие свойства composable-функции.
Анимация появления / исчезновения
Используйте AnimatedVisibility, чтобы скрыть или показать composable-функцию. Дочерние элементы внутри 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 // ... }
Параметры enter и exit в AnimatedVisibility позволяют настроить поведение composable-функции при появлении и исчезновении. Подробную информацию вы найдете в полной документации.
Ещё один способ анимировать видимость composable-функции – анимировать альфа-канал с течением времени с помощью 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) ) { }
Однако при изменении альфа-канала composable-функция остается в композиции и продолжает занимать место, в котором она размещена. Из-за этого программы чтения с экрана и другие специальные возможности могут по-прежнему считать, что элемент находится на экране. С другой стороны, AnimatedVisibility в конечном итоге удаляет элемент из композиции.
Анимировать цвет фона
val animatedColor by animateColorAsState( if (animateBackgroundColor) colorGreen else colorBlue, label = "color" ) Column( modifier = Modifier.drawBehind { drawRect(animatedColor) } ) { // your composable here }
Этот вариант эффективнее, чем использование Modifier.background().
Modifier.background() is acceptable for a one-shot color setting, but when
animating a color over time, this could cause more recompositions than
necessary.
Чтобы цвет фона менялся бесконечно, ознакомьтесь с разделом Повтор анимации.
Как анимировать размер composable-функции
В Compose можно анимировать размер составных элементов несколькими способами. Используйте animateContentSize() для анимации между изменениями размера composable-функции.
Например, если у вас есть поле с текстом, который может занимать от одной до нескольких строк, вы можете использовать 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, чтобы описать, как должны происходить изменения размера.
Как анимировать положение composable-функции
Чтобы анимировать положение composable-функции, используйте 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 } )
Если вы хотите, чтобы composable-функции не перекрывали друг друга при анимации положения или размера, используйте 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) ) }
Modifier.layout{ }Как анимировать отступы composable-функции
Чтобы анимировать отступы composable-функции, используйте 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 } )
Как анимировать высоту composable-функции
Чтобы анимировать высоту composable-функции, используйте 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 и задать для свойства elevation разные значения для каждого состояния.
Как анимировать масштаб, преобразование или вращение текста
При анимации масштаба, перемещения или поворота текста задайте для параметра 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) ) }
Анимировать цвет текста
Чтобы анимировать цвет текста, используйте лямбда-выражение color в composable-функции 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 }, // ... )
Переключайтесь между разными типами контента
Используйте AnimatedContent для анимации между разными composable-функциями. Если вам нужно стандартное затухание между ними, используйте 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.
Анимация при переходе к разным пунктам назначения
Чтобы анимировать переходы между composable-функциями при использовании артефакта navigation-compose, укажите enterTransition и exitTransition в composable-функции. Вы также можете задать анимацию по умолчанию для всех целевых страниц на верхнем уровне 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( // ... ) } }
Существует множество различных типов переходов входа и выхода, которые применяют различные эффекты к входящему и исходящему контенту. Дополнительную информацию см. в документации .
Как повторить анимацию
Используйте rememberInfiniteTransition с infiniteRepeatable
animationSpec, чтобы анимация повторялась непрерывно. Измените RepeatModes, чтобы указать, как он должен двигаться туда и обратно.
Используйте repeatable, чтобы повторить действие заданное количество раз.
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 }
Как запустить анимацию при запуске composable-функции
LaunchedEffect запускается, когда composable-функция входит в композицию. Она запускает анимацию при запуске composable-функции. Вы можете использовать ее для управления изменением состояния анимации. Использование Animatable с методом animateTo для запуска анимации при запуске:
val alphaAnimation = remember { Animatable(0f) } LaunchedEffect(Unit) { alphaAnimation.animateTo(1f) } Box( modifier = Modifier.graphicsLayer { alpha = alphaAnimation.value } )
Как создать последовательную анимацию
Используйте 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)) }
Как создать параллельные анимации
Чтобы запустить параллельные анимации, используйте 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: композицию, макет и отрисовку. Если анимация изменяет фазу макета, все затронутые composable-функции должны быть перекомпонованы и перерисованы. Если анимация происходит на этапе рисования, она по умолчанию более производительна, чем если бы вы запускали анимацию на этапе макета, поскольку в целом ей придется выполнять меньше работы.
Чтобы приложение выполняло как можно меньше действий во время анимации, по возможности выбирайте лямбда-версию Modifier. Это позволяет пропустить рекомпозицию и выполнить анимацию вне фазы композиции. В противном случае используйте Modifier.graphicsLayer{ }, так как этот модификатор всегда выполняется на этапе отрисовки. Подробную информацию об этом можно найти в разделе Отложенное чтение документации по производительности.
Как изменить продолжительность анимации
По умолчанию в Compose для большинства анимаций используется пружинная анимация. Пружины, или анимации, основанные на физике, кажутся более естественными. Их также можно прервать, поскольку они учитывают текущую скорость объекта, а не фиксированное время.
Если вы хотите переопределить настройки по умолчанию, все продемонстрированные ранее API анимации позволяют установить animationSpec, чтобы настроить запуск анимации, независимо от того, хотите ли вы, чтобы она выполнялась в течение определенного времени или была более упругой.
Ниже приведены краткие описания различных вариантов animationSpec.
spring– анимация на основе физики, которая используется по умолчанию. Вы можете изменить жесткость или коэффициент демпфирования, чтобы добиться другого вида и ощущения анимации.tween(сокращение от between) – анимация на основе продолжительности, которая анимирует переход между двумя значениями с помощью функцииEasing.keyframes– спецификация для указания значений в определенных ключевых точках анимации.repeatable– спецификация на основе продолжительности, которая выполняется определенное количество раз, указанное вRepeatMode.infiniteRepeatable– спецификация на основе продолжительности, которая выполняется бесконечно.snap– мгновенный переход к конечному значению без анимации.
Подробную информацию об animationSpecs вы найдете в полной документации.
Дополнительные ресурсы
Дополнительные примеры забавных анимаций в Compose можно найти в следующих статьях:
- 5 быстрых анимаций в Compose
- Как заставить медузу двигаться в Compose
- Настройка
AnimatedContentв Compose - Функции плавности в Compose