Guida rapida alle animazioni in Compose

Compose ha molti meccanismi di animazione integrati e può essere difficile sapere quale scegliere. Di seguito è riportato un elenco di casi d'uso comuni delle animazioni. Per informazioni più dettagliate sull'insieme completo delle diverse opzioni API disponibili, leggi la documentazione completa di Compose Animation.

Anima le proprietà componibili comuni

Compose fornisce API pratiche che ti consentono di risolvere molti casi d'uso comuni delle animazioni. Questa sezione mostra come animare le proprietà comuni di un composable.

Anima la comparsa / scomparsa

Elemento componibile verde che si mostra e si nasconde
Figura 1. Animazione della comparsa e della scomparsa di un elemento in una colonna

Utilizza AnimatedVisibility per nascondere o mostrare un componibile. I componenti secondari all'interno di AnimatedVisibility possono utilizzare Modifier.animateEnterExit() per la propria transizione di entrata o uscita.

var visible by remember {
    mutableStateOf(true)
}
// Animated visibility will eventually remove the item from the composition once the animation has finished.
AnimatedVisibility(visible) {
    // your composable here
    // ...
}

I parametri enter ed exit di AnimatedVisibility ti consentono di configurare il comportamento di un composable quando appare e scompare. Per saperne di più, consulta la documentazione completa.

Un'altra opzione per animare la visibilità di un composable è animare l'alpha nel tempo utilizzando animateFloatAsState:

var visible by remember {
    mutableStateOf(true)
}
val animatedAlpha by animateFloatAsState(
    targetValue = if (visible) 1.0f else 0f,
    label = "alpha"
)
Box(
    modifier = Modifier
        .size(200.dp)
        .graphicsLayer {
            alpha = animatedAlpha
        }
        .clip(RoundedCornerShape(8.dp))
        .background(colorGreen)
        .align(Alignment.TopCenter)
) {
}

Tuttavia, la modifica dell'alpha comporta l'avvertenza che l'elemento composable rimane nella composizione e continua a occupare lo spazio in cui è disposto. Questo potrebbe far sì che gli screen reader e altri meccanismi di accessibilità considerino ancora l'elemento sullo schermo. D'altra parte, AnimatedVisibility alla fine rimuove l'elemento dalla composizione.

Animazione dell'alpha di un composable
Figura 2. Animazione dell'alpha di un composable

Anima il colore di sfondo

Composable con colore di sfondo che cambia nel tempo come animazione, in cui i colori sfumano l'uno nell'altro.
Figura 3. Animazione del colore di sfondo del composable

val animatedColor by animateColorAsState(
    if (animateBackgroundColor) colorGreen else colorBlue,
    label = "color"
)
Column(
    modifier = Modifier.drawBehind {
        drawRect(animatedColor)
    }
) {
    // your composable here
}

Questa opzione è più efficiente rispetto all'utilizzo di Modifier.background(). Modifier.background() è accettabile per un'impostazione del colore one-shot, ma quando un colore viene animato nel tempo, potrebbe causare più ricomposizioni del necessario.

Per animare all'infinito il colore di sfondo, consulta la sezione Ripetizione di un'animazione.

Anima le dimensioni di un composable

Un composable verde che anima la modifica delle dimensioni in modo fluido.
Figura 4. Composable che si anima in modo fluido tra una dimensione piccola e una più grande

Compose ti consente di animare le dimensioni dei componibili in diversi modi. Utilizza animateContentSize() per le animazioni tra le modifiche delle dimensioni dei componibili.

Ad esempio, se hai una casella che contiene testo che può espandersi da una a più righe, puoi utilizzare Modifier.animateContentSize() per ottenere una transizione più fluida:

var expanded by remember { mutableStateOf(false) }
Box(
    modifier = Modifier
        .background(colorBlue)
        .animateContentSize()
        .height(if (expanded) 400.dp else 200.dp)
        .fillMaxWidth()
        .clickable(
            interactionSource = remember { MutableInteractionSource() },
            indication = null
        ) {
            expanded = !expanded
        }

) {
}

Puoi anche utilizzare AnimatedContent, con un SizeTransform per descrivere come devono avvenire le modifiche alle dimensioni.

