Animacje oparte na wartości

Animowanie pojedynczej wartości za pomocą funkcji animate*AsState

Funkcje animate*AsState to najprostsze interfejsy API animacji w Compose do animowania pojedynczej wartości. Podajesz tylko wartość docelową (lub końcową), a interfejs API rozpoczyna animację od bieżącej wartości do podanej wartości.

Poniżej znajdziesz przykład animowania przezroczystości za pomocą tego interfejsu API. Wystarczy, że wartość docelową umieścisz w animateFloatAsState, a wartość alfa stanie się wartością animacji pomiędzy podanymi wartościami (w tym przypadku 1f lub 0.5f).

var enabled by remember { mutableStateOf(true) }

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

Pamiętaj, że nie musisz tworzyć instancji żadnej klasy animacji ani obsługiwać przerwań. W tle zostanie utworzony obiekt animacji (czyli instancja Animatable), który zostanie zapamiętany w miejscu wywołania, a jego wartością początkową będzie pierwsza wartość docelowa. Od tego momentu za każdym razem, gdy podasz temu komponentowi inną wartość docelową, automatycznie rozpocznie się animacja w kierunku tej wartości. Jeśli animacja jest już w toku, zaczyna się od bieżącej wartości (i prędkości) i przechodzi do wartości docelowej. Podczas animacji ten komponent kompozycyjny jest ponownie komponowany i zwraca zaktualizowaną wartość animacji w każdej klatce.

Compose udostępnia gotowe funkcje animate*AsState dla języków Float, Color, Dp, Size, Offset, Rect, Int, IntOffsetIntSize. Możesz łatwo dodać obsługę innych typów danych, podając TwoWayConverter do animateValueAsState, które przyjmuje typ ogólny.

Możesz dostosować specyfikacje animacji, podając AnimationSpec. Więcej informacji znajdziesz w sekcji AnimationSpec.

Animowanie wielu właściwości jednocześnie za pomocą przejścia

Transition zarządza co najmniej jedną animacją jako elementem podrzędnym i uruchamia je jednocześnie w różnych stanach.

Stany mogą być dowolnego typu danych. W wielu przypadkach możesz użyć niestandardowego enum typu, aby zapewnić bezpieczeństwo typów, jak w tym przykładzie:

enum class BoxState {
    Collapsed,
    Expanded
}

updateTransition tworzy i zapamiętuje instancję Transition oraz aktualizuje jej stan.

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

Następnie możesz użyć jednej z funkcji rozszerzenia animate*, aby zdefiniować animację podrzędną w tym przejściu. Określ wartości docelowe dla każdego stanu. Te animate* funkcje zwracają wartość animacji, która jest aktualizowana w każdej klatce podczas animacji, gdy stan przejścia jest aktualizowany za pomocą 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
    }
}

Opcjonalnie możesz przekazać parametr transitionSpec, aby określić inny element AnimationSpec dla każdej kombinacji zmian stanu przejścia. Więcej informacji znajdziesz w sekcji 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
    }
}

Gdy przejście osiągnie stan docelowy, Transition.currentState będzie takie samo jak Transition.targetState. Może to być sygnał informujący o zakończeniu przejścia.

Czasami chcemy, aby stan początkowy różnił się od stanu pierwszego celu. Możemy to osiągnąć, używając updateTransitionMutableTransitionState. Umożliwia to na przykład rozpoczęcie animacji natychmiast po wejściu kodu do kompozycji.

// 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")
// ……

W przypadku bardziej złożonego przejścia obejmującego wiele funkcji kompozycyjnych możesz użyć createChildTransition, aby utworzyć przejście podrzędne. Ta technika jest przydatna do rozdzielania zadań między wieloma podkomponentami w złożonym komponencie. Przejście nadrzędne będzie uwzględniać wszystkie wartości animacji w przejściach podrzędnych.

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
            }
        )
    }
}

Używanie przejścia z AnimatedVisibility i AnimatedContent

