Анимация на основе значений

Анимируйте одно значение с помощью animate*AsState

Функции animate*AsState — это простейшие API-интерфейсы анимации в Compose для анимации одного значения. Вы указываете только целевое значение (или конечное значение), и API запускает анимацию от текущего значения до указанного значения.

Ниже приведен пример анимации альфа-канала с использованием этого API. Просто обернув целевое значение в animateFloatAsState , значение альфа теперь является значением анимации между предоставленными значениями (в данном случае 1f или 0.5f ).

var enabled by remember { mutableStateOf(true) }

val alpha: Float by animateFloatAsState(if (enabled) 1f else 0.5f, label = "alpha")
Box(
    Modifier
        .fillMaxSize()
        .graphicsLayer(alpha = alpha)
        .background(Color.Red)
)

Обратите внимание, что вам не нужно создавать экземпляр какого-либо класса анимации или обрабатывать прерывания. Под капотом объект анимации (а именно, экземпляр Animatable ) будет создан и запомнен в месте вызова с первым целевым значением в качестве начального значения. С этого момента каждый раз, когда вы указываете этому составному элементу другое целевое значение, автоматически запускается анимация для достижения этого значения. Если в полете уже есть анимация, она начинается с текущего значения (и скорости) и анимируется в направлении целевого значения. Во время анимации этот составной объект перекомпоновывается и возвращает обновленное значение анимации каждый кадр.

В стандартной комплектации Compose предоставляет функции animate*AsState для Float , Color , Dp , Size , Offset , Rect , Int , IntOffset и IntSize . Вы можете легко добавить поддержку других типов данных, предоставив TwoWayConverter для animateValueAsState , который принимает универсальный тип.

Вы можете настроить характеристики анимации, предоставив AnimationSpec . См. AnimationSpec для получения дополнительной информации.

Анимация нескольких свойств одновременно с переходом

Transition управляет одной или несколькими анимациями как дочерними и запускает их одновременно между несколькими состояниями.

Состояния могут иметь любой тип данных. Во многих случаях вы можете использовать собственный тип enum для обеспечения безопасности типов, как в этом примере:

enum class BoxState {
    Collapsed,
    Expanded
}

updateTransition создает и запоминает экземпляр Transition и обновляет его состояние.

var currentState by remember { mutableStateOf(BoxState.Collapsed) }
val transition = updateTransition(currentState, label = "box state")

Затем вы можете использовать одну из функций расширения animate* , чтобы определить дочернюю анимацию в этом переходе. Укажите целевые значения для каждого из состояний. Эти функции animate* возвращают значение анимации, которое обновляется каждый кадр во время анимации, когда состояние перехода обновляется с помощью updateTransition .

val rect by transition.animateRect(label = "rectangle") { state ->
    when (state) {
        BoxState.Collapsed -> Rect(0f, 0f, 100f, 100f)
        BoxState.Expanded -> Rect(100f, 100f, 300f, 300f)
    }
}
val borderWidth by transition.animateDp(label = "border width") { state ->
    when (state) {
        BoxState.Collapsed -> 1.dp
        BoxState.Expanded -> 0.dp
    }
}

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

val color by transition.animateColor(
    transitionSpec = {
        when {
            BoxState.Expanded isTransitioningTo BoxState.Collapsed ->
                spring(stiffness = 50f)

            else ->
                tween(durationMillis = 500)
        }
    }, label = "color"
) { state ->
    when (state) {
        BoxState.Collapsed -> MaterialTheme.colorScheme.primary
        BoxState.Expanded -> MaterialTheme.colorScheme.background
    }
}

Как только переход достигнет целевого состояния, Transition.currentState будет таким же, как Transition.targetState . Это можно использовать как сигнал о том, завершился ли переход.

Иногда нам нужно, чтобы начальное состояние отличалось от первого целевого состояния. Для этого мы можем использовать updateTransition с MutableTransitionState . Например, это позволяет нам запускать анимацию, как только код входит в композицию.

// Start in collapsed state and immediately animate to expanded
var currentState = remember { MutableTransitionState(BoxState.Collapsed) }
currentState.targetState = BoxState.Expanded
val transition = rememberTransition(currentState, label = "box state")
// ……

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

enum class DialerState { DialerMinimized, NumberPad }

@Composable
fun DialerButton(isVisibleTransition: Transition<Boolean>) {
    // `isVisibleTransition` spares the need for the content to know
    // about other DialerStates. Instead, the content can focus on
    // animating the state change between visible and not visible.
}

@Composable
fun NumberPad(isVisibleTransition: Transition<Boolean>) {
    // `isVisibleTransition` spares the need for the content to know
    // about other DialerStates. Instead, the content can focus on
    // animating the state change between visible and not visible.
}

@Composable
fun Dialer(dialerState: DialerState) {
    val transition = updateTransition(dialerState, label = "dialer state")
    Box {
        // Creates separate child transitions of Boolean type for NumberPad
        // and DialerButton for any content animation between visible and
        // not visible
        NumberPad(
            transition.createChildTransition {
                it == DialerState.NumberPad
            }
        )
        DialerButton(
            transition.createChildTransition {
                it == DialerState.DialerMinimized
            }
        )
    }
}

Используйте переход с AnimatedVisibility и AnimatedContent

AnimatedVisibility и AnimatedContent доступны как функции расширения Transition . targetState для Transition.AnimatedVisibility и Transition.AnimatedContent является производным от Transition и запускает переходы входа/выхода по мере необходимости, когда targetState Transition изменяется. Эти функции расширения позволяют поднимать в Transition все анимации входа/выхода/sizeTransform, которые в противном случае были бы внутренними для AnimatedVisibility / AnimatedContent . С помощью этих функций расширения изменение состояния AnimatedVisibility / AnimatedContent можно наблюдать снаружи. Вместо логического visible параметра эта версия AnimatedVisibility принимает лямбда-выражение, которое преобразует целевое состояние родительского перехода в логическое значение.

Подробности см. в разделах AnimatedVisibility и AnimatedContent .

var selected by remember { mutableStateOf(false) }
// Animates changes when `selected` is changed.
val transition = updateTransition(selected, label = "selected state")
val borderColor by transition.animateColor(label = "border color") { isSelected ->
    if (isSelected) Color.Magenta else Color.White
}
val elevation by transition.animateDp(label = "elevation") { isSelected ->
    if (isSelected) 10.dp else 2.dp
}
Surface(
    onClick = { selected = !selected },
    shape = RoundedCornerShape(8.dp),
    border = BorderStroke(2.dp, borderColor),
    shadowElevation = elevation
) {
    Column(
        modifier = Modifier
            .fillMaxWidth()
            .padding(16.dp)
    ) {
        Text(text = "Hello, world!")
        // AnimatedVisibility as a part of the transition.
        transition.AnimatedVisibility(
            visible = { targetSelected -> targetSelected },
            enter = expandVertically(),
            exit = shrinkVertically()
        ) {
            Text(text = "It is fine today.")
        }
        // AnimatedContent as a part of the transition.
        transition.AnimatedContent { targetState ->
            if (targetState) {
                Text(text = "Selected")
            } else {
                Icon(imageVector = Icons.Default.Phone, contentDescription = "Phone")
            }
        }
    }
}

Инкапсулируйте переход и сделайте его многоразовым

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

Вы можете сделать это, создав класс, содержащий все значения анимации, и функцию «обновления», которая возвращает экземпляр этого класса. Реализация перехода может быть выделена в новую отдельную функцию. Этот шаблон полезен, когда необходимо централизовать логику анимации или сделать сложную анимацию многоразовой.

enum class BoxState { Collapsed, Expanded }

@Composable
fun AnimatingBox(boxState: BoxState) {
    val transitionData = updateTransitionData(boxState)
    // UI tree
    Box(
        modifier = Modifier
            .background(transitionData.color)
            .size(transitionData.size)
    )
}

// Holds the animation values.
private class TransitionData(
    color: State<Color>,
    size: State<Dp>
) {
    val color by color
    val size by size
}

// Create a Transition and return its animation values.
@Composable
private fun updateTransitionData(boxState: BoxState): TransitionData {
    val transition = updateTransition(boxState, label = "box state")
    val color = transition.animateColor(label = "color") { state ->
        when (state) {
            BoxState.Collapsed -> Color.Gray
            BoxState.Expanded -> Color.Red
        }
    }
    val size = transition.animateDp(label = "size") { state ->
        when (state) {
            BoxState.Collapsed -> 64.dp
            BoxState.Expanded -> 128.dp
        }
    }
    return remember(transition) { TransitionData(color, size) }
}

Создайте бесконечно повторяющуюся анимацию с помощью rememberInfiniteTransition

InfiniteTransition содержит одну или несколько дочерних анимаций, таких как Transition , но анимации начинают выполняться, как только они входят в композицию, и не останавливаются, пока их не удалят. Вы можете создать экземпляр InfiniteTransition с помощью rememberInfiniteTransition . Дочерние анимации можно добавить с помощью animateColor , animatedFloat или animatedValue . Вам также необходимо указать InfinRepeatable , чтобы указать спецификации анимации.

val infiniteTransition = rememberInfiniteTransition(label = "infinite")
val color by infiniteTransition.animateColor(
    initialValue = Color.Red,
    targetValue = Color.Green,
    animationSpec = infiniteRepeatable(
        animation = tween(1000, easing = LinearEasing),
        repeatMode = RepeatMode.Reverse
    ),
    label = "color"
)

Box(
    Modifier
        .fillMaxSize()
        .background(color)
)

Низкоуровневые API-интерфейсы анимации

Все API-интерфейсы анимации высокого уровня, упомянутые в предыдущем разделе, построены на основе API-интерфейсов анимации низкого уровня.

Функции animate*AsState — это простейшие API, которые отображают мгновенное изменение значения как значение анимации. Он поддерживается Animatable — API на основе сопрограмм для анимации одного значения. updateTransition создает объект перехода, который может управлять несколькими значениями анимации и запускать их в зависимости от изменения состояния. rememberInfiniteTransition аналогичен, но он создает бесконечный переход, который может управлять несколькими анимациями, которые продолжают выполняться бесконечно. Все эти API являются составными, за исключением Animatable , что означает, что эти анимации можно создавать вне композиции.

Все эти API основаны на более фундаментальном API Animation . Хотя большинство приложений не взаимодействуют напрямую с Animation , некоторые возможности настройки Animation доступны через API более высокого уровня. Дополнительные сведения о AnimationVector и AnimationSpec см. в разделе Настройка анимации .

Диаграмма, показывающая взаимосвязь между различными API низкоуровневой анимации.

Animatable : анимация одного значения на основе сопрограммы.

Animatable — это держатель значения, который может анимировать значение при его изменении с помощью animateTo . Это API, поддерживающий реализацию animate*AsState . Это обеспечивает последовательное продолжение и взаимоисключаемость, а это означает, что изменение значений всегда происходит непрерывно, и любая текущая анимация будет отменена.

Многие функции Animatable , включая animateTo , предоставляются в виде функций приостановки. Это означает, что их необходимо обернуть в соответствующую область сопрограммы. Например, вы можете использовать компонуемый объект LaunchedEffect для создания области только на время действия указанного значения ключа.

// Start out gray and animate to green/red based on `ok`
val color = remember { Animatable(Color.Gray) }
LaunchedEffect(ok) {
    color.animateTo(if (ok) Color.Green else Color.Red)
}
Box(
    Modifier
        .fillMaxSize()
        .background(color.value)
)

В приведенном выше примере мы создаем и запоминаем экземпляр Animatable с начальным значением Color.Gray . В зависимости от значения логического флага ok , цвет анимируется либо Color.Green , либо Color.Red . Любое последующее изменение логического значения запускает анимацию другого цвета. Если при изменении значения существует продолжающаяся анимация, анимация отменяется, а новая анимация начинается с текущего значения снимка с текущей скоростью.

Это реализация анимации, которая поддерживает API animate*AsState упомянутый в предыдущем разделе. По сравнению с animate*AsState , использование Animatable напрямую дает нам более детальный контроль в нескольких аспектах. Во-первых, Animatable может иметь начальное значение, отличное от первого целевого значения. Например, в приведенном выше примере кода сначала отображается серый прямоугольник, который сразу же начинает меняться на зеленый или красный цвет. Во-вторых, Animatable предоставляет больше операций со значением содержимого, а именно snapTo и animateDecay . snapTo немедленно устанавливает текущее значение на целевое. Это полезно, когда сама анимация не является единственным источником истины и ее необходимо синхронизировать с другими состояниями, такими как события касания. animateDecay запускает анимацию, которая замедляется с заданной скорости. Это полезно для реализации поведения бросания. Дополнительные сведения см. в разделе Жесты и анимация .

По умолчанию Animatable поддерживает Float и Color , но можно использовать любой тип данных, предоставив TwoWayConverter . См. AnimationVector для получения дополнительной информации.

Вы можете настроить характеристики анимации, предоставив AnimationSpec . См. AnimationSpec для получения дополнительной информации.

Animation : анимация с ручным управлением.

Animation — это доступный API анимации самого низкого уровня. Многие из анимаций, которые мы видели до сих пор, основаны на Animation. Существует два подтипа Animation : TargetBasedAnimation и DecayAnimation .

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

TargetBasedAnimation

Другие API охватывают большинство случаев использования, но использование TargetBasedAnimation напрямую позволяет вам самостоятельно контролировать время воспроизведения анимации. В приведенном ниже примере время воспроизведения TargetAnimation контролируется вручную на основе времени кадра, предоставленного withFrameNanos .

val anim = remember {
    TargetBasedAnimation(
        animationSpec = tween(200),
        typeConverter = Float.VectorConverter,
        initialValue = 200f,
        targetValue = 1000f
    )
}
var playTime by remember { mutableLongStateOf(0L) }

LaunchedEffect(anim) {
    val startTime = withFrameNanos { it }

    do {
        playTime = withFrameNanos { it } - startTime
        val animationValue = anim.getValueFromNanos(playTime)
    } while (someCustomCondition())
}

DecayAnimation

В отличие от TargetBasedAnimation , DecayAnimation не требует предоставления targetValue . Вместо этого он вычисляет свое targetValue на основе начальных условий, установленных initialVelocity и initialValue , а также предоставленного DecayAnimationSpec .

Анимации затухания часто используются после жеста броска, чтобы замедлить элементы до полной остановки. Скорость анимации начинается со значения, установленного в initialVelocityVector , и со временем замедляется.

{% дословно %} {% дословно %} {% дословно %} {% endverbatim %} ,

Анимируйте одно значение с помощью animate*AsState