Anima la posizione del composable

Elemento componibile verde che si anima in modo fluido verso il basso e verso destra
Figura 5. Composable che si sposta di un offset

Per animare la posizione di un componibile, utilizza Modifier.offset{ } in combinazione con animateIntOffsetAsState().

var moved by remember { mutableStateOf(false) }
val pxToMove = with(LocalDensity.current) {
    100.dp.toPx().roundToInt()
}
val offset by animateIntOffsetAsState(
    targetValue = if (moved) {
        IntOffset(pxToMove, pxToMove)
    } else {
        IntOffset.Zero
    },
    label = "offset"
)

Box(
    modifier = Modifier
        .offset {
            offset
        }
        .background(colorBlue)
        .size(100.dp)
        .clickable(
            interactionSource = remember { MutableInteractionSource() },
            indication = null
        ) {
            moved = !moved
        }
)

Se vuoi assicurarti che i componibili non vengano disegnati sopra o sotto altri componibili durante l'animazione della posizione o delle dimensioni, utilizza Modifier.layout{ }. Questo modificatore propaga le modifiche di dimensioni e posizione all'elemento principale, che poi influisce sugli altri elementi secondari.

Ad esempio, se stai spostando un Box all'interno di un Column e gli altri elementi secondari devono spostarsi quando si sposta il Box, includi le informazioni sull'offset con Modifier.layout{ } come segue:

var toggled by remember {
    mutableStateOf(false)
}
val interactionSource = remember {
    MutableInteractionSource()
}
Column(
    modifier = Modifier
        .padding(16.dp)
        .fillMaxSize()
        .clickable(indication = null, interactionSource = interactionSource) {
            toggled = !toggled
        }
) {
    val offsetTarget = if (toggled) {
        IntOffset(150, 150)
    } else {
        IntOffset.Zero
    }
    val offset = animateIntOffsetAsState(
        targetValue = offsetTarget, label = "offset"
    )
    Box(
        modifier = Modifier
            .size(100.dp)
            .background(colorBlue)
    )
    Box(
        modifier = Modifier
            .layout { measurable, constraints ->
                val offsetValue = if (isLookingAhead) offsetTarget else offset.value
                val placeable = measurable.measure(constraints)
                layout(placeable.width + offsetValue.x, placeable.height + offsetValue.y) {
                    placeable.placeRelative(offsetValue)
                }
            }
            .size(100.dp)
            .background(colorGreen)
    )
    Box(
        modifier = Modifier
            .size(100.dp)
            .background(colorBlue)
    )
}

2 riquadri,con il secondo riquadro che anima la sua posizione X, Y e il terzo riquadro che risponde spostandosi anche della quantità Y.
Figura 6. Animazione con Modifier.layout{ }

Animare il padding di un composable

Elemento componibile verde che si rimpicciolisce e si ingrandisce al clic, con spaziatura interna animata
Figura 7. Composable con il padding animato

Per animare il padding di un componibile, utilizza animateDpAsState combinato con Modifier.padding():

var toggled by remember {
    mutableStateOf(false)
}
val animatedPadding by animateDpAsState(
    if (toggled) {
        0.dp
    } else {
        20.dp
    },
    label = "padding"
)
Box(
    modifier = Modifier
        .aspectRatio(1f)
        .fillMaxSize()
        .padding(animatedPadding)
        .background(Color(0xff53D9A1))
        .clickable(
            interactionSource = remember { MutableInteractionSource() },
            indication = null
        ) {
            toggled = !toggled
        }
)

Animare l'elevazione di un composable

Figura 8. Animazione dell'elevazione del composable al clic

Per animare l'elevazione di un componibile, utilizza animateDpAsState in combinazione con Modifier.graphicsLayer{ }. Per le modifiche di elevazione una tantum, utilizza Modifier.shadow(). Se stai animando l'ombra, l'utilizzo del modificatore Modifier.graphicsLayer{ } è l'opzione più efficiente.

