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
, IntOffset
i IntSize
. 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 AnimatedVisibility 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), 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 AnimationVector
i AnimationSpec
znajdziesz w artykule Dostosowywanie animacji.
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 Float
i Color
, 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.
Polecane dla Ciebie
- Uwaga: tekst linku wyświetla się, gdy JavaScript jest wyłączony
- Dostosowywanie animacji {:#customize-animations}
- Animacje w tworzeniu wiadomości
- Modyfikatory i komponenty animacji