Функции animate*AsState — это простейшие API-интерфейсы анимации в Compose для анимации одного значения. Вы указываете только целевое значение (или конечное значение), и API запускает анимацию от текущего значения до указанного значения.

Ниже приведен пример анимации альфа-канала с использованием этого API. Просто обернув целевое значение в animateFloatAsState , значение альфа теперь является значением анимации между предоставленными значениями (в данном случае 1f или 0.5f ).

var enabled by remember { mutableStateOf(true) }

val alpha: Float by animateFloatAsState(if (enabled) 1f else 0.5f, label = "alpha")
Box(
    Modifier
        .fillMaxSize()
        .graphicsLayer(alpha = alpha)
        .background(Color.Red)
)

Обратите внимание, что вам не нужно создавать экземпляр какого-либо класса анимации или обрабатывать прерывания. Под капотом объект анимации (а именно, экземпляр Animatable ) будет создан и запомнен в месте вызова с первым целевым значением в качестве начального значения. С этого момента каждый раз, когда вы указываете этому составному элементу другое целевое значение, автоматически запускается анимация для достижения этого значения. Если в полете уже есть анимация, она начинается с текущего значения (и скорости) и анимируется в направлении целевого значения. Во время анимации этот составной объект перекомпоновывается и возвращает обновленное значение анимации каждый кадр.

В стандартной комплектации Compose предоставляет функции animate*AsState для Float , Color , Dp , Size , Offset , Rect , Int , IntOffset и IntSize . Вы можете легко добавить поддержку других типов данных, предоставив TwoWayConverter для animateValueAsState , который принимает универсальный тип.

Вы можете настроить характеристики анимации, предоставив AnimationSpec . См. AnimationSpec для получения дополнительной информации.

Анимация нескольких свойств одновременно с переходом

Transition управляет одной или несколькими анимациями как дочерними и запускает их одновременно между несколькими состояниями.

Состояния могут иметь любой тип данных. Во многих случаях вы можете использовать собственный тип enum для обеспечения безопасности типов, как в этом примере:

enum class BoxState {
    Collapsed,
    Expanded
}

updateTransition создает и запоминает экземпляр Transition и обновляет его состояние.

var currentState by remember { mutableStateOf(BoxState.Collapsed) }
val transition = updateTransition(currentState, label = "box state")

Затем вы можете использовать одну из функций расширения animate* , чтобы определить дочернюю анимацию в этом переходе. Укажите целевые значения для каждого из состояний. Эти функции animate* возвращают значение анимации, которое обновляется каждый кадр во время анимации, когда состояние перехода обновляется с помощью updateTransition .

val rect by transition.animateRect(label = "rectangle") { state ->
    when (state) {
        BoxState.Collapsed -> Rect(0f, 0f, 100f, 100f)
        BoxState.Expanded -> Rect(100f, 100f, 300f, 300f)
    }
}
val borderWidth by transition.animateDp(label = "border width") { state ->
    when (state) {
        BoxState.Collapsed -> 1.dp
        BoxState.Expanded -> 0.dp
    }
}

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

val color by transition.animateColor(
    transitionSpec = {
        when {
            BoxState.Expanded isTransitioningTo BoxState.Collapsed ->
                spring(stiffness = 50f)

            else ->
                tween(durationMillis = 500)
        }
    }, label = "color"
) { state ->
    when (state) {
        BoxState.Collapsed -> MaterialTheme.colorScheme.primary
        BoxState.Expanded -> MaterialTheme.colorScheme.background
    }
}

Как только переход достигнет целевого состояния, Transition.currentState будет таким же, как Transition.targetState . Это можно использовать как сигнал о том, завершился ли переход.

Иногда нам нужно, чтобы начальное состояние отличалось от первого целевого состояния. Для этого мы можем использовать updateTransition с MutableTransitionState . Например, это позволяет нам запускать анимацию, как только код входит в композицию.

// Start in collapsed state and immediately animate to expanded
var currentState = remember { MutableTransitionState(BoxState.Collapsed) }
currentState.targetState = BoxState.Expanded
val transition = rememberTransition(currentState, label = "box state")
// ……

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

enum class DialerState { DialerMinimized, NumberPad }

@Composable
fun DialerButton(isVisibleTransition: Transition<Boolean>) {
    // `isVisibleTransition` spares the need for the content to know
    // about other DialerStates. Instead, the content can focus on
    // animating the state change between visible and not visible.
}

@Composable
fun NumberPad(isVisibleTransition: Transition<Boolean>) {
    // `isVisibleTransition` spares the need for the content to know
    // about other DialerStates. Instead, the content can focus on
    // animating the state change between visible and not visible.
}

@Composable
fun Dialer(dialerState: DialerState) {
    val transition = updateTransition(dialerState, label = "dialer state")
    Box {
        // Creates separate child transitions of Boolean type for NumberPad
        // and DialerButton for any content animation between visible and
        // not visible
        NumberPad(
            transition.createChildTransition {
                it == DialerState.NumberPad
            }
        )
        DialerButton(
            transition.createChildTransition {
                it == DialerState.DialerMinimized
            }
        )
    }
}

Используйте переход с AnimatedVisibility и AnimatedContent

AnimatedVisibility и AnimatedContent доступны как функции расширения Transition . targetState для Transition.AnimatedVisibility и Transition.AnimatedContent является производным от Transition и запускает переходы входа/выхода по мере необходимости, когда targetState Transition изменяется. Эти функции расширения позволяют поднимать в Transition все анимации входа/выхода/sizeTransform, которые в противном случае были бы внутренними для AnimatedVisibility / AnimatedContent . С помощью этих функций расширения изменение состояния AnimatedVisibility / AnimatedContent можно наблюдать снаружи. Вместо логического visible параметра эта версия AnimatedVisibility принимает лямбда-выражение, которое преобразует целевое состояние родительского перехода в логическое значение.

Подробности см. в разделах AnimatedVisibility и AnimatedContent .

var selected by remember { mutableStateOf(false) }
// Animates changes when `selected` is changed.
val transition = updateTransition(selected, label = "selected state")
val borderColor by transition.animateColor(label = "border color") { isSelected ->
    if (isSelected) Color.Magenta else Color.White
}
val elevation by transition.animateDp(label = "elevation") { isSelected ->
    if (isSelected) 10.dp else 2.dp
}
Surface(
    onClick = { selected = !selected },
    shape = RoundedCornerShape(8.dp),
    border = BorderStroke(2.dp, borderColor),
    shadowElevation = elevation
) {
    Column(
        modifier = Modifier
            .fillMaxWidth()
            .padding(16.dp)
    ) {
        Text(text = "Hello, world!")
        // AnimatedVisibility as a part of the transition.
        transition.AnimatedVisibility(
            visible = { targetSelected -> targetSelected },
            enter = expandVertically(),
            exit = shrinkVertically()
        ) {
            Text(text = "It is fine today.")
        }
        // AnimatedContent as a part of the transition.
        transition.AnimatedContent { targetState ->
            if (targetState) {
                Text(text = "Selected")
            } else {
                Icon(imageVector = Icons.Default.Phone, contentDescription = "Phone")
            }
        }
    }
}

Инкапсулируйте переход и сделайте его многоразовым

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

Вы можете сделать это, создав класс, содержащий все значения анимации, и функцию «обновления», которая возвращает экземпляр этого класса. Реализация перехода может быть выделена в новую отдельную функцию. Этот шаблон полезен, когда необходимо централизовать логику анимации или сделать сложную анимацию многоразовой.

