Модификаторы анимации и функции, поддерживающие композицию

В Compose есть встроенные composable-функции и модификаторы для работы с распространенными сценариями анимации.

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

Compose предоставляет несколько компонуемых объектов, которые анимируют появление, исчезновение контента и изменения макета.

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

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

Composable-функция AnimatedVisibility анимирует появление и исчезновение контента.

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
    // ...
}

По умолчанию контент появляется с увеличением и исчезает с уменьшением. Настройте этот переход, указав объекты EnterTransition и ExitTransition.

var visible by remember { mutableStateOf(true) }
val density = LocalDensity.current
AnimatedVisibility(
    visible = visible,
    enter = slideInVertically {
        // Slide in from 40 dp from the top.
        with(density) { -40.dp.roundToPx() }
    } + expandVertically(
        // Expand from the top.
        expandFrom = Alignment.Top
    ) + fadeIn(
        // Fade in with the initial alpha of 0.3f.
        initialAlpha = 0.3f
    ),
    exit = slideOutVertically() + shrinkVertically() + fadeOut()
) {
    Text(
        "Hello",
        Modifier
            .fillMaxWidth()
            .height(200.dp)
    )
}

Как показано в примере выше, вы можете объединить несколько объектов EnterTransition или ExitTransition с помощью оператора +. Каждый из них принимает необязательные параметры, позволяющие настроить его поведение. Дополнительную информацию можно найти на справочных страницах.

Примеры переходов при появлении и исчезновении

EnterTransition ExitTransition
fadeIn
Элемент интерфейса постепенно появляется на экране.
fadeOut
Элемент интерфейса постепенно исчезает из поля зрения.
slideIn
Элемент интерфейса появляется на экране, выдвигаясь из-за его границ.
slideOut
Элемент интерфейса уходит за пределы экрана.
slideInHorizontally
Элемент интерфейса появляется, двигаясь по горизонтали.
slideOutHorizontally
Элемент интерфейса сдвигается по горизонтали и исчезает из поля зрения.
slideInVertically
Элемент интерфейса появляется, перемещаясь по вертикали.
slideOutVertically
Элемент интерфейса сдвигается по вертикали и исчезает из поля зрения.
scaleIn
Элемент интерфейса увеличивается и появляется на экране.
scaleOut
Элемент интерфейса уменьшается и исчезает из поля зрения.
expandIn
Элемент интерфейса разворачивается из центральной точки.
shrinkOut
Элемент интерфейса уменьшается до точки и исчезает.
expandHorizontally
Элемент интерфейса расширяется по горизонтали и становится видимым.
shrinkHorizontally
Элемент интерфейса сжимается по горизонтали и исчезает.
expandVertically
Элемент интерфейса разворачивается по вертикали.
shrinkVertically
Элемент интерфейса сжимается по вертикали и исчезает.

В AnimatedVisibility также есть вариант, который принимает аргумент MutableTransitionState. Это позволяет запускать анимацию, как только composable-функция AnimatedVisibility добавляется в дерево композиции. Он также полезен для наблюдения за состоянием анимации.

// Create a MutableTransitionState<Boolean> for the AnimatedVisibility.
val state = remember {
    MutableTransitionState(false).apply {
        // Start the animation immediately.
        targetState = true
    }
}
Column {
    AnimatedVisibility(visibleState = state) {
        Text(text = "Hello, world!")
    }

    // Use the MutableTransitionState to know the current animation state
    // of the AnimatedVisibility.
    Text(
        text = when {
            state.isIdle && state.currentState -> "Visible"
            !state.isIdle && state.currentState -> "Disappearing"
            state.isIdle && !state.currentState -> "Invisible"
            else -> "Appearing"
        }
    )
}

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

Контент внутри AnimatedVisibility (прямые или косвенные дочерние элементы) может использовать модификатор animateEnterExit, чтобы задавать разное поведение анимации для каждого из них. Визуальный эффект для каждого из этих дочерних элементов представляет собой комбинацию анимаций, указанных в composable-функции AnimatedVisibility, и собственных анимаций входа и выхода дочернего элемента.

var visible by remember { mutableStateOf(true) }

AnimatedVisibility(
    visible = visible,
    enter = fadeIn(),
    exit = fadeOut()
) {
    // Fade in/out the background and the foreground.
    Box(
        Modifier
            .fillMaxSize()
            .background(Color.DarkGray)
    ) {
        Box(
            Modifier
                .align(Alignment.Center)
                .animateEnterExit(
                    // Slide in/out the inner box.
                    enter = slideInVertically(),
                    exit = slideOutVertically()
                )
                .sizeIn(minWidth = 256.dp, minHeight = 64.dp)
                .background(Color.Red)
        ) {
            // Content of the notification…
        }
    }
}

В некоторых случаях вы можете захотеть, чтобы AnimatedVisibility не применял никаких анимаций, чтобы у каждого ребенка была своя отдельная анимация от animateEnterExit. Для этого укажите EnterTransition.None и ExitTransition.None в composable-функции AnimatedVisibility.

Добавить анимацию

Если вы хотите добавить собственные эффекты анимации, помимо встроенных анимаций входа и выхода, получите доступ к базовому экземпляру Transition, используя свойство transition внутри лямбда-выражения контента для AnimatedVisibility. Любые состояния анимации, добавленные в экземпляр Transition, будут выполняться одновременно с анимацией появления и исчезновения AnimatedVisibility. AnimatedVisibility ждет, пока завершатся все анимации в Transition, прежде чем удалить его контент. Если анимация выхода создана независимо от Transition (например, с помощью animate*AsState), AnimatedVisibility не сможет ее учесть и может удалить composable-функцию контента до завершения анимации.