val mutableInteractionSource = remember {
    MutableInteractionSource()
}
val pressed = mutableInteractionSource.collectIsPressedAsState()
val elevation = animateDpAsState(
    targetValue = if (pressed.value) {
        32.dp
    } else {
        8.dp
    },
    label = "elevation"
)
Box(
    modifier = Modifier
        .size(100.dp)
        .align(Alignment.Center)
        .graphicsLayer {
            this.shadowElevation = elevation.value.toPx()
        }
        .clickable(interactionSource = mutableInteractionSource, indication = null) {
        }
        .background(colorGreen)
) {
}

In alternativa, utilizza il composable Card e imposta la proprietà elevation su valori diversi per ogni stato.

Animare la scala, la traslazione o la rotazione del testo

Testo componibile con la scritta "Hello" che si anima tra una dimensione piccola e una più grande.
Figura 9. Testo che si anima in modo fluido tra due dimensioni

Quando animi la scala, la traslazione o la rotazione del testo, imposta il parametro textMotion su TextStyle su TextMotion.Animated. In questo modo, le transizioni tra le animazioni di testo saranno più fluide. Utilizza Modifier.graphicsLayer{ } per tradurre, ruotare o ridimensionare il testo.

val infiniteTransition = rememberInfiniteTransition(label = "infinite transition")
val scale by infiniteTransition.animateFloat(
    initialValue = 1f,
    targetValue = 8f,
    animationSpec = infiniteRepeatable(tween(1000), RepeatMode.Reverse),
    label = "scale"
)
Box(modifier = Modifier.fillMaxSize()) {
    Text(
        text = "Hello",
        modifier = Modifier
            .graphicsLayer {
                scaleX = scale
                scaleY = scale
                transformOrigin = TransformOrigin.Center
            }
            .align(Alignment.Center),
        // Text composable does not take TextMotion as a parameter.
        // Provide it via style argument but make sure that we are copying from current theme
        style = LocalTextStyle.current.copy(textMotion = TextMotion.Animated)
    )
}

Colore del testo animato

Le parole Hello Compose che cambiano colore tra verde e blu
Figura 10. Esempio che mostra l'animazione del colore del testo

Per animare il colore del testo, utilizza la funzione lambda color sul componibile BasicText:

val infiniteTransition = rememberInfiniteTransition(label = "infinite transition")
val animatedColor by infiniteTransition.animateColor(
    initialValue = Color(0xFF60DDAD),
    targetValue = Color(0xFF4285F4),
    animationSpec = infiniteRepeatable(tween(1000), RepeatMode.Reverse),
    label = "color"
)

BasicText(
    text = "Hello Compose",
    color = {
        animatedColor
    },
    // ...
)

Passa da un tipo di contenuto all'altro

Schermata verde con la scritta "Caricamento in corso", schermata blu con la scritta "Caricato" e schermata bianca con la scritta "Errore". I diversi elementi componibili vengono ripetuti con una semplice animazione.
Figura 11. Utilizzo di AnimatedContent per animare le modifiche tra diversi componibili (rallentato)

Utilizza AnimatedContent per animare tra diversi componibili. Se vuoi solo una dissolvenza standard tra i componibili, utilizza Crossfade.

var state by remember {
    mutableStateOf(UiState.Loading)
}
AnimatedContent(
    state,
    transitionSpec = {
        fadeIn(
            animationSpec = tween(3000)
        ) togetherWith fadeOut(animationSpec = tween(3000))
    },
    modifier = Modifier.clickable(
        interactionSource = remember { MutableInteractionSource() },
        indication = null
    ) {
        state = when (state) {
            UiState.Loading -> UiState.Loaded
            UiState.Loaded -> UiState.Error
            UiState.Error -> UiState.Loading
        }
    },
    label = "Animated Content"
) { targetState ->
    when (targetState) {
        UiState.Loading -> {
            LoadingScreen()
        }
        UiState.Loaded -> {
            LoadedScreen()
        }
        UiState.Error -> {
            ErrorScreen()
        }
    }
}

AnimatedContent può essere personalizzato per mostrare molti tipi diversi di transizioni di entrata e uscita. Per saperne di più, consulta la documentazione su AnimatedContent o leggi questo blog post su AnimatedContent.

Animazione durante la navigazione verso destinazioni diverse

Due composabili, uno verde con la dicitura Landing e uno blu con la dicitura Detail, animati facendo scorrere il composabile dei dettagli sopra quello della pagina di destinazione.
Figura 12. Animazione tra composabili utilizzando navigation-compose

