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

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

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

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

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

Зеленый композитный объект, показывающий и скрывающий себя
Рисунок 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
    // ...
}

Параметры входа и выхода 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 в конечном итоге удаляет элемент из композиции.

Анимация альфа-канала составного объекта
Рисунок 2. Анимация альфа-канала составного объекта

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

Можно создавать композиции с фоновым цветом, который со временем меняется в виде анимации, где цвета плавно переходят друг в друга.
Рисунок 3. Анимация цвета фона компонуемого объекта

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

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

Информацию о бесконечной анимации цвета фона см. в разделе «Повторение анимации» .

Анимировать размер компонуемого объекта

Зеленый компонуемый анимационный объект, плавно изменяющий свой размер.
Рисунок 4. Композиция плавно анимируется между маленьким и большим размером.

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 для описания того, как должны происходить изменения размера.

Анимировать позицию компонуемого объекта

Зеленый, плавно анимируемый, движется вниз и вправо.
Рисунок 5. Композиционное перемещение со смещением

Чтобы анимировать положение составного объекта, используйте 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.
Рисунок 6. Анимация с помощью Modifier.layout{ }

Анимированное заполнение компонуемого объекта

Зеленый компонуемый элемент становится меньше и больше при нажатии, с анимированными отступами
Рисунок 7. Композиция с анимированным отступом

Чтобы анимировать заполнение компонуемого объекта, используйте 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
        }
)

Анимированное возвышение составного объекта

Рисунок 8. Анимация изменения высоты 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 и задать для свойства высоты различные значения для каждого штата.

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

Текстовое высказывание, которое можно составить
Рисунок 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)
    )
}

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

Слова
Рисунок 10. Пример, демонстрирующий анимацию цвета текста

Чтобы анимировать цвет текста, используйте лямбда-функцию 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
    },
    // ...
)

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

Зеленый экран с надписью
Рисунок 11. Использование AnimatedContent для анимации изменений между различными компонуемыми элементами (замедленно)

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

Анимация при навигации к разным пунктам назначения

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

Чтобы анимировать переходы между компонуемыми объектами при использовании артефакта navigation-compose , укажите для компонуемого объекта 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(
            // ...
        )
    }
}

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

Повторить анимацию

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

Используйте rememberInfiniteTransition с infiniteRepeatable animationSpec для непрерывного повторения анимации. Измените 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
    }
)

Создание последовательных анимаций

Четыре круга с зелеными стрелками, анимированными между каждым из них, один за другим.
Рисунок 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 : композицию, макет и отрисовку. Если ваша анимация изменяет фазу макета, это требует перерисовки и перерисовки всех затронутых компоновочных элементов. Если анимация выполняется на фазе отрисовки, она по умолчанию более производительна, чем при запуске анимации на фазе макета, поскольку в этом случае требуется меньше работы.

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

Изменить время анимации

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

Ниже приведен обзор различных параметров animationSpec :

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

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

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

Больше примеров забавной анимации в Compose можно найти здесь: