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

Анимируйте одно значение с помощью 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 , и со временем замедляется.

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