Animare un singolo valore con animate*AsState
Le funzioni animate*AsState
sono le API di animazione più semplici in Compose per animare un singolo valore. Fornisci solo il valore target (o finale) e
l'API avvia l'animazione dal valore corrente a quello specificato.
Di seguito è riportato un esempio di animazione dell'alpha utilizzando questa API. Se inserisci il valore target in animateFloatAsState
, il valore alpha diventa un valore di animazione tra i valori forniti (1f
o 0.5f
in questo caso).
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) )
Tieni presente che non è necessario creare un'istanza di una classe di animazione o gestire l'interruzione. Dietro le quinte, verrà creato e memorizzato un oggetto animazione (ovvero un'istanza Animatable
) nel sito di chiamata, con il primo valore target come valore iniziale. Da quel momento in poi, ogni volta che fornisci a questo composable un valore target diverso, viene avviata automaticamente un'animazione verso quel valore. Se è già in corso un'animazione, questa parte dal valore (e dalla velocità) corrente e si anima verso il valore target. Durante l'animazione, questo composable viene ricomposto e restituisce un valore di animazione aggiornato in ogni frame.
Compose fornisce immediatamente funzioni animate*AsState
per Float
,
Color
, Dp
, Size
, Offset
, Rect
, Int
, IntOffset
e
IntSize
. Puoi aggiungere facilmente il supporto di altri tipi di dati fornendo un valore da TwoWayConverter
a animateValueAsState
che accetta un tipo generico.
Puoi personalizzare le specifiche dell'animazione fornendo un AnimationSpec
.
Per ulteriori informazioni, consulta AnimationSpec.
Animare 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 garantire la sicurezza del tipo, come in questo esempio:
enum class BoxState { Collapsed, Expanded }
updateTransition
crea e memorizza un'istanza di Transition
e aggiorna il relativo stato.
var currentState by remember { mutableStateOf(BoxState.Collapsed) } val transition = updateTransition(currentState, label = "box state")
Puoi quindi utilizzare una delle funzioni di estensione animate*
per definire un'animazione secondaria in questa transizione. Specifica i valori target per ciascuno degli stati.
Queste funzioni animate*
restituiscono un valore di animazione che viene aggiornato ogni fotogramma
durante l'animazione quando lo stato di 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 } }
Se vuoi, puoi passare un parametro transitionSpec
per specificare un valore AnimationSpec
diverso per ciascuna delle combinazioni di modifiche dello stato di transizione. Per ulteriori informazioni, 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 è arrivata allo stato target, Transition.currentState
è uguale a Transition.targetState
. Questo può essere utilizzato come indicatore per verificare se la transizione è stata completata.
A volte vogliamo avere uno stato iniziale diverso dal primo stato
target. Possiamo usare updateTransition
con MutableTransitionState
per ottenere questo risultato. Ad esempio, ci 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 composibili, puoi
utilizzare createChildTransition
per creare una transizione secondaria. Questa tecnica è utile per separare i problemi tra più componenti secondari in un composable complesso. La transizione principale sarà consapevole 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 } ) } }
Utilizzare la transizione con AnimatedVisibility
e AnimatedContent
AnimatedVisibility
e AnimatedContent
sono disponibili come funzioni di estensione di Transition
. Il valore targetState
per Transition.AnimatedVisibility
e Transition.AnimatedContent
è ricavato da Transition
e attiva le transizioni di entrata/uscita in base alle esigenze quando il valore targetState
di Transition
è cambiato. Queste funzioni di estensione consentono di sollevare in Transition
tutte le animazioni enter/exit/sizeTransform che altrimenti sarebbero interne a AnimatedVisibility
/AnimatedContent
.
Con queste funzioni di estensione, la variazione di stato di AnimatedVisibility
/AnimatedContent
può essere osservata dall'esterno. Anziché un parametro booleano visible
,
questa versione di AnimatedVisibility
accetta una funzione lambda che converte lo stato di destinazione della transizione principale in un booleano.
Per maggiori 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") } } } }
Incapsulare una transizione e renderla riutilizzabile
Per casi d'uso semplici, definire le animazioni di transizione nello stesso composable dell'interfaccia utente è un'opzione perfettamente valida. Tuttavia, quando lavori su un componente complesso con una serie di valori animati, ti consigliamo di separare l'implementazione dell'animazione dall'interfaccia utente componibile.
Puoi farlo creando una classe che contenga tutti i valori di animazione e una funzione "update" che restituisce un'istanza di quella classe. L'implementazione della transizione può essere estratta nella nuova funzione separata. Questo pattern è utile quando è necessario 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) } }
Creare 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 fermano a meno che non vengano rimosse. Puoi creare un'istanza di InfiniteTransition
con rememberInfiniteTransition
. Le animazioni secondarie possono essere aggiunte con animateColor
, animatedFloat
o animatedValue
. Devi anche specificare un valore infiniteRepeatable per specificare 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 di animazione di basso livello
Tutte le API di animazione di alto livello menzionate nella sezione precedente si basano sulle API di animazione di basso livello.
Le funzioni animate*AsState
sono le API più semplici, che visualizzano una variazione immediata del valore come valore di animazione. È supportato 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 in base
a una modifica dello stato. rememberInfiniteTransition
è simile, ma crea una transizione infinita che può gestire più animazioni che continuano a funzionare indefinitamente. Tutte queste API sono composable, tranne Animatable
, il che significa che queste animazioni possono essere create 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
, alcune delle funzionalità di personalizzazione per Animation
sono disponibili tramite API di livello superiore. Per ulteriori informazioni su AnimationVector
e AnimationSpec
, consulta Personalizzare le animazioni.
Animatable
: animazione con un solo valore basata su coroutine
Animatable
è un contenitore di valori che può animarlo man mano che viene modificato tramite
animateTo
. Questa è l'API di supporto dell'implementazione di animate*AsState
.
Garantisce continuità e esclusione reciproca, il che significa che la variazione del valore è sempre continua e qualsiasi animazione in corso verrà annullata.
Molte funzionalità di Animatable
, tra cui animateTo
, sono fornite come funzioni di sospensione. Ciò significa che devono essere racchiuse in un ambito appropriato della coroutine. Ad esempio, puoi utilizzare il composable LaunchedEffect
per creare un ambito solo per la durata del valore della chiave specificato.
// 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 riportato sopra, creiamo e ricordiamo un'istanza di Animatable
con il valore iniziale Color.Gray
. A seconda del valore del flag booleano
ok
, il colore viene animato in Color.Green
o Color.Red
. Qualsiasi modifica successiva al valore booleano avvia l'animazione all'altro colore. Se al momento della modifica del valore è in corso un'animazione, questa viene annullata e la nuova animazione inizia dal valore dell'istantanea corrente con la velocità corrente.
Questa è l'implementazione dell'animazione che esegue il backup dell'API animate*AsState
menzionata nella sezione precedente. Rispetto a animate*AsState
, l'utilizzo diretto di Animatable
ci offre un controllo più granulare su diversi aspetti. Innanzitutto,
Animatable
può avere un valore iniziale diverso dal primo valore target.
Ad esempio, l'esempio di codice riportato sopra mostra inizialmente una casella grigia, che inizia immediatamente a animarsi in verde o rosso. In secondo luogo, Animatable
offre più operazioni sul valore dei contenuti, ovvero snapTo
e animateDecay
. snapTo
imposta immediatamente il valore corrente sul valore target. Questo è utile quando l'animazione stessa non è l'unica origine di riferimento e deve essere sincronizzata con altri stati, ad esempio gli eventi tocco. animateDecay
avvia un'animazione che rallenta
dalla velocità specificata. Questo è utile per implementare il comportamento di scorrimento. Per ulteriori informazioni, consulta la sezione Gesti e animazioni.
Animatable
supporta Float
e Color
, ma qualsiasi tipo di dato può essere utilizzato fornendo un TwoWayConverter
. Per ulteriori informazioni, consulta
AnimationVector.
Puoi personalizzare le specifiche dell'animazione fornendo un AnimationSpec
.
Per ulteriori informazioni, 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 tipi di Animation
:
TargetBasedAnimation
e DecayAnimation
.
Animation
deve essere utilizzato 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 utilizzato dalle API di livello superiore.
TargetBasedAnimation
Altre API coprono la maggior parte dei casi d'uso, ma l'utilizzo diretto di TargetBasedAnimation
consente di controllare autonomamente la durata di riproduzione dell'animazione. Nell'esempio seguente,
la durata di riproduzione di TargetAnimation
è controllata manualmente in base al tempo
del frame fornito 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 l'indicazione di un targetValue
. ma calcola il suo
targetValue
in base alle condizioni iniziali, impostate da initialVelocity
e
initialValue
e dal valore fornito DecayAnimationSpec
.
Le animazioni di decadimento vengono spesso utilizzate dopo un gesto di lancio per rallentare gli elementi fino a fermarli. 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
- Personalizzare le animazioni {:#customize-animations}
- Animazioni in Compose
- Composabili e modificatori di animazione