enum class BoxState { Collapsed, Expanded }

@Composable
fun AnimatingBox(boxState: BoxState) {
    val transitionData = updateTransitionData(boxState)
    // UI tree
    Box(
        modifier = Modifier
            .background(transitionData.color)
            .size(transitionData.size)
    )
}

// Holds the animation values.
private class TransitionData(
    color: State<Color>,
    size: State<Dp>
) {
    val color by color
    val size by size
}

// Create a Transition and return its animation values.
@Composable
private fun updateTransitionData(boxState: BoxState): TransitionData {
    val transition = updateTransition(boxState, label = "box state")
    val color = transition.animateColor(label = "color") { state ->
        when (state) {
            BoxState.Collapsed -> Color.Gray
            BoxState.Expanded -> Color.Red
        }
    }
    val size = transition.animateDp(label = "size") { state ->
        when (state) {
            BoxState.Collapsed -> 64.dp
            BoxState.Expanded -> 128.dp
        }
    }
    return remember(transition) { TransitionData(color, size) }
}

Создайте бесконечно повторяющуюся анимацию с помощью rememberInfiniteTransition

InfiniteTransition содержит одну или несколько дочерних анимаций, таких как Transition , но анимации начинают выполняться, как только они входят в композицию, и не останавливаются, пока их не удалят. Вы можете создать экземпляр InfiniteTransition с помощью rememberInfiniteTransition . Дочерние анимации можно добавить с помощью animateColor , animatedFloat или animatedValue . Вам также необходимо указать InfinRepeatable , чтобы указать спецификации анимации.

val infiniteTransition = rememberInfiniteTransition(label = "infinite")
val color by infiniteTransition.animateColor(
    initialValue = Color.Red,
    targetValue = Color.Green,
    animationSpec = infiniteRepeatable(
        animation = tween(1000, easing = LinearEasing),
        repeatMode = RepeatMode.Reverse
    ),
    label = "color"
)

Box(
    Modifier
        .fillMaxSize()
        .background(color)
)

Низкоуровневые API-интерфейсы анимации

Все API-интерфейсы анимации высокого уровня, упомянутые в предыдущем разделе, построены на основе API-интерфейсов анимации низкого уровня.

Функции animate*AsState — это простейшие API, которые отображают мгновенное изменение значения как значение анимации. Он поддерживается Animatable — API на основе сопрограмм для анимации одного значения. updateTransition создает объект перехода, который может управлять несколькими значениями анимации и запускать их в зависимости от изменения состояния. rememberInfiniteTransition аналогичен, но он создает бесконечный переход, который может управлять несколькими анимациями, которые продолжают выполняться бесконечно. Все эти API являются составными, за исключением Animatable , что означает, что эти анимации можно создавать вне композиции.

Все эти API основаны на более фундаментальном API Animation . Хотя большинство приложений не взаимодействуют напрямую с Animation , некоторые возможности настройки Animation доступны через API более высокого уровня. Дополнительные сведения о AnimationVector и AnimationSpec см. в разделе Настройка анимации .

Диаграмма, показывающая взаимосвязь между различными API низкоуровневой анимации.

Animatable : анимация одного значения на основе сопрограммы.

Animatable — это держатель значения, который может анимировать значение при его изменении с помощью animateTo . Это API, поддерживающий реализацию animate*AsState . Это обеспечивает последовательное продолжение и взаимоисключаемость, а это означает, что изменение значений всегда происходит непрерывно, и любая текущая анимация будет отменена.

Многие функции Animatable , включая animateTo , предоставляются в виде функций приостановки. Это означает, что их необходимо обернуть в соответствующую область сопрограммы. Например, вы можете использовать компонуемый объект LaunchedEffect для создания области только на время действия указанного значения ключа.

// Start out gray and animate to green/red based on `ok`
val color = remember { Animatable(Color.Gray) }
LaunchedEffect(ok) {
    color.animateTo(if (ok) Color.Green else Color.Red)
}
Box(
    Modifier
        .fillMaxSize()
        .background(color.value)
)

В приведенном выше примере мы создаем и запоминаем экземпляр Animatable с начальным значением Color.Gray . В зависимости от значения логического флага ok , цвет анимируется либо Color.Green , либо Color.Red . Любое последующее изменение логического значения запускает анимацию другого цвета. Если при изменении значения существует продолжающаяся анимация, анимация отменяется, а новая анимация начинается с текущего значения снимка с текущей скоростью.

Это реализация анимации, которая поддерживает API animate*AsState упомянутый в предыдущем разделе. По сравнению с animate*AsState использование Animatable напрямую дает нам более детальный контроль в нескольких аспектах. Во-первых, Animatable может иметь начальное значение, отличное от первого целевого значения. Например, в приведенном выше примере кода сначала отображается серый прямоугольник, который сразу же начинает меняться на зеленый или красный цвет. Во-вторых, Animatable предоставляет больше операций со значением содержимого, а именно snapTo и animateDecay . snapTo немедленно устанавливает текущее значение на целевое. Это полезно, когда сама анимация не является единственным источником истины и ее необходимо синхронизировать с другими состояниями, такими как события касания. animateDecay запускает анимацию, которая замедляется с заданной скорости. Это полезно для реализации поведения бросания. Дополнительные сведения см. в разделе Жесты и анимация .

По умолчанию Animatable поддерживает Float и Color , но можно использовать любой тип данных, предоставив TwoWayConverter . См. AnimationVector для получения дополнительной информации.

Вы можете настроить характеристики анимации, предоставив AnimationSpec . См. AnimationSpec для получения дополнительной информации.

Animation : анимация с ручным управлением.

Animation — это доступный API анимации самого низкого уровня. Многие анимации, которые мы видели до сих пор, основаны на Animation. Существует два подтипа Animation : TargetBasedAnimation и DecayAnimation .

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

TargetBasedAnimation

Другие API охватывают большинство случаев использования, но использование TargetBasedAnimation напрямую позволяет вам самостоятельно контролировать время воспроизведения анимации. В приведенном ниже примере время воспроизведения TargetAnimation контролируется вручную на основе времени кадра, предоставленного withFrameNanos .

val anim = remember {
    TargetBasedAnimation(
        animationSpec = tween(200),
        typeConverter = Float.VectorConverter,
        initialValue = 200f,
        targetValue = 1000f
    )
}
var playTime by remember { mutableLongStateOf(0L) }

LaunchedEffect(anim) {
    val startTime = withFrameNanos { it }

    do {
        playTime = withFrameNanos { it } - startTime
        val animationValue = anim.getValueFromNanos(playTime)
    } while (someCustomCondition())
}

DecayAnimation

В отличие от TargetBasedAnimation , DecayAnimation не требует предоставления targetValue . Вместо этого он вычисляет свое targetValue на основе начальных условий, установленных initialVelocity и initialValue , а также предоставленного DecayAnimationSpec .

Анимации затухания часто используются после жеста броска, чтобы замедлить элементы до полной остановки. Скорость анимации начинается со значения, установленного в initialVelocityVector , и со временем замедляется.

{% дословно %} {% дословно %} {% дословно %} {% endverbatim %} ,

Анимируйте одно значение с помощью animate*AsState

Функции animate*AsState — это простейшие API-интерфейсы анимации в Compose для анимации одного значения. Вы указываете только целевое значение (или конечное значение), и API запускает анимацию от текущего значения до указанного значения.

