Animacje oparte na wartości

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

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

Poniżej znajdziesz przykład animowania kanału alfa za pomocą tego interfejsu API. Wystarczy owinąć wartość docelową w animateFloatAsState, aby wartość alfa stała się wartością animacji między podanymi wartościami (w tym przypadku 1f lub 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)
)

Pamiętaj, że nie musisz tworzyć instancji żadnej klasy animacji ani obsługiwać przerwania. W tle zostanie utworzony obiekt animacji (czyli instancja Animatable) i zapamiętany w miejscu wywołania, przy czym jego początkową wartością będzie pierwsza wartość docelowa. Za każdym razem, gdy podasz w tym elemencie kompozycyjnym inną wartość docelową, automatycznie rozpoczyna się animacja w kierunku tej wartości. Jeśli animacja jest już w trakcie odtwarzania, zaczyna się od bieżącej wartości (i prędkości) i przechodzi do wartości docelowej. W trakcie animacji funkcja kompozycyjna zostaje przekomponowana i zwraca nową wartość animacji po każdej klatce.

Bezpośrednio po zainstalowaniu Compose udostępnia funkcje animate*AsState dla typów danych Float, Color, Dp, Size, Offset, Rect, Int, IntOffsetIntSize. Obsługę innych typów danych możesz łatwo dodać, podając parametr TwoWayConverter dla parametru animateValueAsState, który przyjmuje typ ogólny.

Specyfikacje animacji możesz dostosować, podając AnimationSpec. Więcej informacji znajdziesz w specyfikacji AnimationSpec.

Animuj wiele właściwości jednocześnie podczas przejścia

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

Stany mogą być dowolnego typu danych. W wielu przypadkach możesz użyć niestandardowego typu enum, aby zapewnić bezpieczeństwo typu, 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 funkcje animate* zwracają wartość animacji, która jest aktualizowana każda klatka w trakcie animacji, gdy stan przejścia jest aktualizowany za pomocą parametru 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 AnimationSpec dla każdej kombinacji zmiany stanu przejścia. Więcej informacji znajdziesz w pliku 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 do stanu docelowego zostanie zakończone, Transition.currentState będzie takie samo jak Transition.targetState. Może to służyć jako sygnał, że przejście zostało zakończone.

Czasami chcemy mieć stan początkowy inny niż pierwszy stan docelowy. Możemy użyć do tego celu updateTransition i MutableTransitionState. Umożliwia to na przykład rozpoczęcie animacji, gdy tylko kod wejdzie w kompozycję.

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

Aby utworzyć bardziej złożone przejście z użyciem wielu funkcji kompozycyjnych, możesz użyć funkcji createChildTransition, aby utworzyć przejście podrzędne. Technika ta przydaje się do rozdzielania problemów między wiele podkomponentów w złożonym elemencie kompozycyjnym. Przejść nadrzędna 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żyj przejścia z elementami AnimatedVisibility i AnimatedContent

AnimatedVisibility i AnimatedContent są dostępne jako funkcje rozszerzenia funkcji Transition. targetState dla Transition.AnimatedVisibility i Transition.AnimatedContent pochodzi z Transition i w razie potrzeby aktywuje przejścia typu „wejście/wyjście” po zmianie targetState właściwości Transition. Te funkcje rozszerzeń umożliwiają wciągnięcie wszystkich animacji Enter/exit/sizeTransform, które w przeciwnym razie byłyby wewnętrzne dlaAnimatedVisibility/AnimatedContent, można wciągnąć do elementu Transition. Dzięki tym funkcjom rozszerzeń zmianę stanu funkcji AnimatedVisibility/AnimatedContent można obserwować z zewnątrz. Zamiast parametru visible wartości logicznej ta wersja funkcji AnimatedVisibility używa funkcji lambda, która przekształca docelowy stan nadrzędnego przejścia na wartość logiczną.

Szczegółowe informacje znajdziesz w artykułach 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")
            }
        }
    }
}

Opakowanie przejścia i umożliwienie jego ponownego użycia

W przypadku prostych zastosowań definiowanie animacji przejść w tym samym komponencie co interfejs użytkownika jest całkowicie wystarczające. Gdy jednak pracujesz nad złożonym komponentem z wieloma animowanymi wartościami, warto oddzielić implementację animacji od interfejsu kompozycyjnego.

Aby to zrobić, utwórz klasę zawierającą wszystkie wartości animacji oraz funkcję „update”, która zwraca instancję tej klasy. Przejście może zostać zaimplementowane w nowej, osobnej funkcji. Ten wzorzec jest przydatny, gdy trzeba scentralizować logikę animacji lub umożliwić wielokrotne wykorzystanie złożonych animacji.

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

