Na tej stronie opisujemy, jak tworzyć animacje oparte na wartościach w Jetpack Compose, skupiając się na interfejsach API, które animują wartości na podstawie ich bieżącego i docelowego stanu.
Animowanie pojedynczej wartości za pomocą funkcji animate*AsState
Funkcje animate*AsState
to proste interfejsy API animacji w Compose, które służą do animowania pojedynczej wartości. Podajesz tylko wartość docelową (lub końcową), a interfejs API rozpoczyna animację od bieżącej wartości do wartości określonej.
Poniższy przykład animuje wartość alfa za pomocą tego interfejsu API. Umieszczając wartość docelową w animateFloatAsState
, wartość alfa staje 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) )
Nie musisz tworzyć instancji żadnej klasy animacji ani obsługiwać przerwań. W tle zostanie utworzony i zapamiętany w miejscu wywołania obiekt animacji (czyli instancja Animatable
), 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.
Domyślnie Compose udostępnia funkcje animate*AsState
dla Float
, Color
, Dp
, Size
, Offset
, Rect
, Int
, IntOffset
i IntSize
. Możesz dodać obsługę innych typów danych, podając TwoWayConverter
do animateValueAsState
, która 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 sprawdzić 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żesz użyć tego jako sygnału, czy przejście zostało zakończone.
Czasami możesz chcieć, aby stan początkowy różnił się od pierwszego stanu docelowego. Możesz to zrobić za pomocą updateTransition
i MutableTransitionState
. Umożliwia na przykład rozpoczęcie animacji od razu 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 zna 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
Funkcje AnimatedVisibility
i AnimatedContent
są dostępne jako funkcje rozszerzające Transition
. Wartość targetState
dla Transition.AnimatedVisibility
i Transition.AnimatedContent
jest wyliczana na podstawie wartości Transition
i w razie potrzeby wywołuje animacje wejścia, wyjścia i sizeTransform
, gdy zmienia się wartość targetState
w Transition
. Te funkcje rozszerzenia umożliwiają przeniesienie wszystkich animacji wejścia, wyjścia i sizeTransform
, które w przeciwnym razie byłyby wewnętrzne w AnimatedVisibility
/AnimatedContent
, do Transition
. Dzięki tym funkcjom rozszerzenia możesz obserwować zmianę stanu 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 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 prostych przypadkach użycia zdefiniowanie animacji przejścia w tym samym komponencie, w którym znajduje się interfejs, jest prawidłową opcją. Podczas pracy nad złożonym komponentem z wieloma animowanymi wartościami możesz jednak chcieć oddzielić implementację animacji od komponentu interfejsu.
Możesz to zrobić, tworząc klasę, która zawiera wszystkie wartości animacji, oraz funkcję, która zwraca instancję tej klasy.update
Możesz wyodrębnić implementację przejścia do nowej, osobnej funkcji. Ten wzorzec jest przydatny, gdy chcesz 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 jedną 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
za pomocą rememberInfiniteTransition
i dodać animacje podrzędne za pomocą 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 proste interfejsy API, które renderują natychmiastową zmianę wartości jako wartość animacji. Ta funkcja jest obsługiwana przez Animatable
, interfejs API oparty na korutynach, który służy do animowania pojedynczej wartości.
updateTransition
tworzy obiekt przejścia, który może zarządzać wieloma animowanymi wartościami i uruchamiać je, gdy zmienia się stan. rememberInfiniteTransition
jest podobny, ale tworzy nieskończone przejście, które może zarządzać wieloma animacjami trwającymi w nieskończoność. Wszystkie te interfejsy API są funkcjami kompozycyjnymi z wyjątkiem Animatable
, co oznacza, że możesz tworzyć te animacje poza kompozycją.
Wszystkie te interfejsy API są oparte na bardziej podstawowym interfejsie Animation
API. Chociaż większość aplikacji nie będzie wchodzić w bezpośrednią interakcję z Animation
, możesz uzyskać dostęp do niektórych funkcji dostosowywania za pomocą interfejsów API wyższego poziomu. Więcej informacji o AnimationVector
i AnimationSpec
znajdziesz w sekcji Dostosowywanie animacji.
Animatable
: Animacja pojedynczej wartości oparta na korutynach
Animatable
to obiekt przechowujący wartość, który może animować wartość w miarę jej zmiany za pomocą animateTo
. Jest to interfejs API, który obsługuje wdrożenie funkcji animate*AsState
. Zapewnia to ciągłość i wzajemną wyłączność, co oznacza, że zmiana wartości jest zawsze ciągła, a Compose anuluje każdą trwającą animację.
Wiele funkcji Animatable
, w tym animateTo
, to funkcje zawieszania.
Oznacza to, że musisz umieścić je w odpowiednim zakresie coroutine. 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 tworzysz instancję Animatable
i zapamiętujesz ją z wartością początkową Color.Gray
. W zależności od wartości flagi logicznej ok
kolor animuje się do 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, Compose anuluje animację, a nowa animacja rozpoczyna się od bieżącej wartości migawki z bieżącą prędkością.
Ten interfejs API Animatable
jest podstawową implementacją interfejsu animate*AsState
wspomnianego w poprzedniej sekcji. Bezpośrednie korzystanie z usługi Animatable
zapewnia większą kontrolę na kilka sposobów:
- Po pierwsze,
Animatable
może mieć wartość początkową inną niż pierwsza wartość docelowa. Na przykład w poprzednim przykładzie kodu najpierw widać szare pole, które natychmiast zmienia kolor na zielony lub czerwony. - Po drugie,
Animatable
udostępnia więcej operacji na wartości treści, w szczególnościsnapTo
ianimateDecay
.snapTo
natychmiast ustawia bieżącą wartość na wartość docelową. Jest to przydatne, gdy animacja nie jest jedynym źródłem informacji i musi synchronizować się z innymi stanami, np. ze zdarzeniami dotykowymi.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.
Domyślnie Animatable
obsługuje Float
i Color
, ale możesz użyć 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 do tej pory widzieliśmy, opiera się na Animation
. Istnieją 2 Animation
podtypy: TargetBasedAnimation
i DecayAnimation
.
Używaj Animation
tylko do ręcznego sterowania czasem animacji. Animation
jest bezstanowy i nie ma pojęcia cyklu życia. Służy jako silnik obliczania animacji dla interfejsów 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 kontrolować czas odtwarzania animacji. W tym przykładzie ręcznie kontrolujesz czas odtwarzania elementu TargetAnimation
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 atrybutu TargetBasedAnimation
atrybut DecayAnimation
nie wymaga podania atrybutu targetValue
. Zamiast tego oblicza targetValue
na podstawie warunków początkowych określonych przez initialVelocity
i initialValue
oraz podanego DecayAnimationSpec
.
Animacje zanikania są często używane po wykonaniu gestu szybkiego przesunięcia, aby spowolnić elementy i je zatrzymać. Prędkość animacji zaczyna się od wartości ustawionej przez initialVelocityVector
i zwalnia z upływem czasu.
Polecane dla Ciebie
- Uwaga: tekst linku jest wyświetlany, gdy język JavaScript jest wyłączony.
- Dostosowywanie animacji
- Animacje w Compose
- Modyfikatory i funkcje kompozycyjne animacji