var visible by remember { mutableStateOf(true) }

AnimatedVisibility(
    visible = visible,
    enter = fadeIn(),
    exit = fadeOut()
) { // this: AnimatedVisibilityScope
    // Use AnimatedVisibilityScope#transition to add a custom animation
    // to the AnimatedVisibility.
    val background by transition.animateColor(label = "color") { state ->
        if (state == EnterExitState.Visible) Color.Blue else Color.Gray
    }
    Box(
        modifier = Modifier
            .size(128.dp)
            .background(background)
    )
}

Подробнее о том, как использовать Transition для управления анимацией, рассказывается в статье Как анимировать несколько свойств одновременно с помощью перехода.

Анимация на основе целевого состояния

Функция AnimatedContent анимирует контент, когда он меняется в зависимости от целевого состояния.

Row {
    var count by remember { mutableIntStateOf(0) }
    Button(onClick = { count++ }) {
        Text("Add")
    }
    AnimatedContent(
        targetState = count,
        label = "animated content"
    ) { targetCount ->
        // Make sure to use `targetCount`, not `count`.
        Text(text = "Count: $targetCount")
    }
}

По умолчанию исходный контент исчезает, а затем появляется целевой контент (это поведение называется затуханием). Вы можете настроить анимацию, указав объект ContentTransform в параметре transitionSpec. Чтобы создать экземпляр ContentTransform, объедините объект EnterTransition с объектом ExitTransition, используя инфиксную функцию with. Вы можете применить SizeTransform к объекту ContentTransform, прикрепив его с помощью инфиксной функции using.

AnimatedContent(
    targetState = count,
    transitionSpec = {
        // Compare the incoming number with the previous number.
        if (targetState > initialState) {
            // If the target number is larger, it slides up and fades in
            // while the initial (smaller) number slides up and fades out.
            slideInVertically { height -> height } + fadeIn() togetherWith
                slideOutVertically { height -> -height } + fadeOut()
        } else {
            // If the target number is smaller, it slides down and fades in
            // while the initial number slides down and fades out.
            slideInVertically { height -> -height } + fadeIn() togetherWith
                slideOutVertically { height -> height } + fadeOut()
        }.using(
            // Disable clipping since the faded slide-in/out should
            // be displayed out of bounds.
            SizeTransform(clip = false)
        )
    }, label = "animated content"
) { targetCount ->
    Text(text = "$targetCount")
}

EnterTransition определяет, как должен появляться целевой контент, а ExitTransition – как должен исчезать исходный контент. Помимо всех функций EnterTransition и ExitTransition, доступных в AnimatedVisibility, в AnimatedContent есть slideIntoContainer и slideOutOfContainer. Это удобные альтернативы slideInHorizontally/Vertically и slideOutHorizontally/Vertically, которые рассчитывают расстояние скольжения на основе размеров исходного и целевого контента AnimatedContent.

SizeTransform определяет, как должен анимироваться размер между исходным и целевым контентом. При создании анимации вы можете использовать как исходный, так и целевой размер. SizeTransform также определяет, нужно ли обрезать контент по размеру компонента во время анимации.

var expanded by remember { mutableStateOf(false) }
Surface(
    color = MaterialTheme.colorScheme.primary,
    onClick = { expanded = !expanded }
) {
    AnimatedContent(
        targetState = expanded,
        transitionSpec = {
            fadeIn(animationSpec = tween(150, 150)) togetherWith
                fadeOut(animationSpec = tween(150)) using
                SizeTransform { initialSize, targetSize ->
                    if (targetState) {
                        keyframes {
                            // Expand horizontally first.
                            IntSize(targetSize.width, initialSize.height) at 150
                            durationMillis = 300
                        }
                    } else {
                        keyframes {
                            // Shrink vertically first.
                            IntSize(initialSize.width, targetSize.height) at 150
                            durationMillis = 300
                        }
                    }
                }
        }, label = "size transform"
    ) { targetExpanded ->
        if (targetExpanded) {
            Expanded()
        } else {
            ContentIcon()
        }
    }
}

Анимировать переходы при появлении и исчезновении дочерних элементов

Как и AnimatedVisibility, модификатор animateEnterExit доступен в лямбда-выражении контента AnimatedContent. Используйте этот вариант, чтобы применить EnterAnimation и ExitAnimation к каждому из прямых или косвенных дочерних элементов по отдельности.

Добавить анимацию

Как и AnimatedVisibility, поле transition доступно в лямбда-выражении контента AnimatedContent. Используйте его, чтобы создать собственный эффект анимации, который будет воспроизводиться одновременно с переходом AnimatedContent. Подробную информацию можно найти в разделе updateTransition.

Анимация между двумя макетами

Crossfade анимирует переход между двумя макетами с помощью перекрестного затухания. При переключении значения, передаваемого параметру current, контент меняется с анимацией перекрестного затухания.

var currentPage by remember { mutableStateOf("A") }
Crossfade(targetState = currentPage, label = "cross fade") { screen ->
    when (screen) {
        "A" -> Text("Page A")
        "B" -> Text("Page B")
    }
}

Встроенные модификаторы анимации

В Compose есть модификаторы для анимации определенных изменений непосредственно в composable-функциях.

Анимация изменения размера composable-функции

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

Модификатор 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
        }

) {
}

Анимация элементов списка

Если вы хотите анимировать изменение порядка элементов в ленивом списке или сетке, ознакомьтесь с документацией по анимации элементов ленивого макета.