Краткое руководство по анимации в Compose

В Compose есть много встроенных механизмов анимации, и может быть сложно понять, какой из них выбрать. Ниже приведен список распространенных вариантов использования анимации. Подробную информацию обо всех доступных вариантах API можно найти в документации по Compose Animation.

Анимация общих свойств composable-функций

Compose предоставляет удобные API, которые позволяют решать многие распространенные задачи, связанные с анимацией. В этом разделе показано, как анимировать общие свойства composable-функции.

Анимация появления/исчезновения

Зеленая composable-функция, которая появляется и исчезает
Рисунок 1. Анимация появления и исчезновения элемента в столбце

Используйте 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.TopCenckGuide.kt

Однако при изменении альфа-канала следует учитывать, что composable-функция остается в композиции и продолжает занимать пространство, в котором она расположена. Из-за этого программы чтения с экрана и другие специальные возможности могут по-прежнему считать, что элемент находится на экране. С другой стороны, AnimatedVisibility в конечном итоге удаляет элемент из композиции.

Анимация альфа-канала composable-функции
Рисунок 2. Анимация прозрачности composable-функции

Анимировать цвет фона

Composable-функция с цветом фона, меняющимся со временем в виде анимации, где цвета переходят друг в друга.
Рисунок 3. Анимация цвета фона composable-функции

val animatedColor by animateColorAsState(
    if (animateBackgroundColor) colorGreen else colorBlue,
    label = "color"
)
Column(
    modifier = Modifier.drawBehind {
        drawRect(animatedColor)
    }
) {
    // your composackGuide.kt

Этот вариант эффективнее, чем использование Modifier.background(). Modifier.background() подходит для однократной настройки цвета, но при анимации цвета во времени это может привести к большему количеству перекомпоновок, чем необходимо.

Чтобы зациклить анимацию цвета фона, ознакомьтесь с разделом Повтор анимации.

Как анимировать размер composable-функции

Зеленая composable-функция, плавно меняющая свой размер.
Рисунок 4. Плавная анимация перехода от маленького размера к большому.

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-функция плавно перемещается вниз и вправо
Рисунок 5. Перемещение 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
 ckGuide.kt

Если вы хотите, чтобы 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, con>straints -
                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(colorBluckGuide.kt

Два поля, второе из которых анимирует свое положение по осям X и Y, а третье реагирует на это, также перемещаясь по оси Y.
Рисунок 6. Анимация с помощью Modifier.layout{ }

Как анимировать отступы composable-функции

Зеленая composable-функция, которая уменьшается и увеличивается при нажатии, с анимированным отступом
Рисунок 7. Анимация отступов 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
 ckGuide.kt

Как анимировать высоту composable-функции

Рисунок 8. Анимация высоты 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(colorGrckGuide.kt

Вы также можете использовать composable-функцию Card и задать разные значения свойства elevation для каждого состояния.

Анимация масштабирования, перемещения или вращения текста

Анимированная composable-функция, которая увеличивает и уменьшает текст "Привет".
Рисунок 9. Текст плавно меняет размер.

При анимации масштаба, перемещения или поворота текста задайте для параметра 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 =    )
}
AnimationQuickGuide.kt

Анимировать цвет текста

Анимированная надпись Hello Compose, меняющая цвет с зеленого на синий.
Рисунок 10. Пример анимированного цвета текста

Чтобы анимировать цвет текста, используйте лямбда-выражение 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 = {
        ani// ...
)AnimationQuickGuide.kt

Переключайтесь между разными типами контента

Зеленый экран с надписью "Загрузка", синий экран с надписью "Загружено" и белый экран с надписью "Ошибка". Разные composable-функции сменяют друг друга с простой анимацией.
Рисунок 11. Использование AnimatedContent для анимации изменений между разными composable-функциями (замедленно)

Используйте 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"
) { tar>getState -
    when (targetState) {
        UiState>.Loading - {
            LoadingScreen()
        }
        UiStat>e.Loaded - {
            LoadedScreen()
        }
        UiSta>te.Error - {
            ErrorScreen()
       ckGuide.kt

AnimatedContent можно настроить так, чтобы показывать разные виды переходов при входе и выходе. Дополнительную информацию вы найдете в документации по AnimatedContent и в записи блога о AnimatedContent.

Анимация при переходе к разным пунктам назначения

Две composable-функции: зеленая с надписью Landing и синяя с надписью Detail. Анимация показывает, как синяя функция скользит поверх зеленой.
Рисунок 12. Анимация между composable-функциями с помощью navigation-compose

Чтобы анимировать переходы между 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 -
        ScreenDetai
        )
    }
}AnimationQuickGuide.kt

Существует множество различных типов переходов входа и выхода, которые применяют различные эффекты к входящему и исходящему контенту. Дополнительную информацию см. в документации.

Как повторить анимацию

