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

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

Анимируйте общие свойства компонуемых объектов.

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.TopCenter)
) {
}

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

Анимация альфа-канала composable-функции
Рисунок 2. Анимация альфа-канала composable-функции

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

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

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-функции

Зеленая 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
        }
)

Если вы хотите, чтобы 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)
    )
}

Два поля, второе из которых анимирует свое положение по осям 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
        }
)

Как анимировать высоту 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(colorGreen)
) {
}

Вы также можете использовать компонуемый объект 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 = TextMotion.Animated)
    )
}

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

Анимированная надпись 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 = {
        animatedColor
    },
    // ...
)

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

Зеленый экран с надписью "Загрузка", синий экран с надписью "Загружено" и белый экран с надписью "Ошибка". Разные 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"
) { targetState ->
    when (targetState) {
        UiState.Loading -> {
            LoadingScreen()
        }
        UiState.Loaded -> {
            LoadedScreen()
        }
        UiState.Error -> {
            ErrorScreen()
        }
    }
}

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 ->
        ScreenDetails(
            // ...
        )
    }
}

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

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

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

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

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

Четыре круга с зелеными стрелками, которые по очереди анимируются.
Рисунок 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 = tween(100))
}

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

Три круга с зелеными стрелками, указывающими на каждый из них. Все элементы анимированы.
Рисунок 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(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 – мгновенный переход к конечному значению без анимации.
Две анимации, демонстрирующие отсутствие набора спецификаций и набор пользовательских спецификаций Spring.
Рисунок 16. Без спецификации и со специальной спецификацией Spring

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

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

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