Ниже приведен пример анимации альфа-канала с использованием этого API. Просто обернув целевое значение в animateFloatAsState , значение альфа теперь является значением анимации между предоставленными значениями (в данном случае 1f или 0.5f ).

var enabled by remember { mutableStateOf(true) }

val alpha: Float by animateFloatAsState(if (enabled) 1f else 0.5f, label = "alpha")
Box(
    Modifier
        .fillMaxSize()
        .graphicsLayer(alpha = alpha)
        .background(Color.Red)
)

Обратите внимание, что вам не нужно создавать экземпляр какого-либо класса анимации или обрабатывать прерывания. Под капотом объект анимации (а именно, экземпляр Animatable ) будет создан и запомнен в месте вызова с первым целевым значением в качестве начального значения. С этого момента каждый раз, когда вы указываете этому составному элементу другое целевое значение, автоматически запускается анимация для достижения этого значения. Если в полете уже есть анимация, она начинается с текущего значения (и скорости) и анимируется в направлении целевого значения. Во время анимации этот составной объект перекомпоновывается и возвращает обновленное значение анимации каждый кадр.

В стандартной комплектации Compose предоставляет функции animate*AsState для Float , Color , Dp , Size , Offset , Rect , Int , IntOffset и IntSize . Вы можете легко добавить поддержку других типов данных, предоставив TwoWayConverter для animateValueAsState , который принимает универсальный тип.

Вы можете настроить характеристики анимации, предоставив AnimationSpec . См. AnimationSpec для получения дополнительной информации.

Анимация нескольких свойств одновременно с переходом

Transition управляет одной или несколькими анимациями как дочерними и запускает их одновременно между несколькими состояниями.

Состояния могут иметь любой тип данных. Во многих случаях вы можете использовать собственный тип enum для обеспечения безопасности типов, как в этом примере:

enum class BoxState {
    Collapsed,
    Expanded
}

updateTransition создает и запоминает экземпляр Transition и обновляет его состояние.

var currentState by remember { mutableStateOf(BoxState.Collapsed) }
val transition = updateTransition(currentState, label = "box state")

Затем вы можете использовать одну из функций расширения animate* , чтобы определить дочернюю анимацию в этом переходе. Укажите целевые значения для каждого из состояний. Эти функции animate* возвращают значение анимации, которое обновляется каждый кадр во время анимации, когда состояние перехода обновляется с помощью updateTransition .

val rect by transition.animateRect(label = "rectangle") { state ->
    when (state) {
        BoxState.Collapsed -> Rect(0f, 0f, 100f, 100f)
        BoxState.Expanded -> Rect(100f, 100f, 300f, 300f)
    }
}
val borderWidth by transition.animateDp(label = "border width") { state ->
    when (state) {
        BoxState.Collapsed -> 1.dp
        BoxState.Expanded -> 0.dp
    }
}

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

val color by transition.animateColor(
    transitionSpec = {
        when {
            BoxState.Expanded isTransitioningTo BoxState.Collapsed ->
                spring(stiffness = 50f)

            else ->
                tween(durationMillis = 500)
        }
    }, label = "color"
) { state ->
    when (state) {
        BoxState.Collapsed -> MaterialTheme.colorScheme.primary
        BoxState.Expanded -> MaterialTheme.colorScheme.background
    }
}

Как только переход достигнет целевого состояния, Transition.currentState будет таким же, как Transition.targetState . Это можно использовать как сигнал о том, завершился ли переход.

Иногда нам нужно, чтобы начальное состояние отличалось от первого целевого состояния. Для этого мы можем использовать updateTransition с MutableTransitionState . Например, это позволяет нам запускать анимацию, как только код входит в композицию.

// Start in collapsed state and immediately animate to expanded
var currentState = remember { MutableTransitionState(BoxState.Collapsed) }
currentState.targetState = BoxState.Expanded
val transition = rememberTransition(currentState, label = "box state")
// ……

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

enum class DialerState { DialerMinimized, NumberPad }

@Composable
fun DialerButton(isVisibleTransition: Transition<Boolean>) {
    // `isVisibleTransition` spares the need for the content to know
    // about other DialerStates. Instead, the content can focus on
    // animating the state change between visible and not visible.
}

@Composable
fun NumberPad(isVisibleTransition: Transition<Boolean>) {
    // `isVisibleTransition` spares the need for the content to know
    // about other DialerStates. Instead, the content can focus on
    // animating the state change between visible and not visible.
}

@Composable
fun Dialer(dialerState: DialerState) {
    val transition = updateTransition(dialerState, label = "dialer state")
    Box {
        // Creates separate child transitions of Boolean type for NumberPad
        // and DialerButton for any content animation between visible and
        // not visible
        NumberPad(
            transition.createChildTransition {
                it == DialerState.NumberPad
            }
        )
        DialerButton(
            transition.createChildTransition {
                it == DialerState.DialerMinimized
            }
        )
    }
}

Используйте переход с AnimatedVisibility и AnimatedContent

AnimatedVisibility и AnimatedContent доступны как функции расширения Transition . targetState для Transition.AnimatedVisibility и Transition.AnimatedContent является производным от Transition и запускает переходы входа/выхода по мере необходимости, когда targetState Transition изменяется. Эти функции расширения позволяют поднимать в Transition все анимации входа/выхода/sizeTransform, которые в противном случае были бы внутренними для AnimatedVisibility / AnimatedContent . С помощью этих функций расширения изменение состояния AnimatedVisibility / AnimatedContent можно наблюдать снаружи. Вместо логического visible параметра эта версия AnimatedVisibility принимает лямбда-выражение, которое преобразует целевое состояние родительского перехода в логическое значение.

Подробности см. в разделах AnimatedVisibility и AnimatedContent .

var selected by remember { mutableStateOf(false) }
// Animates changes when `selected` is changed.
val transition = updateTransition(selected, label = "selected state")
val borderColor by transition.animateColor(label = "border color") { isSelected ->
    if (isSelected) Color.Magenta else Color.White
}
val elevation by transition.animateDp(label = "elevation") { isSelected ->
    if (isSelected) 10.dp else 2.dp
}
Surface(
    onClick = { selected = !selected },
    shape = RoundedCornerShape(8.dp),
    border = BorderStroke(2.dp, borderColor),
    shadowElevation = elevation
) {
    Column(
        modifier = Modifier
            .fillMaxWidth()
            .padding(16.dp)
    ) {
        Text(text = "Hello, world!")
        // AnimatedVisibility as a part of the transition.
        transition.AnimatedVisibility(
            visible = { targetSelected -> targetSelected },
            enter = expandVertically(),
            exit = shrinkVertically()
        ) {
            Text(text = "It is fine today.")
        }
        // AnimatedContent as a part of the transition.
        transition.AnimatedContent { targetState ->
            if (targetState) {
                Text(text = "Selected")
            } else {
                Icon(imageVector = Icons.Default.Phone, contentDescription = "Phone")
            }
        }
    }
}

Инкапсулируйте переход и сделайте его многоразовым

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

Вы можете сделать это, создав класс, содержащий все значения анимации, и функцию «обновления», которая возвращает экземпляр этого класса. Реализация перехода может быть выделена в новую отдельную функцию. Этот шаблон полезен, когда необходимо централизовать логику анимации или сделать сложную анимацию многоразовой.

enum class BoxState { Collapsed, Expanded }

@Composable
fun AnimatingBox(boxState: BoxState) {
    val transitionData = updateTransitionData(boxState)
    // UI tree
    Box(
        modifier = Modifier
            .background(transitionData.color)
            .size(transitionData.size)
    )
}

// Holds the animation values.
private class TransitionData(
    color: State<Color>,
    size: State<Dp>
) {
    val color by color
    val size by size
}

// Create a Transition and return its animation values.
@Composable
private fun updateTransitionData(boxState: BoxState): TransitionData {
    val transition = updateTransition(boxState, label = "box state")
    val color = transition.animateColor(label = "color") { state ->
        when (state) {
            BoxState.Collapsed -> Color.Gray
            BoxState.Expanded -> Color.Red
        }
    }
    val size = transition.animateDp(label = "size") { state ->
        when (state) {
            BoxState.Collapsed -> 64.dp
            BoxState.Expanded -> 128.dp
        }
    }
    return remember(transition) { TransitionData(color, size) }
}

Создайте бесконечно повторяющуюся анимацию с помощью rememberInfiniteTransition

InfiniteTransition содержит одну или несколько дочерних анимаций, таких как Transition , но анимации начинают выполняться, как только они входят в композицию, и не останавливаются, пока не будут удалены. Вы можете создать экземпляр InfiniteTransition с помощью rememberInfiniteTransition . Дочерние анимации можно добавить с помощью animateColor , animatedFloat или animatedValue . Вам также необходимо указать InfinRepeatable , чтобы указать спецификации анимации.

val infiniteTransition = rememberInfiniteTransition(label = "infinite")
val color by infiniteTransition.animateColor(
    initialValue = Color.Red,
    targetValue = Color.Green,
    animationSpec = infiniteRepeatable(
        animation = tween(1000, easing = LinearEasing),
        repeatMode = RepeatMode.Reverse
    ),
    label = "color"
)

Box(
    Modifier
        .fillMaxSize()
        .background(color)
)

Низкоуровневые API-интерфейсы анимации

Все API-интерфейсы анимации высокого уровня, упомянутые в предыдущем разделе, построены на основе API-интерфейсов анимации низкого уровня.

Функции animate*AsState — это простейшие API, которые отображают мгновенное изменение значения как значение анимации. Он поддерживается Animatable — API на основе сопрограмм для анимации одного значения. updateTransition создает объект перехода, который может управлять несколькими значениями анимации и запускать их в зависимости от изменения состояния. rememberInfiniteTransition аналогичен, но он создает бесконечный переход, который может управлять несколькими анимациями, которые продолжают выполняться бесконечно. Все эти API являются составными, за исключением Animatable , что означает, что эти анимации можно создавать вне композиции.

Все эти API основаны на более фундаментальном API Animation . Хотя большинство приложений не взаимодействуют напрямую с Animation , некоторые возможности настройки Animation доступны через API более высокого уровня. Дополнительные сведения о AnimationVector и AnimationSpec см. в разделе Настройка анимации .

Диаграмма, показывающая взаимосвязь между различными API низкоуровневой анимации.

Animatable : анимация одного значения на основе сопрограммы.

Animatable — это держатель значения, который может анимировать значение при его изменении с помощью animateTo . Это API, поддерживающий реализацию animate*AsState . Это обеспечивает последовательное продолжение и взаимоисключаемость, а это означает, что изменение значений всегда происходит непрерывно, и любая текущая анимация будет отменена.

Многие функции Animatable , включая animateTo , предоставляются в виде функций приостановки. Это означает, что их необходимо обернуть в соответствующую область сопрограммы. Например, вы можете использовать компонуемый объект LaunchedEffect для создания области только на время действия указанного значения ключа.

// Start out gray and animate to green/red based on `ok`
val color = remember { Animatable(Color.Gray) }
LaunchedEffect(ok) {
    color.animateTo(if (ok) Color.Green else Color.Red)
}
Box(
    Modifier
        .fillMaxSize()
        .background(color.value)
)

В приведенном выше примере мы создаем и запоминаем экземпляр Animatable с начальным значением Color.Gray . В зависимости от значения логического флага ok , цвет анимируется либо Color.Green , либо Color.Red . Любое последующее изменение логического значения запускает анимацию другого цвета. Если при изменении значения присутствует продолжающаяся анимация, анимация отменяется, а новая анимация начинается с текущего значения снимка с текущей скоростью.

Это реализация анимации, которая поддерживает API animate*AsState упомянутый в предыдущем разделе. По сравнению с animate*AsState , использование Animatable напрямую дает нам более детальный контроль в нескольких аспектах. Во-первых, Animatable может иметь начальное значение, отличное от первого целевого значения. Например, в приведенном выше примере кода сначала отображается серый прямоугольник, который сразу же начинает меняться на зеленый или красный цвет. Во-вторых, Animatable предоставляет больше операций со значением содержимого, а именно snapTo и animateDecay . snapTo немедленно устанавливает текущее значение на целевое. Это полезно, когда сама анимация не является единственным источником истины и ее необходимо синхронизировать с другими состояниями, такими как события касания. animateDecay запускает анимацию, которая замедляется с заданной скорости. Это полезно для реализации поведения бросания. Дополнительные сведения см. в разделе Жесты и анимация .

По умолчанию Animatable поддерживает Float и Color , но можно использовать любой тип данных, предоставив TwoWayConverter . См. AnimationVector для получения дополнительной информации.

Вы можете настроить характеристики анимации, предоставив AnimationSpec . См. AnimationSpec для получения дополнительной информации.

Animation : анимация с ручным управлением.

Animation — это доступный API анимации самого низкого уровня. Многие анимации, которые мы видели до сих пор, основаны на Animation. Существует два подтипа Animation : TargetBasedAnimation и DecayAnimation .

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

TargetBasedAnimation

Другие API охватывают большинство случаев использования, но использование TargetBasedAnimation напрямую позволяет вам самостоятельно контролировать время воспроизведения анимации. В приведенном ниже примере время воспроизведения TargetAnimation контролируется вручную на основе времени кадра, предоставленного withFrameNanos .

val anim = remember {
    TargetBasedAnimation(
        animationSpec = tween(200),
        typeConverter = Float.VectorConverter,
        initialValue = 200f,
        targetValue = 1000f
    )
}
var playTime by remember { mutableLongStateOf(0L) }

LaunchedEffect(anim) {
    val startTime = withFrameNanos { it }

    do {
        playTime = withFrameNanos { it } - startTime
        val animationValue = anim.getValueFromNanos(playTime)
    } while (someCustomCondition())
}

DecayAnimation

В отличие от TargetBasedAnimation , DecayAnimation не требует предоставления targetValue . Вместо этого он вычисляет свое targetValue на основе начальных условий, установленных initialVelocity и initialValue , а также предоставленного DecayAnimationSpec .

Анимации затухания часто используются после жеста броска, чтобы замедлить элементы до полной остановки. Скорость анимации начинается со значения, установленного в initialVelocityVector , и со временем замедляется.

{% дословно %} {% дословно %} {% дословно %} {% endverbatim %} ,

Анимируйте одно значение с помощью animate*AsState

Функции animate*AsState — это простейшие API-интерфейсы анимации в Compose для анимации одного значения. Вы указываете только целевое значение (или конечное значение), и API запускает анимацию от текущего значения до указанного значения.