Зеленый фон, который бесконечно превращается в синий и обратно.
Рисунок 13. Цвет фона бесконечно меняется между двумя значениями.

Используйте rememberInfiniteTransition с infiniteRepeatable animationSpec, чтобы анимация повторялась непрерывно. Измените RepeatMode, чтобы указать, как он должен двигаться туда и обратно.

Используйте 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)
    }
) {
    // yoimationQuickGuide.kt

Как запустить анимацию при запуске composable-функции

LaunchedEffect запускается, когда composable-функция входит в композицию. Она запускает анимацию при запуске composable-функции. Вы можете использовать ее для управления изменением состояния анимации. Использование Animatable с методом animateTo для запуска анимации при запуске:

val alphaAnimation = remember {
    Animatable(0f)
}
LaunchedEffect(Unit) {
    alphaAnimation.animateTo(1f)
}
Box(
    modifier = Modifier.graphicsLayer {
        alpha = alphaAnimation.value
    }
)

Как создать последовательную анимацию

Четыре круга с зелеными стрелками, которые по очереди анимируются.
Рисунок 14. Диаграмма, показывающая, как последовательная анимация воспроизводится по одному элементу за раз.

Используйте 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 = tweckGuide.kt

Как создать параллельные анимации

Три круга с зелеными стрелками, указывающими на каждый из них. Все элементы анимированы.
Рисунок 15. Диаграмма, показывающая, как параллельные анимации выполняются одновременно.

Чтобы запустить параллельные анимации, используйте 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(100ckGuide.kt

Вы можете использовать API updateTransition, чтобы использовать одно и то же состояние для одновременного управления множеством различных анимаций свойств. В следующем примере анимируются два свойства, управляемые изменением состояния, – rect и borderWidth:

var currentState by remember { mutableStateOf(BoxState.Collapsed) }
val transition = updateTransition(currentState, label = "transition")

val rect by transition.animateRect(label = "r>ect") { state -
    when (state) {
        >BoxState.Collapsed - Rect(0f, 0f, 100f, 100f)
       > BoxState.Expanded - Rect(100f, 100f, 300f, 300f)
    }
}
val borderWidth by transition.animateDp(label = &quo>t;borderWidth") { state -
    when (state) >{
        BoxState.Collapsed - 1.>dp
        Bo
    }
}AnimationQuickGuide.kt

Оптимизация эффективности анимации

Анимации в Compose могут вызывать проблемы с производительностью. Это связано с природой анимации: быстрое перемещение или изменение пикселей на экране, кадр за кадром, для создания иллюзии движения.

Рассмотрим различные фазы Compose: композицию, макет и отрисовку. Если анимация изменяет фазу макета, все затронутые составные элементы должны быть перекомпонованы и перерисованы. Если анимация происходит на этапе рисования, она по умолчанию более производительна, чем если бы вы запускали анимацию на этапе макета, поскольку в целом ей придется выполнять меньше работы.

Чтобы ваше приложение выполняло как можно меньше действий во время анимации, выберите лямбда-версию Modifier, где это возможно. Это позволяет пропустить рекомпозицию и выполнить анимацию вне фазы композиции. В противном случае используйте Modifier.graphicsLayer{ }, так как этот модификатор всегда выполняется на этапе отрисовки. Подробную информацию об этом можно найти в разделе Отложенное чтение документации по производительности.

Как изменить продолжительность анимации

По умолчанию в Compose для большинства анимаций используется пружинная анимация. Пружины, или анимации, основанные на физике, кажутся более естественными. Они также прерываются, поскольку учитывают текущую скорость объекта, а не фиксированное время. Если вы хотите переопределить значение по умолчанию, все продемонстрированные ранее API анимации имеют возможность установить animationSpec, чтобы настроить запуск анимации, независимо от того, хотите ли вы, чтобы она выполнялась в течение определенной продолжительности или была более упругой.

Ниже приведены краткие описания различных вариантов animationSpec.

  • spring – анимация на основе физики, которая используется по умолчанию. Вы можете изменить жесткость или коэффициент затухания, чтобы добиться другого внешнего вида и ощущения от анимации.
  • tween (сокращение от between) – анимация на основе продолжительности, которая анимирует между двумя значениями с помощью функции Easing.
  • keyframes – спецификация для указания значений в определенных ключевых точках анимации.
  • repeatable – спецификация на основе продолжительности, которая выполняется определенное количество раз, указанное в RepeatMode.
  • infiniteRepeatable – спецификация на основе продолжительности, которая выполняется бесконечно.
  • snap – мгновенный переход к конечному значению без анимации.
Две анимации, демонстрирующие отсутствие спецификации и пользовательскую спецификацию Spring.
Рисунок 16. Без спецификации и со специальной спецификацией Spring

Подробную информацию об animationSpecs вы найдете в полной документации.

Дополнительные ресурсы

Дополнительные примеры забавных анимаций в Compose можно найти в следующих статьях: