Animowanie pojedynczej wartości za pomocą funkcji animate*AsState
Funkcje animate*AsState
to najprostsze interfejsy API animacji w Compose do animowania pojedynczej wartości. Podajesz tylko wartość docelową (lub końcową), a interfejs API rozpoczyna animację od bieżącej wartości do podanej wartości.
Poniżej znajdziesz przykład animowania przezroczystości za pomocą tego interfejsu API. Wystarczy, że wartość docelową umieścisz w animateFloatAsState
, a wartość alfa stanie się wartością animacji
pomiędzy podanymi wartościami (w tym przypadku 1f
lub 0.5f
).
var enabled by remember { mutableStateOf(true) } val animatedAlpha: Float by animateFloatAsState(if (enabled) 1f else 0.5f, label = "alpha") Box( Modifier .fillMaxSize() .graphicsLayer { alpha = animatedAlpha } .background(Color.Red) )
Pamiętaj, że nie musisz tworzyć instancji żadnej klasy animacji ani obsługiwać przerwań. W tle zostanie utworzony obiekt animacji (czyli instancja Animatable
), który zostanie zapamiętany w miejscu wywołania, a jego wartością początkową będzie pierwsza wartość docelowa. Od tego momentu za każdym razem, gdy podasz temu komponentowi inną wartość docelową, automatycznie rozpocznie się animacja w kierunku tej wartości. Jeśli animacja jest już w toku, zaczyna się od bieżącej wartości (i prędkości) i przechodzi do wartości docelowej. Podczas animacji ten komponent kompozycyjny jest ponownie komponowany i zwraca zaktualizowaną wartość animacji w każdej klatce.
Compose udostępnia gotowe funkcje animate*AsState
dla języków Float
, Color
, Dp
, Size
, Offset
, Rect
, Int
, IntOffset
i IntSize
. Możesz łatwo dodać obsługę innych typów danych, podając TwoWayConverter
do animateValueAsState
, które przyjmuje typ ogólny.
Możesz dostosować specyfikacje animacji, podając AnimationSpec
.
Więcej informacji znajdziesz w sekcji AnimationSpec.
Animowanie wielu właściwości jednocześnie za pomocą przejścia
Transition
zarządza co najmniej jedną animacją jako elementem podrzędnym i uruchamia je
jednocześnie w różnych stanach.
Stany mogą być dowolnego typu danych. W wielu przypadkach możesz użyć niestandardowego enum
typu, aby zapewnić bezpieczeństwo typów, 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 animate*
funkcje zwracają wartość animacji, która jest aktualizowana w każdej klatce podczas animacji, gdy stan przejścia jest aktualizowany za pomocą 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 element 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 } }
Gdy przejście osiągnie stan docelowy, Transition.currentState
będzie takie samo jak Transition.targetState
. Może to być sygnał informujący o zakończeniu przejścia.
Czasami chcemy, aby stan początkowy różnił się od stanu pierwszego celu. Możemy to osiągnąć, używając updateTransition
z MutableTransitionState
. Umożliwia to na przykład rozpoczęcie animacji natychmiast po wejściu kodu do kompozycji.
// 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") // ……
W przypadku bardziej złożonego przejścia obejmującego wiele funkcji kompozycyjnych możesz użyć createChildTransition
, aby utworzyć przejście podrzędne. Ta technika jest przydatna do rozdzielania zadań między wieloma podkomponentami w złożonym komponencie. 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 AnimatedVisibility
i AnimatedContent
AnimatedVisibility
i AnimatedContent
są dostępne jako funkcje rozszerzające Transition
. Wartości targetState
dla Transition.AnimatedVisibility
i Transition.AnimatedContent
są wyliczane na podstawie wartości Transition
, a przejścia wejścia/wyjścia są wywoływane w razie potrzeby, gdy zmieni się wartość targetState
elementu Transition
. Te funkcje rozszerzające umożliwiają przeniesienie do elementu Transition
wszystkich animacji wejścia/wyjścia/przekształcenia rozmiaru, które w przeciwnym razie byłyby wewnętrzne dla elementu AnimatedVisibility
/AnimatedContent
.
Dzięki tym funkcjom rozszerzenia można obserwować zmianę stanu urządzenia AnimatedVisibility
/AnimatedContent
z zewnątrz. Zamiast parametru logicznego visible
ta wersja funkcji AnimatedVisibility
przyjmuje funkcję lambda, która przekształca stan docelowy przejścia nadrzędnego w wartość logiczną.
Szczegółowe informacje znajdziesz w sekcjach 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") } } } }
Enkapsulacja przejścia i możliwość ponownego użycia
W przypadku prostych zastosowań zdefiniowanie animacji przejścia w tym samym komponencie, co interfejs, jest w pełni prawidłową opcją. Jeśli jednak pracujesz nad złożonym komponentem z wieloma animowanymi wartościami, możesz chcieć oddzielić implementację animacji od komponentu interfejsu.
Możesz to zrobić, tworząc klasę, która zawiera wszystkie wartości animacji, oraz funkcję „update”, która zwraca instancję tej klasy. Implementację przejścia można wyodrębnić do nowej, oddzielnej funkcji. Ten wzorzec jest przydatny, gdy trzeba scentralizować logikę animacji lub sprawić, aby złożone animacje były wielokrotnego użytku.
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ą, np. Transition
, ale animacje zaczynają się odtwarzać od razu po wejściu do kompozycji i nie zatrzymują się, dopóki nie zostaną usunięte. Możesz utworzyć instancję InfiniteTransition
z rememberInfiniteTransition
. Animacje podrzędne można dodawać za pomocą elementów animateColor
, animatedFloat
lub animatedValue
. Musisz też określić parametr infiniteRepeatable, aby podać 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 renderują natychmiastową zmianę wartości jako wartość animacji. Jest on oparty na interfejsie Animatable
, który jest interfejsem API opartym na korutynach do animowania pojedynczej wartości. updateTransition
tworzy obiekt przejścia, który może zarządzać wieloma animowanymi wartościami i uruchamiać je na podstawie zmiany stanu. rememberInfiniteTransition
jest podobna, ale tworzy nieskończone przejście, które może zarządzać wieloma animacjami działającymi w nieskończoność. Wszystkie te interfejsy API są funkcjami kompozycyjnymi z wyjątkiem Animatable
, co oznacza, że te animacje można tworzyć poza kompozycją.
Wszystkie te interfejsy API są oparte na bardziej podstawowym interfejsie Animation
API. Większość aplikacji nie będzie wchodzić w bezpośrednią interakcję z Animation
, ale niektóre 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.
Animatable
: animacja pojedynczej wartości oparta na współprogramie
Animatable
to obiekt przechowujący wartość, która może być animowana w miarę jej zmiany za pomocą animateTo
. Jest to interfejs API, który obsługuje implementację animate*AsState
.
Zapewnia to ciągłość i wzajemną wyłączność, co oznacza, że zmiana wartości jest zawsze ciągła, a wszelkie trwające animacje zostaną anulowane.
Wiele funkcji Animatable
, w tym animateTo
, jest udostępnianych jako funkcje zawieszające. Oznacza to, że muszą być one umieszczone w odpowiednim zakresie korutyny. Możesz na przykład użyć funkcji 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 instancję Animatable
z wartością początkową Color.Gray
i zapamiętujemy ją. W zależności od wartości flagi logicznej
ok
kolor animuje się do wartości Color.Green
lub Color.Red
. Każda kolejna zmiana wartości logicznej rozpoczyna animację do drugiego koloru. Jeśli w momencie zmiany wartości trwa animacja, zostanie ona anulowana, a nowa animacja rozpocznie się od bieżącej wartości z bieżącą prędkością.
Jest to implementacja animacji, która obsługuje interfejs animate*AsState
API wspomniany w poprzedniej sekcji. W porównaniu z animate*AsState
bezpośrednie użycie Animatable
daje nam większą kontrolę w kilku aspektach. Po pierwsze,Animatable
może mieć wartość początkową inną niż pierwsza wartość docelowa.
Na przykład w przykładzie kodu powyżej początkowo wyświetla się szare pole, które natychmiast zaczyna animować się na zielono lub czerwono. Po drugie, Animatable
zapewnia więcej 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ć zsynchronizowana z innymi stanami, np. ze zdarzeniami dotyku. animateDecay
rozpoczyna animację, która zwalnia od podanej prędkości. Jest to przydatne do implementowania zachowania związanego z szybkim przesuwaniem. Więcej informacji znajdziesz w sekcji Gest i animacja.
Animatable
obsługuje Float
i Color
, ale można używać dowolnego typu danych, podając TwoWayConverter
. Więcej informacji znajdziesz w sekcji AnimationVector.
Możesz dostosować specyfikacje animacji, podając AnimationSpec
.
Więcej informacji znajdziesz w sekcji AnimationSpec.
Animation
: animacja sterowana ręcznie
Animation
to interfejs API animacji najniższego poziomu. Wiele animacji, które widzieliśmy do tej pory, opiera się na animacji. Wyróżniamy 2 podtypy Animation
: TargetBasedAnimation
i DecayAnimation
.
Animation
należy używać tylko do ręcznego sterowania czasem trwania animacji.
Animation
jest bezstanowy i nie ma pojęcia cyklu życia. Jest to mechanizm obliczania animacji, z którego korzystają interfejsy API wyższego poziomu.
TargetBasedAnimation
Większość przypadków użycia jest obsługiwana przez inne interfejsy API, ale bezpośrednie użycie TargetBasedAnimation
pozwala samodzielnie kontrolować czas odtwarzania animacji. W poniższym przykładzie czas odtwarzania elementu TargetAnimation
jest kontrolowany ręcznie na podstawie czasu klatki podanego przez element 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 TargetBasedAnimation
atrybut DecayAnimation
nie wymaga podania wartości targetValue
. Zamiast tego oblicza wartość targetValue
na podstawie warunków początkowych określonych przez initialVelocity
i initialValue
oraz podanej wartości DecayAnimationSpec
.
Animacje zanikania są często używane po wykonaniu gestu szybkiego przesunięcia, aby spowolnić elementy i zatrzymać je. Szybkość animacji zaczyna się od wartości ustawionej przez initialVelocityVector
i z czasem zwalnia.
Polecane dla Ciebie
- Uwaga: tekst linku jest wyświetlany, gdy JavaScript jest wyłączony.
- Dostosowywanie animacji {:#customize-animations}
- Animacje w Compose
- Modyfikatory i funkcje kompozycyjne animacji