Ниже приведен пример анимации альфа-канала с использованием этого API. Просто обернув целевое значение в animateFloatAsState , значение альфа теперь является значением анимации между предоставленными значениями (в данном случае 1f или 0.5f ).

var enabled by remember { mutableStateOf(true) }

val alpha: Float by animateFloatAsState(if (enabled) 1f else 0.5f, label = "alpha")
Box(
    Modifier
        .fillMaxSize()
        .graphicsLayer(alpha = alpha)
        .background(Color.Red)
)

Обратите внимание, что вам не нужно создавать экземпляр какого-либо класса анимации или обрабатывать прерывания. Под капотом объект анимации (а именно, экземпляр Animatable ) будет создан и запомнен в месте вызова с первым целевым значением в качестве начального значения. С этого момента каждый раз, когда вы указываете этому составному объекту другое целевое значение, автоматически запускается анимация для достижения этого значения. Если в полете уже есть анимация, анимация начинается с его текущего значения (и скорости) и анимирует целевое значение. Во время анимации этот композитный перекачивается и возвращает обновленное анимационное значение каждый кадр.

Из коробки Compose обеспечивает animate*AsState функции для Float , Color , Dp , Size , Offset , Rect , Int , IntOffset и IntSize . Вы можете легко добавить поддержку другим типам данных, предоставляя animateValueAsState общий тип TwoWayConverter , который занимает общий тип.

Вы можете настроить спецификации анимации, предоставив AnimationSpec . Смотрите AnimationSpec для получения дополнительной информации.

Анимировать множественные свойства одновременно с переходом

Transition управляет одной или несколькими анимациями в качестве детей и управляет ими одновременно между несколькими состояниями.

Государства могут быть любого типа данных. Во многих случаях вы можете использовать пользовательский тип enum для обеспечения безопасности типа, как в этом примере:

enum class BoxState {
    Collapsed,
    Expanded
}

updateTransition создает и помнит экземпляр Transition и обновляет его состояние.

var currentState by remember { mutableStateOf(BoxState.Collapsed) }
val transition = updateTransition(currentState, label = "box state")

Затем вы можете использовать одну из функций animate* Extension для определения дочерней анимации в этом переходе. Укажите целевые значения для каждого из состояний. Эти animate* функции возвращают анимационное значение, которое обновляется каждый кадр во время анимации, когда переходное состояние обновляется с помощью updateTransition .

val rect by transition.animateRect(label = "rectangle") { state ->
    when (state) {
        BoxState.Collapsed -> Rect(0f, 0f, 100f, 100f)
        BoxState.Expanded -> Rect(100f, 100f, 300f, 300f)
    }
}
val borderWidth by transition.animateDp(label = "border width") { state ->
    when (state) {
        BoxState.Collapsed -> 1.dp
        BoxState.Expanded -> 0.dp
    }
}

При желании вы можете передать параметр transitionSpec , чтобы указать различные AnimationSpec для каждой из комбинаций изменений переходного состояния. Смотрите AnimationSpec для получения дополнительной информации.

val color by transition.animateColor(
    transitionSpec = {
        when {
            BoxState.Expanded isTransitioningTo BoxState.Collapsed ->
                spring(stiffness = 50f)

            else ->
                tween(durationMillis = 500)
        }
    }, label = "color"
) { state ->
    when (state) {
        BoxState.Collapsed -> MaterialTheme.colorScheme.primary
        BoxState.Expanded -> MaterialTheme.colorScheme.background
    }
}

После того, как переход наступил в целевое состояние, Transition.currentState будет таким же, как Transition.targetState . Это можно использовать в качестве сигнала для того, закончился ли переход.

Иногда мы хотим иметь начальное состояние, отличное от первого целевого состояния. Мы можем использовать updateTransition с MutableTransitionState для достижения этого. Например, это позволяет нам начать анимацию, как только код входит в композицию.

// Start in collapsed state and immediately animate to expanded
var currentState = remember { MutableTransitionState(BoxState.Collapsed) }
currentState.targetState = BoxState.Expanded
val transition = rememberTransition(currentState, label = "box state")
// ……

Для более сложного перехода, включающего несколько композиционных функций, вы можете использовать createChildTransition для создания ребенка. Этот метод полезен для разделения проблем между несколькими подкомпонентами в сложном композитном. Родительский переход будет знать обо всех анимационных ценностях в детских переходах.

enum class DialerState { DialerMinimized, NumberPad }

@Composable
fun DialerButton(isVisibleTransition: Transition<Boolean>) {
    // `isVisibleTransition` spares the need for the content to know
    // about other DialerStates. Instead, the content can focus on
    // animating the state change between visible and not visible.
}

@Composable
fun NumberPad(isVisibleTransition: Transition<Boolean>) {
    // `isVisibleTransition` spares the need for the content to know
    // about other DialerStates. Instead, the content can focus on
    // animating the state change between visible and not visible.
}

@Composable
fun Dialer(dialerState: DialerState) {
    val transition = updateTransition(dialerState, label = "dialer state")
    Box {
        // Creates separate child transitions of Boolean type for NumberPad
        // and DialerButton for any content animation between visible and
        // not visible
        NumberPad(
            transition.createChildTransition {
                it == DialerState.NumberPad
            }
        )
        DialerButton(
            transition.createChildTransition {
                it == DialerState.DialerMinimized
            }
        )
    }
}

Используйте переход с помощью AnimatedVisibility и AnimatedContent

AnimatedVisibility и AnimatedContent доступны в качестве функций расширения Transition . targetState для Transition.AnimatedVisibility Анимированнаявисность и Transition.AnimatedContent контент получен из Transition и запускает переходы enter/exit по мере необходимости, когда изменяется targetState Transition . Эти функции расширения позволяют всем анимациям Enter/Exit/Sizetransform, которые в противном случае были бы внутренними для AnimatedVisibility / AnimatedContent , чтобы поднять в Transition . С этими функциями расширения изменение состояния AnimatedVisibility / AnimatedContent может наблюдаться снаружи. Вместо логического visible параметра эта версия AnimatedVisibility принимает лямбду, которая преобразует целевое состояние родительского перехода в логическое.

Смотрите Animatedvisibility и AnimatedContent для деталей.

var selected by remember { mutableStateOf(false) }
// Animates changes when `selected` is changed.
val transition = updateTransition(selected, label = "selected state")
val borderColor by transition.animateColor(label = "border color") { isSelected ->
    if (isSelected) Color.Magenta else Color.White
}
val elevation by transition.animateDp(label = "elevation") { isSelected ->
    if (isSelected) 10.dp else 2.dp
}
Surface(
    onClick = { selected = !selected },
    shape = RoundedCornerShape(8.dp),
    border = BorderStroke(2.dp, borderColor),
    shadowElevation = elevation
) {
    Column(
        modifier = Modifier
            .fillMaxWidth()
            .padding(16.dp)
    ) {
        Text(text = "Hello, world!")
        // AnimatedVisibility as a part of the transition.
        transition.AnimatedVisibility(
            visible = { targetSelected -> targetSelected },
            enter = expandVertically(),
            exit = shrinkVertically()
        ) {
            Text(text = "It is fine today.")
        }
        // AnimatedContent as a part of the transition.
        transition.AnimatedContent { targetState ->
            if (targetState) {
                Text(text = "Selected")
            } else {
                Icon(imageVector = Icons.Default.Phone, contentDescription = "Phone")
            }
        }
    }
}

Инкапсулировать переход и сделать его многоразовым

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