Per animare le transizioni tra i componibili quando utilizzi l'artefatto navigation-compose, specifica enterTransition e exitTransition su un componibile. Puoi anche impostare l'animazione predefinita da utilizzare per tutte le destinazioni nel livello superiore NavHost:

val navController = rememberNavController()
NavHost(
    navController = navController, startDestination = "landing",
    enterTransition = { EnterTransition.None },
    exitTransition = { ExitTransition.None }
) {
    composable("landing") {
        ScreenLanding(
            // ...
        )
    }
    composable(
        "detail/{photoUrl}",
        arguments = listOf(navArgument("photoUrl") { type = NavType.StringType }),
        enterTransition = {
            fadeIn(
                animationSpec = tween(
                    300, easing = LinearEasing
                )
            ) + slideIntoContainer(
                animationSpec = tween(300, easing = EaseIn),
                towards = AnimatedContentTransitionScope.SlideDirection.Start
            )
        },
        exitTransition = {
            fadeOut(
                animationSpec = tween(
                    300, easing = LinearEasing
                )
            ) + slideOutOfContainer(
                animationSpec = tween(300, easing = EaseOut),
                towards = AnimatedContentTransitionScope.SlideDirection.End
            )
        }
    ) { backStackEntry ->
        ScreenDetails(
            // ...
        )
    }
}

Esistono molti tipi diversi di transizioni di entrata e uscita che applicano effetti diversi ai contenuti in entrata e in uscita. Per saperne di più, consulta la documentazione.

Ripeti un'animazione

Uno sfondo verde che si trasforma in uno sfondo blu, all'infinito, con un'animazione tra i due colori.
Figura 13. Colore di sfondo che si anima tra due valori, all'infinito

Utilizza rememberInfiniteTransition con un infiniteRepeatable animationSpec per ripetere continuamente l'animazione. Modifica RepeatModes per specificare come deve andare avanti e indietro.

Utilizza repeatable per ripetere un numero di volte prestabilito.

val infiniteTransition = rememberInfiniteTransition(label = "infinite")
val color by infiniteTransition.animateColor(
    initialValue = Color.Green,
    targetValue = Color.Blue,
    animationSpec = infiniteRepeatable(
        animation = tween(1000, easing = LinearEasing),
        repeatMode = RepeatMode.Reverse
    ),
    label = "color"
)
Column(
    modifier = Modifier.drawBehind {
        drawRect(color)
    }
) {
    // your composable here
}

Avvia un'animazione al lancio di un composable

LaunchedEffect viene eseguito quando un composable entra nella composizione. Avvia un'animazione al lancio di un composable, che puoi utilizzare per gestire la modifica dello stato dell'animazione. Utilizzo di Animatable con il metodo animateTo per avviare l'animazione all'avvio:

val alphaAnimation = remember {
    Animatable(0f)
}
LaunchedEffect(Unit) {
    alphaAnimation.animateTo(1f)
}
Box(
    modifier = Modifier.graphicsLayer {
        alpha = alphaAnimation.value
    }
)

Crea animazioni sequenziali

Quattro cerchi con frecce verdi che si animano tra l'uno e l'altro, una dopo l'altra.
Figura 14. Diagramma che mostra l'avanzamento di un'animazione sequenziale, una per una.

Utilizza le API di coroutine Animatable per eseguire animazioni sequenziali o simultanee. La chiamata di animateTo su Animatable una dopo l'altra fa sì che ogni animazione attenda il completamento delle animazioni precedenti prima di procedere. Questo perché si tratta di una funzione di sospensione.

val alphaAnimation = remember { Animatable(0f) }
val yAnimation = remember { Animatable(0f) }

LaunchedEffect("animationKey") {
    alphaAnimation.animateTo(1f)
    yAnimation.animateTo(100f)
    yAnimation.animateTo(500f, animationSpec = tween(100))
}

Crea animazioni simultanee

Tre cerchi con frecce verdi che si animano verso ciascuno di essi, tutte insieme contemporaneamente.
Figura 15. Diagramma che mostra come avanzano le animazioni simultanee, tutte contemporaneamente.