Utwórz nieskończenie powtarzającą się animację w rememberInfiniteTransition

InfiniteTransition zawiera co najmniej 1 animację podrzędną, tak jak Transition, ale animacje zaczynają się odtwarzać, gdy tylko zostaną dodane do kompozycji, i nie przestaną, dopóki nie zostaną usunięte. Możesz utworzyć instancję InfiniteTransition za pomocą rememberInfiniteTransition. Animacje podrzędne można dodawać za pomocą atrybutów animateColor, animatedFloat lub animatedValue. Musisz też określić parametr infiniteRepeatable, aby określić 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 powodują natychmiastową zmianę wartości jako wartość animacji. Opiera się na interfejsie Animatable, który jest opartym na współprogramie interfejsem API do animowania pojedynczej wartości. updateTransition tworzy obiekt przejścia, który może zarządzać wieloma wartościami animacji i uruchamiać je na podstawie zmiany stanu. rememberInfiniteTransition działa podobnie, ale tworzy nieskończone przejście, które może zarządzać wieloma animacjami, które będą działać w nieskończoność. Wszystkie te interfejsy API są kompozycjami, z wyjątkiem interfejsu Animatable, co oznacza, że te animacje można tworzyć poza kompozycją.

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

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

Animatable: animacja pojedynczej wartości na podstawie współprogramu

Animatable to uchwyt wartości, który może animować wartość podczas jej zmiany za pomocą animateTo. To interfejs API obsługujący implementację animate*AsState. Zapewnia to spójne kontynuowanie i wykluczanie się wzajemne, co oznacza, że zmiana wartości jest zawsze ciągła, a każda trwająca animacja zostanie anulowana.

Wiele funkcji Animatable, w tym animateTo, jest dostępnych jako funkcje zawieszania. Oznacza to, że muszą być one zawinięte w odpowiednim zakresie współbieżności. Możesz na przykład użyć komponentu 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 i zapamiętujemy instancję Animatable o wartości początkowej Color.Gray. W zależności od wartości flagi logicznej ok kolor zmienia się na Color.Green lub Color.Red. Każda kolejna zmiana wartości logicznej powoduje rozpoczęcie animacji do innego koloru. Jeśli po zmianie wartości będzie wyświetlana animacja, animacja zostanie anulowana, a nowa animacja rozpocznie się z bieżącą wartością zrzutu z bieżącą prędkością.

To implementacja animacji, która tworzy kopię zapasową interfejsu API animate*AsState, o którym wspomnieliśmy w poprzedniej sekcji. W porównaniu z animate*AsState bezpośrednie korzystanie z Animatable daje nam większą kontrolę w kilku aspektach. Po pierwsze, Animatable może mieć inną wartość początkową niż pierwsza wartość docelowa. Na przykład w tym przykładzie kodu najpierw wyświetla się szare pole, które natychmiast zaczyna się animować na zielono lub na czerwono. Po drugie Animatable udostępnia więcej operacji na wartości treści, czyli snapTo i animateDecay. 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ć synchronizowana z innymi stanami, np. zdarzeniami dotknięcia. animateDecay uruchamia animację, która zwalnia z danej prędkości. Jest to przydatne przy implementowaniu zachowania przelotu. Więcej informacji znajdziesz w sekcji Gesty i animacja.

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

Specyfikacje animacji możesz dostosować, podając AnimationSpec. Więcej informacji znajdziesz w specyfikacji animacji (AnimationSpec).

Animation: animacja sterowana ręcznie

Animation to interfejs API Animation najniższego poziomu. Wiele z animacji, które do tej pory widzieliśmy, powstało na podstawie Animation. Istnieją 2 podtypy Animation: TargetBasedAnimation i DecayAnimation.

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

TargetBasedAnimation

Inne interfejsy API sprawdzają się w większości przypadków, ale użycie interfejsu TargetBasedAnimation pozwala bezpośrednio kontrolować czas odtwarzania animacji. W poniższym przykładzie czas odtwarzania obiektu TargetAnimation jest ustawiany ręcznie na podstawie czasu renderowania klatki podanego przez 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 atrybutu TargetBasedAnimation atrybut DecayAnimation nie wymaga podania wartości targetValue. Zamiast tego oblicza wartość targetValue na podstawie warunków początkowych ustawionych przez initialVelocity i initialValue oraz podanych wartości DecayAnimationSpec.

Animacje wygaszania są często używane po użyciu gestu przesunięcia, aby spowolnić elementy aż do zatrzymania. Szybkość animacji zaczyna się od wartości ustawionej przez initialVelocityVector i z czasem zwalnia.