AnimatedVisibilityAnimatedContent są dostępne jako funkcje rozszerzające Transition. Wartości targetState dla Transition.AnimatedVisibility i Transition.AnimatedContent są wyliczane na podstawie wartości Transition, a przejścia wejścia/wyjścia są wywoływane w razie potrzeby, gdy zmieni się wartość targetState elementu Transition. Te funkcje rozszerzające umożliwiają przeniesienie do elementu Transition wszystkich animacji wejścia/wyjścia/przekształcenia rozmiaru, które w przeciwnym razie byłyby wewnętrzne dla elementu AnimatedVisibility/AnimatedContent. Dzięki tym funkcjom rozszerzenia można obserwować zmianę stanu urządzenia AnimatedVisibility/AnimatedContent z zewnątrz. Zamiast parametru logicznego visible ta wersja funkcji AnimatedVisibility przyjmuje funkcję lambda, która przekształca stan docelowy przejścia nadrzędnego w wartość logiczną.

Szczegółowe informacje znajdziesz w sekcjach AnimatedVisibilityAnimatedContent.

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")
            }
        }
    }
}

Enkapsulacja przejścia i możliwość ponownego użycia

W przypadku prostych zastosowań zdefiniowanie animacji przejścia w tym samym komponencie, co interfejs, jest w pełni prawidłową opcją. Jeśli jednak pracujesz nad złożonym komponentem z wieloma animowanymi wartościami, możesz chcieć oddzielić implementację animacji od komponentu interfejsu.

Możesz to zrobić, tworząc klasę, która zawiera wszystkie wartości animacji, oraz funkcję „update”, która zwraca instancję tej klasy. Implementację przejścia można wyodrębnić do nowej, oddzielnej funkcji. Ten wzorzec jest przydatny, gdy trzeba scentralizować logikę animacji lub sprawić, aby złożone animacje były wielokrotnego użytku.

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) }
}

Tworzenie animacji powtarzanej w nieskończoność za pomocą rememberInfiniteTransition

InfiniteTransition zawiera co najmniej 1 animację podrzędną, np. Transition, ale animacje zaczynają się odtwarzać od razu po wejściu do kompozycji i nie zatrzymują się, dopóki nie zostaną usunięte. Możesz utworzyć instancję InfiniteTransitionrememberInfiniteTransition. Animacje podrzędne można dodawać za pomocą elementów animateColor, animatedFloat lub animatedValue. Musisz też określić parametr infiniteRepeatable, aby podać specyfikacje animacji.

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)
)

Interfejsy API animacji niskiego poziomu

Wszystkie interfejsy API animacji wysokiego poziomu wymienione w poprzedniej sekcji są oparte na interfejsach API animacji niskiego poziomu.

Funkcje animate*AsState to najprostsze interfejsy API, które renderują natychmiastową zmianę wartości jako wartość animacji. Jest on oparty na interfejsie Animatable, który jest interfejsem API opartym na korutynach do animowania pojedynczej wartości. updateTransition tworzy obiekt przejścia, który może zarządzać wieloma animowanymi wartościami i uruchamiać je na podstawie zmiany stanu. rememberInfiniteTransition jest podobna, ale tworzy nieskończone przejście, które może zarządzać wieloma animacjami działającymi w nieskończoność. Wszystkie te interfejsy API są funkcjami kompozycyjnymi z wyjątkiem Animatable, co oznacza, że te animacje można tworzyć poza kompozycją.

Wszystkie te interfejsy API są oparte na bardziej podstawowym interfejsie Animation API. Większość aplikacji nie będzie wchodzić w bezpośrednią interakcję z Animation, ale niektóre możliwości dostosowywania Animation są dostępne za pomocą interfejsów API wyższego poziomu. Więcej informacji o AnimationVectorAnimationSpec znajdziesz w artykule Dostosowywanie animacji.

Diagram przedstawiający relacje między różnymi interfejsami API animacji niskiego poziomu

Animatable: animacja pojedynczej wartości oparta na współprogramie

