Animacje oparte na wartości

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

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

Poniżej znajdziesz przykład animacji w wersji alfa za pomocą tego interfejsu API. Dzięki prostemu pakowaniu wartości docelowej w obiekt animateFloatAsState wartość alfa jest teraz 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)
Box(
    Modifier.fillMaxSize()
        .graphicsLayer(alpha = alpha)
        .background(Color.Red)
)

Pamiętaj, że nie musisz tworzyć instancji żadnej klasy animacji ani obsługiwać przerwania. Podstawowy obiekt animacji (tj. wystąpienie Animatable) zostanie utworzony i zapamiętany w witrynie wywołania, przy czym pierwsza wartość docelowa będzie wartością początkową. Od tej pory za każdym razem, gdy podasz w funkcji kompozycyjnej inną wartość docelową, automatycznie rozpocznie się animacja uwzględniająca tę wartość. Jeśli masz już animację w trakcie wyświetlania, animacja rozpoczyna się od jej bieżącej wartości (i prędkości) i przesuwa się w kierunku wartości docelowej. W trakcie animacji funkcja kompozycyjna jest tworzona ponownie i w każdej klatce zwraca zaktualizowaną wartość animacji.

Funkcja tworzenia wiadomości zapewnia gotowe funkcje animate*AsState związane z: Float, Color, Dp, Size, Offset, Rect, Int, IntOffset iIntSize. Możesz łatwo dodać obsługę innych typów danych, podając typ TwoWayConverter animateValueAsState o typie ogólnym.

Specyfikację animacji możesz dostosować, przesyłając AnimationSpec. Więcej informacji znajdziesz w sekcji AnimationSpec.

Animuj wiele usług jednocześnie z przejściem

Transition zarządza co najmniej 1 animacją jako jej elementami podrzędnymi i uruchamia je jednocześnie między wieloma stanami.

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

enum class BoxState {
    Collapsed,
    Expanded
}

updateTransition tworzy i pamięta instancję Transition, a następnie aktualizuje jej stan.

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

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

Po osiągnięciu stanu docelowego wartość Transition.currentState będzie taka sama jak wartość Transition.targetState. W ten sposób pokażesz, czy przenoszenie się zakończyło.

Czasami chcemy, aby początkowy stan różnił się od pierwszego. Możemy to osiągnąć za pomocą updateTransition z elementem MutableTransitionState. Pozwalają np. uruchamiać animację od razu po wprowadzeniu kodu do kompozycji.

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

Do bardziej złożonego przejścia obejmującego wiele funkcji kompozycyjnych możesz utworzyć przejście podrzędne za pomocą createChildTransition. Ta technika jest przydatna do rozdzielania wątpliwości na wiele podkomponentów w złożonym elemencie kompozycyjnym. 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 elementami AnimatedVisibility i AnimatedContent

AnimatedVisibility i AnimatedContent są dostępne jako funkcje rozszerzenia Transition. Parametr targetState dla właściwości Transition.AnimatedVisibility i Transition.AnimatedContent pochodzi z parametru Transition i w razie potrzeby aktywuje przejścia wejścia/wyjścia po zmianie elementu targetState Transition. Te funkcje rozszerzenia umożliwiają umieszczanie w obrębie funkcji Transition wszystkich animacji Enter/Exit/sizeTransform, które w innym przypadku byłyby wewnętrzne w obrębie AnimatedVisibility/AnimatedContent. Dzięki tym funkcjom rozszerzeń zmianę stanu AnimatedVisibility/AnimatedContent można obserwować z zewnątrz. Zamiast logicznego parametru visible ta wersja obiektu AnimatedVisibility używa funkcji lambda, która konwertuje docelowy stan przejścia nadrzędnego na wartość logiczną.

Szczegółowe informacje znajdziesz w sekcjach AnimatedWidoczność i 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),
    elevation = 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")
            }
        }
    }
}

Otocz przejścia i umożliw jego ponowne wykorzystanie

W prostych przypadkach bardzo poprawnym rozwiązaniem jest zdefiniowanie animacji przejść w ramach tego samego elementu kompozycyjnego co interfejs użytkownika. Przy pracy nad złożonym komponentem z wieloma animowanymi wartościami może Ci się jednak przydać oddzielenie implementacji animacji od interfejsu funkcji kompozycyjnej.

Możesz to zrobić, tworząc klasę, która zawiera wszystkie wartości animacji, i funkcję „update”, która zwraca instancję tej klasy. Implementację przejścia można wyodrębnić do nowej, osobnej funkcji. Ten wzorzec jest przydatny, gdy trzeba scentralizować działanie logiki animacji lub umożliwić wielokrotne korzystanie ze 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ńczoną powtarzającą się animację w aplikacji rememberInfiniteTransition

InfiniteTransition zawiera co najmniej jedną animację podrzędną, np. Transition, ale po wejściu do kompozycji animacje zaczyna wyświetlać się po wejściu w kompozycję i nie zatrzymuje się, dopóki nie zostanie usunięta. Instancję InfiniteTransition można utworzyć za pomocą rememberInfiniteTransition. Animacje podrzędne można dodawać za pomocą właściwości animateColor, animatedFloat lub animatedValue. Musisz też podać właściwość infiniteRepeatable, by określić specyfikacje animacji.

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

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

