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
, IntOffset
i IntSize
. 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 zrobić, możesz użyć updateTransition
z 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 AnimatedVisibility
i AnimatedContent
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 zmiany stanu AnimatedVisibility
/AnimatedContent
można obserwować 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 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 udostępnienie go do ponownego użycia
W przypadku prostych zastosowań definiowanie animacji przejść w tym samym komponencie co interfejs użytkownika jest całkowicie wystarczające. Jeśli jednak pracujesz nad złożonym komponentem z wieloma animowanymi wartościami, możesz 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. Przejście może zostać zaimplementowane w 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 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 usługi 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
z wartością początkową 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 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, 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 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 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 poniższym przykładzie 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 initialVelocity
i initialValue
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 initialVelocityVector
i z czasem zwalnia.
Polecane dla Ciebie
- Uwaga: tekst linku jest wyświetlany, gdy obsługa JavaScript jest wyłączona
- Dostosowywanie animacji {:#customize-animations}
- Animacje w Compose
- Modyfikatory i komponenty animacji