Utilizza le API coroutine (Animatable#animateTo() o animate) o l'API Transition per ottenere animazioni simultanee. Se utilizzi più funzioni di lancio in un contesto di coroutine, le animazioni vengono avviate contemporaneamente:

val alphaAnimation = remember { Animatable(0f) }
val yAnimation = remember { Animatable(0f) }

LaunchedEffect("animationKey") {
    launch {
        alphaAnimation.animateTo(1f)
    }
    launch {
        yAnimation.animateTo(100f)
    }
}

Puoi utilizzare l'API updateTransition per utilizzare lo stesso stato per gestire molte animazioni di proprietà diverse contemporaneamente. L'esempio seguente anima due proprietà controllate da una modifica dello stato, rect e borderWidth:

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

val rect by transition.animateRect(label = "rect") { state ->
    when (state) {
        BoxState.Collapsed -> Rect(0f, 0f, 100f, 100f)
        BoxState.Expanded -> Rect(100f, 100f, 300f, 300f)
    }
}
val borderWidth by transition.animateDp(label = "borderWidth") { state ->
    when (state) {
        BoxState.Collapsed -> 1.dp
        BoxState.Expanded -> 0.dp
    }
}

Ottimizza le prestazioni delle animazioni

Le animazioni in Compose possono causare problemi di prestazioni. Ciò è dovuto alla natura di un'animazione: spostare o modificare rapidamente i pixel sullo schermo, fotogramma per fotogramma, per creare l'illusione del movimento.

Considera le diverse fasi di Compose: composizione, layout e disegno. Se l'animazione modifica la fase di layout, tutti i composabili interessati devono essere ridisposti e ridisegnati. Se l'animazione si verifica nella fase di disegno, per impostazione predefinita è più efficiente rispetto all'esecuzione dell'animazione nella fase di layout, in quanto il lavoro complessivo da svolgere è minore.

Per assicurarti che l'app esegua il minor numero possibile di operazioni durante l'animazione, scegli la versione lambda di un Modifier, se possibile. In questo modo, la ricomposizione viene saltata e l'animazione viene eseguita al di fuori della fase di composizione. In caso contrario, utilizza Modifier.graphicsLayer{ }, poiché questo modificatore viene sempre eseguito nella fase di disegno. Per saperne di più, consulta la sezione sulla posticipazione delle letture nella documentazione sulle prestazioni.

Modifica la durata dell'animazione

Per impostazione predefinita, Compose utilizza animazioni con effetto molla per la maggior parte delle animazioni. Le molle, o animazioni basate sulla fisica, sembrano più naturali. Sono anche interrompibili, in quanto tengono conto della velocità attuale dell'oggetto, anziché di un tempo fisso. Se vuoi eseguire l'override dell'impostazione predefinita, tutte le API Animation mostrate in precedenza hanno la possibilità di impostare un animationSpec per personalizzare l'esecuzione di un'animazione, ad esempio se vuoi che venga eseguita per una determinata durata o che sia più rimbalzante.

Di seguito è riportato un riepilogo delle diverse opzioni di animationSpec:

  • spring: animazione basata sulla fisica, l'impostazione predefinita per tutte le animazioni. Puoi modificare la rigidità o il rapporto di smorzamento per ottenere un aspetto diverso dell'animazione.
  • tween (abbreviazione di between): animazione basata sulla durata, anima tra due valori con una funzione Easing.
  • keyframes: specifica per indicare i valori in determinati punti chiave di un'animazione.
  • repeatable: specifica basata sulla durata che viene eseguita un determinato numero di volte, specificato da RepeatMode.
  • infiniteRepeatable: specifica basata sulla durata che viene eseguita per sempre.
  • snap: passa istantaneamente al valore finale senza alcuna animazione.
Due animazioni che mostrano la differenza tra l'assenza di un set di specifiche e un set di specifiche Spring personalizzato.
Figura 16. Nessun set di specifiche rispetto al set di specifiche Spring personalizzato

Leggi la documentazione completa per saperne di più su animationSpecs.

Risorse aggiuntive

Per altri esempi di animazioni divertenti in Compose, dai un'occhiata ai seguenti: