Animowanie jednej wartości za pomocą funkcji animate*AsState
Funkcje animate*AsState
to najprostsze interfejsy API animacji w komponencie do animowania pojedynczej wartości. Podajesz tylko wartość docelową (lub wartość końcową), a interfejs API rozpocznie animację od bieżącej wartości do określonej.
Poniżej znajdziesz przykład animacji w wersji alfa za pomocą tego interfejsu API. Dzięki prostemu pakowaniu wartości docelowej w obiekt animateFloatAsState
wartość alfa jest teraz 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) Box( Modifier.fillMaxSize() .graphicsLayer(alpha = alpha) .background(Color.Red) )
Pamiętaj, że nie musisz tworzyć instancji żadnej klasy animacji ani obsługiwać przerwania. Podstawowy obiekt animacji (tj. wystąpienie Animatable
) zostanie utworzony i zapamiętany w witrynie wywołania, przy czym pierwsza wartość docelowa będzie wartością początkową. Od tej pory za każdym razem, gdy podasz w funkcji kompozycyjnej inną wartość docelową, automatycznie rozpocznie się animacja uwzględniająca tę wartość. Jeśli masz już animację w trakcie wyświetlania, animacja rozpoczyna się od jej bieżącej wartości (i prędkości) i przesuwa się w kierunku wartości docelowej. W trakcie animacji funkcja kompozycyjna jest tworzona ponownie i w każdej klatce zwraca zaktualizowaną wartość animacji.
Funkcja tworzenia wiadomości zapewnia gotowe funkcje animate*AsState
związane z: Float
, Color
, Dp
, Size
, Offset
, Rect
, Int
, IntOffset
iIntSize
. Możesz łatwo dodać obsługę innych typów danych, podając typ TwoWayConverter
animateValueAsState
o typie ogólnym.
Specyfikację animacji możesz dostosować, przesyłając AnimationSpec
.
Więcej informacji znajdziesz w sekcji AnimationSpec.
Animuj wiele usług jednocześnie z przejściem
Transition
zarządza co najmniej 1 animacją jako jej elementami podrzędnymi i uruchamia je jednocześnie między wieloma stanami.
Stany mogą mieć dowolny typ danych. W wielu przypadkach możesz użyć niestandardowego typu enum
, aby zapewnić bezpieczeństwo, jak w tym przykładzie:
enum class BoxState { Collapsed, Expanded }
updateTransition
tworzy i pamięta instancję Transition
, a następnie aktualizuje jej stan.
var currentState by remember { mutableStateOf(BoxState.Collapsed) } val transition = updateTransition(currentState, label = "box state")
Następnie możesz zdefiniować animację podrzędną w tym przejściu za pomocą jednej z funkcji rozszerzenia animate*
. Określ wartości docelowe dla każdego stanu.
Te funkcje animate*
zwracają wartość animacji, która jest aktualizowana w trakcie każdej klatki podczas 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 parametr 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 } }
Po osiągnięciu stanu docelowego wartość Transition.currentState
będzie taka sama jak wartość Transition.targetState
. W ten sposób pokażesz,
czy przenoszenie się zakończyło.
Czasami chcemy, aby początkowy stan różnił się od pierwszego. Możemy to osiągnąć za pomocą updateTransition
z elementem MutableTransitionState
. Pozwalają np. uruchamiać animację od razu po wprowadzeniu kodu do kompozycji.
// Start in collapsed state and immediately animate to expanded var currentState = remember { MutableTransitionState(BoxState.Collapsed) } currentState.targetState = BoxState.Expanded val transition = updateTransition(currentState, label = "box state") // ……
Do bardziej złożonego przejścia obejmującego wiele funkcji kompozycyjnych możesz utworzyć przejście podrzędne za pomocą createChildTransition
. Ta technika jest przydatna do rozdzielania wątpliwości
na wiele podkomponentów w złożonym elemencie kompozycyjnym. 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 elementami AnimatedVisibility
i AnimatedContent
AnimatedVisibility
i AnimatedContent
są dostępne jako funkcje rozszerzenia Transition
. Parametr targetState
dla właściwości Transition.AnimatedVisibility
i Transition.AnimatedContent
pochodzi z parametru Transition
i w razie potrzeby aktywuje przejścia wejścia/wyjścia po zmianie elementu targetState
Transition
. Te funkcje rozszerzenia umożliwiają umieszczanie w obrębie funkcji Transition
wszystkich animacji Enter/Exit/sizeTransform, które w innym przypadku byłyby wewnętrzne w obrębie AnimatedVisibility
/AnimatedContent
.
Dzięki tym funkcjom rozszerzeń zmianę stanu AnimatedVisibility
/AnimatedContent
można obserwować z zewnątrz. Zamiast logicznego parametru visible
ta wersja obiektu AnimatedVisibility
używa funkcji lambda, która konwertuje docelowy stan przejścia nadrzędnego na wartość logiczną.
Szczegółowe informacje znajdziesz w sekcjach AnimatedWidoczność 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), elevation = 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") } } } }
Otocz przejścia i umożliw jego ponowne wykorzystanie
W prostych przypadkach bardzo poprawnym rozwiązaniem jest zdefiniowanie animacji przejść w ramach tego samego elementu kompozycyjnego co interfejs użytkownika. Przy pracy nad złożonym komponentem z wieloma animowanymi wartościami może Ci się jednak przydać oddzielenie implementacji animacji od interfejsu funkcji kompozycyjnej.
Możesz to zrobić, tworząc klasę, która zawiera wszystkie wartości animacji, i 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ć działanie logiki animacji lub umożliwić wielokrotne korzystanie ze 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ńczoną powtarzającą się animację w aplikacji rememberInfiniteTransition
InfiniteTransition
zawiera co najmniej jedną animację podrzędną, np. Transition
, ale po wejściu do kompozycji animacje zaczyna wyświetlać się po wejściu w kompozycję i nie zatrzymuje się, dopóki nie zostanie usunięta. Instancję InfiniteTransition
można utworzyć za pomocą rememberInfiniteTransition
. Animacje podrzędne można dodawać za pomocą właściwości animateColor
, animatedFloat
lub animatedValue
. Musisz też podać właściwość infiniteRepeatable, by określić specyfikacje animacji.
val infiniteTransition = rememberInfiniteTransition() val color by infiniteTransition.animateColor( initialValue = Color.Red, targetValue = Color.Green, animationSpec = infiniteRepeatable( animation = tween(1000, easing = LinearEasing), repeatMode = RepeatMode.Reverse ) ) Box(Modifier.fillMaxSize().background(color))
Interfejsy API animacji niskiego poziomu
Wszystkie interfejsy API animacji wysokiego poziomu wymienione w poprzedniej sekcji są tworzone na podstawie niskopoziomowych interfejsów API animacji.
Funkcje animate*AsState
to najprostsze interfejsy API, które renderują natychmiastową zmianę wartości jako wartości animacji. Jest wspierana przez interfejs API Animatable
, który jest oparty na współtworzonych interfejsach API do animowania pojedynczej wartości. updateTransition
tworzy obiekt przejścia, który może zarządzać wieloma wartościami animowanymi i uruchamiać je po zmianie stanu. rememberInfiniteTransition
działa podobnie, ale tworzy nieskończone przejście, które może zarządzać wieloma animacjami wyświetlanymi bez końca. Wszystkie te interfejsy API są funkcjami kompozycyjnymi, z wyjątkiem interfejsu Animatable
, który oznacza, że te animacje można tworzyć poza kompozycją.
Wszystkie te interfejsy API są oparte na podstawowym interfejsie API Animation
. Większość aplikacji nie będzie wchodzić bezpośrednio w interakcje z Animation
, ale niektóre z 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 z jedną wartością opartą na korutynach
Animatable
to element przechowywania wartości, który może animować wartość, gdy zmienia się ona w funkcji animateTo
. To jest interfejs API, który tworzy kopię zapasową implementacji animate*AsState
.
Zapewnia spójność ciągłości i wzajemnego wyłączności, co oznacza, że zmiana wartości jest zawsze ciągła, a każda trwająca animacja zostaje anulowana.
Wiele funkcji Animatable
, w tym animateTo
, jest dostępnych jako funkcje zawieszania. Oznacza to, że należy je opakować w odpowiedni zakres koordynacyjny. Możesz np. użyć funkcji kompozycyjnej LaunchedEffect
, aby utworzyć zakres ograniczony do czasu 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 przykładzie powyżej tworzymy i zapamiętujemy wystąpienie Animatable
z wartością początkową Color.Gray
. W zależności od wartości flagi wartości logicznej
ok
kolor zmienia się na Color.Green
lub Color.Red
. Każda kolejna zmiana tej wartości rozpoczyna animację nowego koloru. Jeśli w momencie zmiany wartości trwa animacja, animacja zostanie anulowana, a nowa animacja rozpocznie się od bieżącej wartości zrzutu z bieżącą prędkością.
To implementacja animacji, która tworzy kopię zapasową interfejsu animate*AsState
API wspomnianego w poprzedniej sekcji. W porównaniu z modelem animate*AsState
użycie danych Animatable
bezpośrednio daje nam precyzyjną kontrolę w kilku aspektach. Po pierwsze Animatable
może mieć wartość początkową inną niż pierwsza wartość docelowa.
Na przykład w podanym wyżej kodzie najpierw wyświetla się szare pole, które natychmiast zaczyna animować się w kolorach zielonych lub czerwonych. Po drugie Animatable
obsługuje 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. ze zdarzeniami dotknięcia. animateDecay
uruchamia animację, która zwalnia z podanej prędkości. Przydaje się to do implementowania działania przerzucania. Więcej informacji znajdziesz w sekcji Gesty i animacja.
Gotowe do użycia funkcje Animatable
obsługują Float
i Color
, ale możesz używać dowolnego typu danych, podając parametr TwoWayConverter
. Więcej informacji znajdziesz w sekcji AnimationVector.
Specyfikację animacji możesz dostosować, dodając parametr AnimationSpec
.
Więcej informacji znajdziesz w sekcji AnimationSpec.
Animation
: animacja sterowana ręcznie
Animation
to najniższy dostępny poziom interfejsu Animation API. Wiele animacji, które widzieliśmy
do tej pory, opiera się na animacji. Istnieją 2 podtypy Animation
: TargetBasedAnimation
i DecayAnimation
.
Typu Animation
należy używać tylko do ręcznego kontrolowania czasu animacji.
Animation
jest bezstanowy i nie ma żadnego pojęcia cyklu życia. Działa jako mechanizm obliczania animacji wykorzystywany przez interfejsy API wyższego poziomu.
TargetBasedAnimation
Inne interfejsy API mają zastosowanie do większości przypadków użycia, ale użycie bezpośrednio interfejsu TargetBasedAnimation
pozwala samodzielnie kontrolować czas odtwarzania animacji. W poniższym przykładzie czas odtwarzania obiektu TargetAnimation
jest ustawiany ręcznie na podstawie czasu renderowania klatki przez withFrameNanos
.
val anim = remember { TargetBasedAnimation( animationSpec = tween(200), typeConverter = Float.VectorConverter, initialValue = 200f, targetValue = 1000f ) } var playTime by remember { mutableStateOf(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
panel DecayAnimation
nie wymaga wartości targetValue
. Zamiast tego oblicza targetValue
na podstawie warunków początkowych wyznaczonych przez initialVelocity
i initialValue
oraz podany parametr DecayAnimationSpec
.
Animacje o słabości są często stosowane po geście przesuwania, by spowolnić elementy do zatrzymania. Prędkość animacji rozpoczyna się od wartości określonej przez initialVelocityVector
i maleje w miarę upływu czasu.
Polecane dla Ciebie
- Uwaga: tekst linku jest wyświetlany, gdy JavaScript jest wyłączony
- Dostosowywanie animacji {:#customize-animations}
- Animacje w sekcji Utwórz
- Modyfikatory animacji i elementy kompozycyjne