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

В 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 не сможет ее учесть и может удалить контент до ее завершения.

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 доступно в лямбда-выражении content элемента 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
        }

) {
}

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

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