Animazioni basate sul valore

Animazione di un singolo valore con animate*AsState

Le funzioni animate*AsState sono le API di animazione più semplici in Compose per animare un singolo valore. Devi fornire solo il valore target (o valore finale) e l'API avvia l'animazione dal valore corrente al valore specificato.

Di seguito è riportato un esempio di animazione della versione alpha utilizzando questa API. Mediante il wrapping del valore target in animateFloatAsState, il valore alfa diventa ora 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)
Box(
    Modifier.fillMaxSize()
        .graphicsLayer(alpha = alpha)
        .background(Color.Red)
)

Nota che non è necessario creare un'istanza di qualsiasi classe di animazione o gestire l'interruzione. In background, verrà creato e memorizzato un oggetto di animazione (ovvero un'istanza Animatable) nel sito della chiamata, con il primo valore target come valore iniziale. Da questo momento in poi, ogni volta che fornisci al componibile un valore target diverso, viene avviata automaticamente un'animazione in base a quel valore. Se è già presente un'animazione in corso, l'animazione parte dal valore corrente (e dalla velocità) e si anima verso il valore target. Durante l'animazione, questo componibile viene ricomposto e restituisce un valore di animazione aggiornato per ogni frame.

All'istante, Compose fornisce le funzioni animate*AsState per Float, Color, Dp, Size, Offset, Rect, Int, IntOffset e IntSize. Puoi aggiungere facilmente il supporto per altri tipi di dati fornendo a animateValueAsState un tipo generico che prevede TwoWayConverter.

Puoi personalizzare le specifiche dell'animazione fornendo un elemento AnimationSpec. Per ulteriori informazioni, consulta AnimationSpec.

Animazione di più proprietà contemporaneamente con una transizione

Transition gestisce una o più animazioni come relative 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 dei tipi, come in questo esempio:

enum class BoxState {
    Collapsed,
    Expanded
}

updateTransition crea e memorizza un'istanza di Transition e ne aggiorna il suo stato.

var currentState by remember { mutableStateOf(BoxState.Collapsed) }
val transition = updateTransition(currentState, label = "box state")

Puoi quindi utilizzare una delle funzioni dell'estensione animate* per definire un'animazione secondaria in questa transizione. Specifica i valori target per ciascuno degli stati. Queste funzioni animate* restituiscono un valore dell'animazione che viene aggiornato ogni frame 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 AnimationSpec diverso per ogni combinazione 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 raggiunta la transizione allo stato target, Transition.currentState sarà uguale a Transition.targetState. Questo può essere usato come indicatore per capire se la transizione è terminata.

A volte vogliamo avere uno stato iniziale diverso dal primo stato target. Possiamo utilizzare updateTransition con MutableTransitionState per ottenere questo risultato. Ad esempio, ci consente di avviare l'animazione non appena il codice inserisce la composizione.

// 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")
// ……

Per una transizione più complessa che coinvolge più funzioni componibili, puoi utilizzare createChildTransition per creare una transizione figlio. Questa tecnica è utile per separare i problemi tra più sottocomponenti in un componibile complesso. La transizione principale prenderà in considerazione tutti i valori dell'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 transizioni di entrata/uscita secondo necessità quando il valore targetState di Transition viene modificato. Queste funzioni di estensione consentono di sollevare tutte le animazioni enter/exit/sizeTransform che altrimenti risulterebbero interne in AnimatedVisibility/AnimatedContent in Transition. Con queste funzioni di estensione, il cambiamento di stato di AnimatedVisibility/AnimatedContent può essere osservato dall'esterno. Anziché un parametro booleano visible, questa versione di AnimatedVisibility utilizza una funzione lambda che converte lo stato target della transizione padre in un valore booleano.

Per maggiori dettagli, vedi AnimatedVisibilità 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),
    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")
            }
        }
    }
}

Incapsulare una transizione e renderla riutilizzabile

Per casi d'uso semplici, definire animazioni di transizione nello stesso componibile della UI è un'opzione perfettamente valida. Quando lavori su un componente complesso con una serie di valori animati, tuttavia, potresti voler separare l'implementazione dell'animazione dall'interfaccia utente componibile.

