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 przezroczystości 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. Od tego momentu za każdym razem, gdy podasz temu komponentowi inną wartość docelową, automatycznie rozpocznie się animacja do 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. Podczas animacji ten komponent jest ponownie składany i zwraca zaktualizowaną wartość animacji w każdym ujęciu.

Bezpośrednio po zainstalowaniu Compose udostępnia funkcje animate*AsState dla typów danych Float, Color, Dp, Size, Offset, Rect, Int, IntOffsetIntSize. Możesz łatwo dodać obsługę innych typów danych, 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.

Animowanie wielu właściwości jednocześnie za pomocą 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 w każdej klatce animacji, gdy stan przejścia jest aktualizowany za pomocą funkcji 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ć inną wartość AnimationSpec dla każdej z kombinacji zmian 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. Aby to osiągnąć, możemy użyć funkcji updateTransition z funkcją 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. Ta technika jest przydatna do oddzielania problemów w kilku podkomponentach złożonego komponentu. Przejście nadrzędne będzie wiedzieć o wszystkich wartościach 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 uwzględnieniem właściwości AnimatedVisibilityAnimatedContent

AnimatedVisibility i AnimatedContent są dostępne jako funkcje rozszerzenia funkcji Transition. Wartość targetState dla Transition.AnimatedVisibility i Transition.AnimatedContent jest wyprowadzona z Transition, a przejścia wejścia/wyjścia są uruchamiane w razie potrzeby, gdy wartość targetState Transition ulegnie zmianie. Te funkcje rozszerzenia umożliwiają przeniesienie do elementu Transition wszystkich animacji wejścia/wyjścia/zmiany rozmiaru, które w przeciwnym razie byłyby wewnętrzne dla elementów AnimatedVisibility/AnimatedContent. Dzięki tym funkcjom rozszerzenia można obserwować zmiany stanu AnimatedVisibility/AnimatedContent z zewnątrz. Zamiast parametru logicznego visible ta wersja funkcji AnimatedVisibility przyjmuje funkcję lambda, która konwertuje stan docelowy nadrzędnej funkcji 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 pracujesz nad złożonym komponentem z wieloma animowanymi wartościami, możesz jednak chcieć oddzielić implementację animacji od kompozycyjnego interfejsu użytkownika.

Aby to zrobić, utwórz klasę zawierającą wszystkie wartości animacji oraz 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ć logikę animacji lub umożliwić wielokrotne używanie 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) }
}

Tworzenie animacji powtarzanej w nieskończoność za pomocą 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ż podać 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. Jest ono obsługiwane przez interfejs Animatable, który jest interfejsem API opartym na korobojacji 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 Animation. Chociaż większość aplikacji nie będzie bezpośrednio współpracować z interfejsem Animation, niektóre funkcje dostosowywania 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 udostępnianych jako funkcje zawieszone. 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 tym przykładzie tworzymy i zapamiętujemy instancję zmiennej 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 w momencie zmiany wartości trwa animacja, zostaje ona anulowana, a nowa animacja rozpoczyna się od bieżącej wartości migawki z bieżącą prędkością.

To jest implementacja animacji, która obsługuje interfejs API animate*AsState wymieniony 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ć wartość początkową inną od pierwszej wartości docelowej. 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 umożliwia wykonywanie większej liczby operacji na wartości treści, a mianowicie snapToanimateDecay. snapTonatychmiast 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, takimi jak zdarzenia dotykowe. animateDecay uruchamia animację, która zwalnia z danej prędkości. Jest to przydatne podczas implementowania działania fling. 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 AnimationSpec.

Animation: animacja sterowana ręcznie

Animation to interfejs API animacji o najniższym poziomie dostępu. 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. Animation jest bezstanowy i nie ma żadnej koncepcji cyklu życia. Jest to mechanizm obliczania animacji, którego używają interfejsy API wyższego poziomu.

TargetBasedAnimation

Inne interfejsy API obsługują większość przypadków użycia, ale korzystanie bezpośrednio z TargetBasedAnimation pozwala Ci kontrolować czas odtwarzania animacji. W przykładzie poniżej czas odtwarzania TargetAnimation jest ręcznie kontrolowany na podstawie czasu kadru 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 tagu TargetBasedAnimation tag DecayAnimation nie wymaga podania parametru targetValue. Zamiast tego oblicza wartość targetValue na podstawie warunków początkowych ustawionych przez initialVelocityinitialValue oraz podanych wartości DecayAnimationSpec.

Animacje wygaszania są często używane po wykonaniu gestu przesunięcia, aby spowolnić elementy aż do zatrzymania. Prędkość animacji zaczyna się od wartości określonej przez initialVelocityVectori z czasem zwalnia.