Animatable to obiekt przechowujący wartość, która może być animowana w miarę jej zmiany za pomocą animateTo. Jest to interfejs API, który obsługuje implementację animate*AsState. Zapewnia to ciągłość i wzajemną wyłączność, co oznacza, że zmiana wartości jest zawsze ciągła, a wszelkie trwające animacje zostaną anulowane.

Wiele funkcji Animatable, w tym animateTo, jest udostępnianych jako funkcje zawieszające. Oznacza to, że muszą być one umieszczone w odpowiednim zakresie korutyny. Możesz na przykład użyć funkcji LaunchedEffect, aby utworzyć zakres tylko na czas trwania określonej wartości klucza.

// 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)
)

W powyższym przykładzie tworzymy instancję Animatable z wartością początkową Color.Gray i zapamiętujemy ją. W zależności od wartości flagi logicznej ok kolor animuje się do wartości Color.Green lub Color.Red. Każda kolejna zmiana wartości logicznej rozpoczyna animację do drugiego koloru. Jeśli w momencie zmiany wartości trwa animacja, zostanie ona anulowana, a nowa animacja rozpocznie się od bieżącej wartości z bieżącą prędkością.

Jest to implementacja animacji, która obsługuje interfejs animate*AsState API wspomniany w poprzedniej sekcji. W porównaniu z animate*AsState bezpośrednie użycie Animatable daje nam większą kontrolę w kilku aspektach. Po pierwsze,Animatable może mieć wartość początkową inną niż pierwsza wartość docelowa. Na przykład w przykładzie kodu powyżej początkowo wyświetla się szare pole, które natychmiast zaczyna animować się na zielono lub czerwono. Po drugie, Animatable zapewnia więcej operacji na wartości treści, a mianowicie snapToanimateDecay. snapTo natychmiast ustawia bieżącą wartość na wartość docelową. Jest to przydatne, gdy sama animacja nie jest jedynym źródłem informacji i musi być zsynchronizowana z innymi stanami, np. ze zdarzeniami dotyku. animateDecay rozpoczyna animację, która zwalnia od podanej prędkości. Jest to przydatne do implementowania zachowania związanego z szybkim przesuwaniem. Więcej informacji znajdziesz w sekcji Gest i animacja.

Animatable obsługuje FloatColor, ale można używać dowolnego typu danych, podając TwoWayConverter. Więcej informacji znajdziesz w sekcji AnimationVector.

Możesz dostosować specyfikacje animacji, podając AnimationSpec. Więcej informacji znajdziesz w sekcji AnimationSpec.

Animation: animacja sterowana ręcznie

Animation to interfejs API animacji najniższego poziomu. Wiele animacji, które widzieliśmy do tej pory, opiera się na animacji. Wyróżniamy 2 podtypy Animation: TargetBasedAnimationDecayAnimation.

Animation należy używać tylko do ręcznego sterowania czasem trwania animacji. Animation jest bezstanowy i nie ma pojęcia cyklu życia. Jest to mechanizm obliczania animacji, z którego korzystają interfejsy API wyższego poziomu.

TargetBasedAnimation

Większość przypadków użycia jest obsługiwana przez inne interfejsy API, ale bezpośrednie użycie TargetBasedAnimation pozwala samodzielnie kontrolować czas odtwarzania animacji. W poniższym przykładzie czas odtwarzania elementu TargetAnimation jest kontrolowany ręcznie na podstawie czasu klatki podanego przez element 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

W przeciwieństwie do TargetBasedAnimation atrybut DecayAnimation nie wymaga podania wartości targetValue. Zamiast tego oblicza wartość targetValue na podstawie warunków początkowych określonych przez initialVelocityinitialValue oraz podanej wartości DecayAnimationSpec.

Animacje zanikania są często używane po wykonaniu gestu szybkiego przesunięcia, aby spowolnić elementy i zatrzymać je. Szybkość animacji zaczyna się od wartości ustawionej przez initialVelocityVector i z czasem zwalnia.