Animazioni basate sul valore

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. Raggruppando semplicemente il valore target in animateFloatAsState, il valore alpha corrisponde ora a 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. Di base, nel sito di chiamata verrà creato e memorizzato un oggetto di animazione (ovvero un'istanza Animatable), con il primo valore target come valore iniziale. Da questo momento in poi, ogni volta che fornisci un valore target diverso per questo componibile, viene avviata automaticamente un'animazione per 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.

Per impostazione predefinita, Compose fornisce 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. Per raggiungere questo obiettivo, puoi utilizzare updateTransition con MutableTransitionState. 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 componibili, 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 terrà conto di tutti i valori dell'animazione nelle transizioni figlio.

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. targetState per Transition.AnimatedVisibility e Transition.AnimatedContent deriva da Transition e attivano le transizioni di tipo entra/uscita secondo le necessità quando il valore targetState di Transition viene modificato. 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, la definizione di animazioni di transizione nella stessa 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 le 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 ripetuta 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. 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 rimangono in esecuzione per sempre. Tutte queste API sono componibili, ad eccezione di 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.

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 valori che può animare il valore 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 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 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 verso l'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 attuale sul valore target. Ciò è utile quando l'animazione stessa non è l'unica fonte attendibile e deve essere sincronizzata con altri stati, come gli eventi touch. 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 la sezione 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 sono adatte alla maggior parte dei casi d'uso, ma l'utilizzo diretto di TargetBasedAnimation ti consente di controllare personalmente la 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 DecayAnimationSpec fornito.

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.