Вы можете сделать это, создав класс, который содержит все значения анимации и функцию «обновления», которая возвращает экземпляр этого класса. Реализация перехода может быть извлечена в новую отдельную функцию. Этот шаблон полезен, когда существует необходимость централизовать логику анимации или сделать сложные анимации многократно.

enum class BoxState { Collapsed, Expanded }

@Composable
fun AnimatingBox(boxState: BoxState) {
    val transitionData = updateTransitionData(boxState)
    // UI tree
    Box(
        modifier = Modifier
            .background(transitionData.color)
            .size(transitionData.size)
    )
}

// Holds the animation values.
private class TransitionData(
    color: State<Color>,
    size: State<Dp>
) {
    val color by color
    val size by size
}

// Create a Transition and return its animation values.
@Composable
private fun updateTransitionData(boxState: BoxState): TransitionData {
    val transition = updateTransition(boxState, label = "box state")
    val color = transition.animateColor(label = "color") { state ->
        when (state) {
            BoxState.Collapsed -> Color.Gray
            BoxState.Expanded -> Color.Red
        }
    }
    val size = transition.animateDp(label = "size") { state ->
        when (state) {
            BoxState.Collapsed -> 64.dp
            BoxState.Expanded -> 128.dp
        }
    }
    return remember(transition) { TransitionData(color, size) }
}

Создать бесконечно повторяющуюся анимацию с rememberInfiniteTransition

InfiniteTransition содержит одну или несколько дочерних анимаций, таких как Transition , но анимации начинают работать, как только они входят в композицию и не останавливаются, если они не будут удалены. Вы можете создать экземпляр InfiniteTransition с rememberInfiniteTransition . Детская анимация может быть добавлена ​​с помощью animateColor , animatedFloat или animatedValue . Вам также необходимо указать InfiniteRepeatable , чтобы указать спецификации анимации.

val infiniteTransition = rememberInfiniteTransition(label = "infinite")
val color by infiniteTransition.animateColor(
    initialValue = Color.Red,
    targetValue = Color.Green,
    animationSpec = infiniteRepeatable(
        animation = tween(1000, easing = LinearEasing),
        repeatMode = RepeatMode.Reverse
    ),
    label = "color"
)

Box(
    Modifier
        .fillMaxSize()
        .background(color)
)

API-интерфейсы анимации низкого уровня

Все высокоуровневые анимационные API, упомянутые в предыдущем разделе, построены на основе API-интерфейсов анимации низкого уровня.

Функции animate*AsState являются самыми простыми API, которые делают мгновенное изменение значения в качестве значения анимации. Он поддерживается Animatable , который представляет собой API на основе Coroutine для анимации одного значения. updateTransition создает объект перехода, который может управлять несколькими анимирующими значениями и запускать их на основе изменения состояния. rememberInfiniteTransition что InfiniteTransition похожа, но он создает бесконечный переход, который может управлять несколькими анимациями, которые продолжают работать на неопределенный срок. Все эти API являются композиционными, за исключением Animatable , что означает, что эти анимации могут быть созданы вне композиции.

Все эти API основаны на более фундаментальной Animation API. Хотя большинство приложений не будут напрямую взаимодействовать с Animation , некоторые возможности настройки Animation доступны с помощью API более высокого уровня. См. Настройте анимацию для получения дополнительной информации о AnimationVector и AnimationSpec .

Диаграмма показывает взаимосвязь между различными анимационными API низкого уровня

Animatable : анимация с одной стоимостью на основе Coroutine

Animatable - это держатель стоимости, который может анимировать ценность, поскольку она изменяется через animateTo . Это API, поддерживающая реализацию animate*AsState . Это обеспечивает последовательное продолжение и взаимную исключительность, что означает, что изменение ценности всегда непрерывное, и любая постоянная анимация будет отменена.

Многие функции Animatable , включая animateTo , предоставляются в качестве функций приостановки. Это означает, что они должны быть завернуты в соответствующую сферу скручивания. Например, вы можете использовать Composable LaunchedEffect для создания прицела только для продолжительности указанного значения ключа.

// Start out gray and animate to green/red based on `ok`
val color = remember { Animatable(Color.Gray) }
LaunchedEffect(ok) {
    color.animateTo(if (ok) Color.Green else Color.Red)
}
Box(
    Modifier
        .fillMaxSize()
        .background(color.value)
)

В приведенном выше примере мы создаем и запоминаем экземпляр Animatable с начальным значением Color.Gray . В зависимости от значения логического флага ok , цвет оживляет или Color.Green или Color.Red . Любое последующее изменение логического значения начинает анимацию для другого цвета. Если есть постоянная анимация при изменении значения, анимация отменяется, и новая анимация начинается с текущего значения снимка с током скоростью.

Это реализация анимации, которая подтверждает animate*AsState API, упомянутый в предыдущем разделе. По сравнению с animate*AsState , использование Animatable напрямую дает нам более тонкий контроль над несколькими отношениями. Во -первых, Animatable может иметь начальное значение, отличное от своего первого целевого значения. Например, пример кода выше показывает серую коробку, которая сразу же начинает анимировать как зеленый, либо красный. Во -вторых, Animatable обеспечивает больше операций по значению контента, а именно snapTo и animateDecay . snapTo устанавливает текущее значение немедленно целевому значению. Это полезно, когда сама анимация не является единственным источником истины и должна синхронизироваться с другими состояниями, такими как события прикосновения. animateDecay начинает анимацию, которая замедляется с заданной скорости. Это полезно для реализации поведения фри. Смотрите жест и анимацию для получения дополнительной информации.

Из коробки, Animatable поддерживают Float и Color , но любой тип данных можно использовать, предоставляя TwoWayConverter . Смотрите анимационную версию для получения дополнительной информации.

Вы можете настроить спецификации анимации, предоставив AnimationSpec . Смотрите AnimationSpec для получения дополнительной информации.

Animation : анимация вручную контролируется

Animation -это API анимации с самым низким уровнем. Многие из анимаций, которые мы видели до сих пор, строят анимацию. Существует два подтипа Animation : TargetBasedAnimation и DecayAnimation .

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

TargetBasedAnimation

Другие API -интерфейсы охватывают большинство вариантов использования, но использование TargetBasedAnimation напрямую позволяет вам контролировать время игры в анимации самостоятельно. В приведенном ниже примере время воспроизведения TargetAnimation контролируется вручную на основе времени кадра, предоставленного withFrameNanos .

val anim = remember {
    TargetBasedAnimation(
        animationSpec = tween(200),
        typeConverter = Float.VectorConverter,
        initialValue = 200f,
        targetValue = 1000f
    )
}
var playTime by remember { mutableLongStateOf(0L) }

LaunchedEffect(anim) {
    val startTime = withFrameNanos { it }

    do {
        playTime = withFrameNanos { it } - startTime
        val animationValue = anim.getValueFromNanos(playTime)
    } while (someCustomCondition())
}

DecayAnimation

В отличие от TargetBasedAnimation , DecayAnimation не требует предоставления targetValue . Вместо этого он вычисляет свой targetValue на основе начальных условий, установленных initialVelocity и initialValue и предоставленного DecayAnimationSpec .

Анимации распада часто используются после жеста, чтобы замедлить элементы до остановки. Скорость анимации начинается с значения, установленного initialVelocityVector и замедляется с течением времени.

{% дословно %} {% дословно %} {% дословно %} {% дословно %}