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

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

Анимация общих составных свойств

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

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

Зеленый составной элемент, показывающий и прячущийся
Рисунок 1. Анимация появления и исчезновения элемента в столбце

Используйте 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 в конечном итоге удаляет элемент из композиции.

Анимация альфа компонуемого объекта
Рисунок 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. 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
        }
)

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

Рисунок 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 .

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

Два составных объекта, один зеленый с надписью «Приземление» и один синий с надписью «Детали», анимация осуществляется путем сдвига составного элемента детали над составным элементом приземления.
Рис. 12. Анимация между составными объектами с помощью 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 с 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
    }
)

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

Четыре круга с зелеными стрелками, которые анимируются один за другим.
Рисунок 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 : анимация, основанная на физике, используется по умолчанию для всех анимаций. Вы можете изменить жесткость или DampingRatio, чтобы добиться другого внешнего вида анимации.
  • tween (сокращение от Between ): анимация на основе продолжительности, анимация между двумя значениями с помощью функции Easing .
  • keyframes : спецификация для указания значений в определенных ключевых точках анимации.
  • repeatable : спецификация на основе продолжительности, которая запускается определенное количество раз, указанное в RepeatMode .
  • infiniteRepeatable : спецификация на основе продолжительности, которая работает вечно.
  • snap : мгновенно привязывается к конечному значению без какой-либо анимации.
Напишите здесь свой альтернативный текст
Рисунок 16. Набор спецификаций без набора спецификаций и набор спецификаций Custom Spring

Прочтите полную документацию для получения дополнительной информации об анимационных спецификациях .

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

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