Puoi farlo creando una classe che contenga tutti i valori dell'animazione e una funzione "update" che restituisca un'istanza di quella classe. L'implementazione della transizione può essere estratta nella nuova funzione separata. Questo pattern è utile quando è necessario centralizzare la logica dell'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 a ripetizione all'infinito con rememberInfiniteTransition

InfiniteTransition contiene una o più animazioni secondarie come Transition, ma le animazioni iniziano non appena entrano nella composizione e non si interrompono 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 inoltre specificare un valore infiniterepeatable per specificare le specifiche dell'animazione.

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))

API di animazione di basso livello

Tutte le API di animazione di alto livello menzionate nella sezione precedente sono basate sulle basi delle API di animazione di basso livello.

Le funzioni animate*AsState sono le API più semplici che eseguono il rendering di una variazione di valore istantanea come un valore di animazione. È supportato da Animatable, un'API basata su coroutine per animare un singolo valore. updateTransition crea un oggetto di transizione in grado di gestire più valori di animazione ed eseguirli in base a una modifica dello stato. rememberInfiniteTransition è simile, ma crea una transizione infinita che consente di gestire più animazioni che continuano a funzionare a tempo indeterminato. Tutte queste API sono componibili tranne Animatable, il che significa che queste animazioni possono essere create al di fuori della composizione.

Tutte queste API sono basate sull'API Animation più fondamentale. Anche se la maggior parte delle app non interagirà direttamente con Animation, alcune delle funzionalità di personalizzazione di Animation sono disponibili tramite API di livello superiore. Per saperne di più su AnimationVector e AnimationSpec, consulta la sezione Personalizzare le animazioni.

Diagramma che mostra la relazione tra le varie API di animazione di basso livello

Animatable: animazione con valore singolo basata su coroutine

Animatable è un contenitore di valore che può animare il valore quando viene modificato tramite animateTo. Questa è l'API che esegue il backup dell'implementazione di animate*AsState. Assicura una continuazione coerente ed esclusività reciproca, il che significa che la modifica 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 inseriti in un ambito della coroutine appropriato. Ad esempio, puoi utilizzare l'elemento componibile LaunchedEffect per creare un ambito solo per la durata della coppia chiave-valore 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, creiamo e memorizziamo 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 l'animazione per l'altro colore. Se è presente un'animazione in corso quando il valore viene modificato, l'animazione viene annullata e la nuova animazione inizia dal valore dello snapshot corrente con la velocità attuale.

Questa è l'implementazione dell'animazione che esegue il backup dell'API animate*AsState menzionata nella sezione precedente. Rispetto a animate*AsState, l'uso diretto di Animatable offre un controllo più granulare su diversi aspetti. Innanzitutto, Animatable può avere un valore iniziale diverso dal primo valore target. Ad esempio, il codice di esempio riportato sopra mostra una casella grigia all'inizio, 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 fonte attendibile e deve essere sincronizzata con altri stati, ad esempio gli eventi touch. animateDecay avvia un'animazione che rallenta a partire dalla velocità specificata. Ciò è utile per implementare un comportamento di fling. Per ulteriori informazioni, consulta Gesto e animazione.

All'istante, Animatable supporta Float e Color, ma qualsiasi tipo di dati può essere utilizzato fornendo un TwoWayConverter. Per ulteriori informazioni, vedi 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 sull'animazione. Esistono due sottotipi di Animation: TargetBasedAnimation e DecayAnimation.

Animation deve essere utilizzato solo per controllare manualmente la durata dell'animazione. Animation è stateless e non prevede alcun concetto di ciclo di vita. Serve come motore di calcolo dell'animazione utilizzato dalle API di livello superiore.

TargetBasedAnimation

Altre API coprono la maggior parte dei casi d'uso, ma l'uso diretto di TargetBasedAnimation ti consente di controllare autonomamente il tempo di riproduzione dell'animazione. Nell'esempio riportato di seguito, la durata di riproduzione di TargetAnimation è controllata manualmente 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 { mutableStateOf(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'inserimento di un targetValue. Calcola invece i suoi targetValue in base alle condizioni iniziali, impostate da initialVelocity e initialValue e al valore DecayAnimationSpec specificato.

Le animazioni di decadimento vengono spesso utilizzate dopo un gesto di scorrimento per rallentare gli elementi fino a un stop. La velocità dell'animazione inizia al valore impostato da initialVelocityVector e rallenta nel tempo.