Questa pagina descrive come creare animazioni basate su valori in Jetpack Compose, concentrandosi sulle API che animano i valori in base ai loro stati attuali e di destinazione.
Anima un singolo valore con animate*AsState
Le funzioni animate*AsState sono API di animazione semplici in Compose per l'animazione di un singolo valore. Devi fornire solo il valore target (o valore finale) e l'API avvia l'animazione dal valore corrente al valore specificato.
L'esempio seguente anima l'alpha utilizzando questa API. Se racchiudi il valore target in animateFloatAsState, il valore alfa diventa un valore di animazione compreso tra i valori forniti (1f o 0.5f in questo caso).
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) )
Non è necessario creare un'istanza di una classe di animazione o gestire
l'interruzione. Dietro le quinte, verrà creato un oggetto di animazione (ovvero un'istanza di Animatable) e memorizzato nel sito di chiamata, con il primo valore target come valore iniziale. Da quel momento in poi, ogni volta che fornisci a questo componibile un valore target diverso, viene avviata automaticamente un'animazione verso quel valore. Se è già in corso un'animazione, questa inizia dal suo valore (e dalla sua velocità) corrente e si anima verso il valore target. Durante l'animazione, questo composable viene ricomposto e restituisce un valore di animazione aggiornato per ogni frame.
Per impostazione predefinita, Compose fornisce funzioni animate*AsState per Float, Color, Dp, Size, Offset, Rect, Int, IntOffset e IntSize. Puoi aggiungere il supporto di altri tipi di dati fornendo un TwoWayConverter a animateValueAsState che accetta un tipo generico.
Puoi personalizzare le specifiche dell'animazione fornendo un
AnimationSpec. Per saperne di più, consulta AnimationSpec.
Anima più proprietà contemporaneamente con una transizione
Transition gestisce una o più animazioni come elementi secondari e le esegue contemporaneamente tra più stati.
Gli stati possono essere di qualsiasi tipo di dati. In molti casi, puoi utilizzare un tipo enum personalizzato per verificare la sicurezza dei tipi, come in questo esempio:
enum class BoxState { Collapsed, Expanded }
updateTransition crea e memorizza un'istanza di Transition e ne aggiorna lo stato.
var currentState by remember { mutableStateOf(BoxState.Collapsed) } val transition = updateTransition(currentState, label = "box state")
Puoi quindi utilizzare una delle funzioni di estensione di animate* per definire un'animazione secondaria in questa transizione. Specifica i valori target per ciascuno stato.
Queste funzioni animate* restituiscono un valore di animazione che viene aggiornato a ogni frame durante l'animazione quando lo stato della transizione viene aggiornato con 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 } }
Facoltativamente, puoi passare un parametro transitionSpec per specificare un AnimationSpec diverso per ogni combinazione di modifiche dello stato di transizione. Per saperne di più, consulta 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 } }
Una volta che una transizione ha raggiunto lo stato di destinazione, Transition.currentState è uguale a Transition.targetState. Puoi utilizzarlo come indicatore per capire se la transizione è terminata.
In alcuni casi, potresti voler avere uno stato iniziale diverso dal primo stato di destinazione. Per farlo, puoi utilizzare updateTransition con MutableTransitionState. Ad esempio, ti consente di avviare l'animazione non appena il codice entra nella composizione.
// 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") // ……
Per una transizione più complessa che coinvolge più funzioni componibili, puoi utilizzare createChildTransition per creare una transizione secondaria. Questa tecnica è utile per separare le responsabilità tra più sottocomponenti in un componibile complesso. La transizione principale è a conoscenza di tutti i valori di animazione nelle transizioni secondarie.
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 } ) } }
Utilizza la transizione con AnimatedVisibility e AnimatedContent
AnimatedVisibility e AnimatedContent sono disponibili come funzioni di estensione di Transition. targetState per Transition.AnimatedVisibility
e Transition.AnimatedContent deriva da Transition e attiva
le animazioni di entrata, uscita e sizeTransform in base alle esigenze quando targetState di Transition
cambia. Queste funzioni di estensione ti consentono di eseguire l'hoisting di tutte le animazioni di entrata, uscita e sizeTransform che altrimenti sarebbero interne a AnimatedVisibility/AnimatedContent in Transition. Con queste funzioni di estensione, puoi osservare il cambiamento di stato di AnimatedVisibility/AnimatedContent dall'esterno. Anziché un parametro booleano visible, questa versione di AnimatedVisibility accetta un'espressione lambda che converte lo stato di destinazione della transizione principale in un valore booleano.
Per i dettagli, consulta AnimatedVisibility e 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") } } } }
Incapsula una transizione e rendila riutilizzabile
Per casi d'uso semplici, definire le animazioni di transizione nello stesso componibile della UI è un'opzione valida. Tuttavia, quando lavori su un componente complesso con un numero di valori animati, potresti voler separare l'implementazione dell'animazione dalla UI componibile.
Puoi farlo creando una classe che contiene tutti i valori di animazione e una funzione update che restituisce un'istanza di quella classe. Puoi estrarre l'implementazione della transizione nella nuova funzione separata. Questo pattern è utile quando devi centralizzare la logica di animazione o rendere riutilizzabili animazioni complesse.
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) } }
Crea un'animazione che si ripete all'infinito con rememberInfiniteTransition
InfiniteTransition contiene una o più animazioni secondarie come Transition,
ma le animazioni iniziano a essere eseguite non appena entrano nella composizione e non si
interrompono a meno che non vengano rimosse. Puoi creare un'istanza di InfiniteTransition
con rememberInfiniteTransition e aggiungere animazioni secondarie con animateColor,
animateFloat o animateValue. Devi anche specificare un infiniteRepeatable per indicare le specifiche dell'animazione.
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) )
API Animation di basso livello
Tutte le API Animation di alto livello menzionate nella sezione precedente si basano sulle API Animation di basso livello.
Le funzioni animate*AsState sono API semplici che eseguono il rendering di una modifica istantanea del valore come valore di animazione. Questa funzionalità è supportata da Animatable, un'API basata su coroutine per l'animazione di un singolo valore.
updateTransition crea un oggetto di transizione che può gestire più valori di animazione ed eseguirli quando cambia uno stato. rememberInfiniteTransition
è simile, ma crea una transizione infinita che può gestire più animazioni che continuano a tempo indeterminato. Tutte queste API sono componibili, ad eccezione di Animatable, il che significa che puoi creare queste animazioni al di fuori della composizione.
Tutte queste API si basano sull'API Animation più fondamentale. Sebbene la maggior parte delle app non interagisca direttamente con Animation, puoi accedere ad alcune delle sue funzionalità di personalizzazione tramite API di livello superiore. Per saperne di più su AnimationVector e AnimationSpec, consulta Personalizza le animazioni.
Animatable: animazione di un singolo valore basata su coroutine
Animatable è un contenitore di valori che può animare il valore quando viene modificato
utilizzando animateTo. Questa è l'API che supporta l'implementazione di animate*AsState. Garantisce una continuazione coerente e l'esclusività reciproca, il che significa che la modifica del valore è sempre continua e Compose annulla qualsiasi animazione in corso.
Molte funzionalità di Animatable, tra cui animateTo, sono funzioni di sospensione.
Ciò significa che devi eseguirne il wrapping in un ambito di coroutine appropriato. Ad esempio, puoi utilizzare il componibile LaunchedEffect per creare un ambito solo per la durata del valore della chiave specificata.
// 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) )
Nell'esempio precedente, crei e memorizzi un'istanza di Animatable con il valore iniziale di Color.Gray. A seconda del valore del flag booleano ok, il colore si anima in Color.Green o Color.Red. Qualsiasi modifica successiva al valore booleano avvia un'animazione per l'altro colore.
Se un'animazione è in corso quando il valore cambia, Compose la annulla e la nuova animazione inizia dal valore dell'istantanea corrente con la velocità corrente.
Questa API Animatable è l'implementazione sottostante di animate*AsState menzionata nella sezione precedente. L'utilizzo diretto di Animatable offre un controllo più granulare in diversi modi:
- Innanzitutto,
Animatablepuò avere un valore iniziale diverso dal primo valore target. Ad esempio, l'esempio di codice precedente mostra inizialmente una casella grigia, che diventa immediatamente verde o rossa. - In secondo luogo,
Animatablefornisce più operazioni sul valore dei contenuti, in particolaresnapToeanimateDecay.snapToimposta immediatamente il valore corrente sul valore target. Questo è utile quando l'animazione non è l'unica fonte di riferimento e deve sincronizzarsi con altri stati, ad esempio gli eventi touch.animateDecayavvia un'animazione che rallenta a partire dalla velocità specificata. Questo è utile per implementare il comportamento di scorrimento veloce.
Per saperne di più, consulta Gesti e animazioni.
Per impostazione predefinita, Animatable supporta Float e Color, ma puoi utilizzare qualsiasi tipo di dati fornendo un TwoWayConverter. Per saperne di più, consulta AnimationVector.
Puoi personalizzare le specifiche dell'animazione fornendo un AnimationSpec.
Per saperne di più, consulta AnimationSpec.
Animation: animazione controllata manualmente
Animation è l'API Animation di livello più basso disponibile. Molte delle animazioni che abbiamo visto finora si basano su Animation. Esistono due sottotipi di Animation: TargetBasedAnimation e DecayAnimation.
Utilizza Animation solo per controllare manualmente il tempo dell'animazione. Animation è stateless e non ha alcun concetto di ciclo di vita. Funge da motore di calcolo delle animazioni per le API di livello superiore.
TargetBasedAnimation
Altre API coprono la maggior parte dei casi d'uso, ma l'utilizzo diretto di TargetBasedAnimation ti consente di controllare il tempo di riproduzione dell'animazione. Nell'esempio seguente, controlli manualmente il tempo di riproduzione di TargetAnimation in base alla durata frame fornita da 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
A differenza di TargetBasedAnimation, DecayAnimation non richiede di fornire un targetValue. Al contrario, calcola il proprio targetValue in base alle condizioni di partenza, impostate da initialVelocity e initialValue, e al DecayAnimationSpec fornito.
Le animazioni di decadimento vengono spesso utilizzate dopo un gesto di scorrimento rapido per rallentare gli elementi fino ad arrestarli. La velocità dell'animazione inizia con il valore impostato da initialVelocityVector e rallenta nel tempo.
Consigliati per te
- Nota: il testo del link viene visualizzato quando JavaScript è disattivato
- Personalizza le animazioni
- Animazioni in Compose
- Modificatori e composabili di animazione