Interfejsy API animacji niskiego poziomu

Wszystkie interfejsy API animacji wysokiego poziomu wymienione w poprzedniej sekcji są tworzone na podstawie niskopoziomowych interfejsów API animacji.

Funkcje animate*AsState to najprostsze interfejsy API, które renderują natychmiastową zmianę wartości jako wartości animacji. Jest wspierana przez interfejs API Animatable, który jest oparty na współtworzonych interfejsach API do animowania pojedynczej wartości. updateTransition tworzy obiekt przejścia, który może zarządzać wieloma wartościami animowanymi i uruchamiać je po zmianie stanu. rememberInfiniteTransition działa podobnie, ale tworzy nieskończone przejście, które może zarządzać wieloma animacjami wyświetlanymi bez końca. Wszystkie te interfejsy API są funkcjami kompozycyjnymi, z wyjątkiem interfejsu Animatable, który oznacza, że te animacje można tworzyć poza kompozycją.

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

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

Animatable: animacja z jedną wartością opartą na korutynach

Animatable to element przechowywania wartości, który może animować wartość, gdy zmienia się ona w funkcji animateTo. To jest interfejs API, który tworzy kopię zapasową implementacji animate*AsState. Zapewnia spójność ciągłości i wzajemnego wyłączności, co oznacza, że zmiana wartości jest zawsze ciągła, a każda trwająca animacja zostaje anulowana.

Wiele funkcji Animatable, w tym animateTo, jest dostępnych jako funkcje zawieszania. Oznacza to, że należy je opakować w odpowiedni zakres koordynacyjny. Możesz np. użyć funkcji kompozycyjnej LaunchedEffect, aby utworzyć zakres ograniczony do czasu 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 przykładzie powyżej tworzymy i zapamiętujemy wystąpienie Animatable z wartością początkową Color.Gray. W zależności od wartości flagi wartości logicznej ok kolor zmienia się na Color.Green lub Color.Red. Każda kolejna zmiana tej wartości rozpoczyna animację nowego koloru. Jeśli w momencie zmiany wartości trwa animacja, animacja zostanie anulowana, a nowa animacja rozpocznie się od bieżącej wartości zrzutu z bieżącą prędkością.

To implementacja animacji, która tworzy kopię zapasową interfejsu animate*AsState API wspomnianego w poprzedniej sekcji. W porównaniu z modelem animate*AsState użycie danych Animatable bezpośrednio daje nam precyzyjną kontrolę w kilku aspektach. Po pierwsze Animatable może mieć wartość początkową inną niż pierwsza wartość docelowa. Na przykład w podanym wyżej kodzie najpierw wyświetla się szare pole, które natychmiast zaczyna animować się w kolorach zielonych lub czerwonych. Po drugie Animatable obsługuje 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. ze zdarzeniami dotknięcia. animateDecay uruchamia animację, która zwalnia z podanej prędkości. Przydaje się to do implementowania działania przerzucania. Więcej informacji znajdziesz w sekcji Gesty i animacja.

Gotowe do użycia funkcje Animatable obsługują Float i Color, ale możesz używać dowolnego typu danych, podając parametr TwoWayConverter. Więcej informacji znajdziesz w sekcji AnimationVector.

Specyfikację animacji możesz dostosować, dodając parametr AnimationSpec. Więcej informacji znajdziesz w sekcji AnimationSpec.

Animation: animacja sterowana ręcznie

Animation to najniższy dostępny poziom interfejsu Animation API. Wiele animacji, które widzieliśmy do tej pory, opiera się na animacji. Istnieją 2 podtypy Animation: TargetBasedAnimation i DecayAnimation.

Typu Animation należy używać tylko do ręcznego kontrolowania czasu animacji. Animation jest bezstanowy i nie ma żadnego pojęcia cyklu życia. Działa jako mechanizm obliczania animacji wykorzystywany przez interfejsy API wyższego poziomu.

TargetBasedAnimation

Inne interfejsy API mają zastosowanie do większości przypadków użycia, ale użycie bezpośrednio interfejsu TargetBasedAnimation pozwala samodzielnie kontrolować czas odtwarzania animacji. W poniższym przykładzie czas odtwarzania obiektu TargetAnimation jest ustawiany ręcznie na podstawie czasu renderowania klatki przez withFrameNanos.

val anim = remember {
    TargetBasedAnimation(
        animationSpec = tween(200),
        typeConverter = Float.VectorConverter,
        initialValue = 200f,
        targetValue = 1000f
    )
}
var playTime by remember { mutableStateOf(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 panel DecayAnimation nie wymaga wartości targetValue. Zamiast tego oblicza targetValue na podstawie warunków początkowych wyznaczonych przez initialVelocity i initialValue oraz podany parametr DecayAnimationSpec.

Animacje o słabości są często stosowane po geście przesuwania, by spowolnić elementy do zatrzymania. Prędkość animacji rozpoczyna się od wartości określonej przez initialVelocityVector i maleje w miarę upływu czasu.