Jetpack Compose: Animazioni Avanzate Passo dopo Passo
Guida completa alle animazioni avanzate in Compose: transizioni, AnimatedVisibility, Animatable, gesture e prestazioni per interfacce Android fluide.

Le animazioni trasformano un'applicazione funzionale in un'esperienza memorabile. Jetpack Compose offre una potente API dichiarativa che semplifica notevolmente la creazione di interfacce fluide. Questa guida esplora le tecniche avanzate per costruire animazioni performanti e manutenibili.
Questo tutorial presuppone familiarità con le basi di Compose (recomposition, stato, modifier). Per le fondamenta conviene consultare prima la guida alle domande di colloquio su Jetpack Compose.
Fondamenti dell'API di Animazione in Compose
Compose mette a disposizione vari livelli di API per le animazioni. La scelta dipende dal livello di controllo desiderato e dalla complessità dell'animazione.
L'API si articola in tre categorie principali: animazioni di alto livello (AnimatedVisibility, AnimatedContent), animazioni basate sullo stato (animate*AsState) e animazioni di basso livello (Animatable, Transition).
// Overview of the three animation API levels
@Composable
fun AnimationApiOverview() {
// High level: simple predefined animations
AnimatedVisibility(visible = isVisible) {
Text("Animated content")
}
// Intermediate level: state-driven animation
val alpha by animateFloatAsState(
targetValue = if (isSelected) 1f else 0.5f,
label = "alpha"
)
// Low level: full control over animation
val animatable = remember { Animatable(0f) }
LaunchedEffect(targetValue) {
animatable.animateTo(targetValue)
}
}Scegliere il livello di API adatto è cruciale per mantenere un codice leggibile pur conservando la flessibilità necessaria.
AnimatedVisibility: Animazioni Eleganti di Entrata e Uscita
AnimatedVisibility è il punto di ingresso ideale per animare la comparsa e la scomparsa degli elementi. Questa API gestisce automaticamente la composizione e la decomposizione del contenuto.
I parametri enter ed exit accettano combinazioni di transizioni che definiscono il comportamento dell'animazione. Le transizioni si possono combinare con l'operatore +.
@Composable
fun ExpandableCard(
title: String,
content: String,
modifier: Modifier = Modifier
) {
var isExpanded by remember { mutableStateOf(false) }
Card(
modifier = modifier.clickable { isExpanded = !isExpanded }
) {
Column(modifier = Modifier.padding(16.dp)) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(text = title, style = MaterialTheme.typography.titleMedium)
Icon(
imageVector = if (isExpanded) Icons.Default.ExpandLess
else Icons.Default.ExpandMore,
contentDescription = null
)
}
// Expansion animation with fade + slide
AnimatedVisibility(
visible = isExpanded,
enter = fadeIn(animationSpec = tween(300)) +
expandVertically(animationSpec = tween(300)),
exit = fadeOut(animationSpec = tween(200)) +
shrinkVertically(animationSpec = tween(200))
) {
Text(
text = content,
modifier = Modifier.padding(top = 12.dp),
style = MaterialTheme.typography.bodyMedium
)
}
}
}
}Il contenuto all'interno di AnimatedVisibility viene composto solo quando visible = true, ottimizzando le prestazioni nelle liste con molti elementi espandibili.
Le transizioni disponibili includono fadeIn/fadeOut, slideIn/slideOut, expandIn/shrinkOut, scaleIn/scaleOut. Si possono combinare liberamente per creare effetti personalizzati.
animate*AsState: Animazioni Guidate dallo Stato
La famiglia di funzioni animate*AsState anima automaticamente i cambiamenti dei valori primitivi. È l'approccio più idiomatico per le animazioni semplici in Compose.
Ogni tipo di dato ha la propria funzione dedicata: animateColorAsState, animateFloatAsState, animateDpAsState, animateIntAsState ecc.
@Composable
fun InteractiveButton(
isSelected: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
// Animated background color
val backgroundColor by animateColorAsState(
targetValue = if (isSelected) MaterialTheme.colorScheme.primary
else MaterialTheme.colorScheme.surfaceVariant,
animationSpec = tween(durationMillis = 250),
label = "backgroundColor"
)
// Animated elevation
val elevation by animateDpAsState(
targetValue = if (isSelected) 8.dp else 2.dp,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
),
label = "elevation"
)
// Animated text size
val textSize by animateFloatAsState(
targetValue = if (isSelected) 18f else 14f,
label = "textSize"
)
Surface(
modifier = modifier.clickable(onClick = onClick),
color = backgroundColor,
shadowElevation = elevation,
shape = RoundedCornerShape(12.dp)
) {
Text(
text = if (isSelected) "Selected" else "Select",
modifier = Modifier.padding(horizontal = 24.dp, vertical = 12.dp),
fontSize = textSize.sp
)
}
}Il parametro animationSpec controlla il comportamento temporale dell'animazione. I due spec più usati sono tween (durata fissa con easing) e spring (fisica realistica con rimbalzo).
Transition: Orchestrare Più Animazioni
Quando più proprietà devono essere animate in modo coordinato, updateTransition fornisce un controllo centralizzato. Questa API garantisce che tutte le animazioni restino sincronizzate.
Il pattern consiste nel definire uno stato enum e poi creare animazioni per ogni proprietà che dipende da quello stato.
// Card state: defines visual behavior
enum class CardState { Collapsed, Expanded, Selected }
@Composable
fun AnimatedStateCard(
cardState: CardState,
modifier: Modifier = Modifier
) {
// Central transition coordinating all animations
val transition = updateTransition(
targetState = cardState,
label = "cardTransition"
)
// Card height based on state
val cardHeight by transition.animateDp(
transitionSpec = { spring(stiffness = Spring.StiffnessLow) },
label = "height"
) { state ->
when (state) {
CardState.Collapsed -> 80.dp
CardState.Expanded -> 200.dp
CardState.Selected -> 160.dp
}
}
// Border color based on state
val borderColor by transition.animateColor(
transitionSpec = { tween(300) },
label = "borderColor"
) { state ->
when (state) {
CardState.Collapsed -> Color.Transparent
CardState.Expanded -> MaterialTheme.colorScheme.outline
CardState.Selected -> MaterialTheme.colorScheme.primary
}
}
// Corner radius based on state
val cornerRadius by transition.animateDp(
label = "cornerRadius"
) { state ->
when (state) {
CardState.Collapsed -> 8.dp
CardState.Expanded -> 16.dp
CardState.Selected -> 24.dp
}
}
Card(
modifier = modifier
.height(cardHeight)
.border(2.dp, borderColor, RoundedCornerShape(cornerRadius)),
shape = RoundedCornerShape(cornerRadius)
) {
// Card content
}
}Pronto a superare i tuoi colloqui su Android?
Pratica con i nostri simulatori interattivi, flashcards e test tecnici.
Animatable: Controllo Totale dell'Animazione
Animatable è l'API di basso livello che offre un controllo programmatico completo. Questo approccio è necessario per animazioni interrompibili, gesture o scenari complessi.
A differenza di animate*AsState, Animatable permette di fermare, invertire o modificare un'animazione in corso senza attenderne il termine.
@Composable
fun SwipeableCard(
onDismiss: () -> Unit,
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
// Horizontal offset controlled by Animatable
val offsetX = remember { Animatable(0f) }
val scope = rememberCoroutineScope()
// Swipe threshold to trigger dismissal
val dismissThreshold = 300f
Box(
modifier = modifier
.offset { IntOffset(offsetX.value.roundToInt(), 0) }
.pointerInput(Unit) {
detectHorizontalDragGestures(
onDragEnd = {
scope.launch {
if (abs(offsetX.value) > dismissThreshold) {
// Animate out then callback
val target = if (offsetX.value > 0) 1000f else -1000f
offsetX.animateTo(
targetValue = target,
animationSpec = tween(200)
)
onDismiss()
} else {
// Return to initial position with spring
offsetX.animateTo(
targetValue = 0f,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy
)
)
}
}
},
onHorizontalDrag = { _, dragAmount ->
scope.launch {
// snapTo for instant finger tracking
offsetX.snapTo(offsetX.value + dragAmount)
}
}
)
}
) {
content()
}
}I metodi chiave di Animatable sono animateTo() (animare verso un obiettivo), snapTo() (cambiamento istantaneo) e stop() (interruzione).
AnimatedContent: Transizioni di Contenuto
AnimatedContent anima le transizioni tra contenuti differenti. Questa API è perfetta per i cambi di stato che modificano completamente l'interfaccia mostrata.
La chiave targetState determina quando deve avvenire una transizione. Il transitionSpec definisce come interagiscono il contenuto in uscita e quello in entrata.
@Composable
fun CounterWithAnimation(
count: Int,
modifier: Modifier = Modifier
) {
AnimatedContent(
targetState = count,
modifier = modifier,
transitionSpec = {
// Determine animation direction
val direction = if (targetState > initialState) {
// New number enters from top
slideInVertically { height -> -height } + fadeIn() togetherWith
slideOutVertically { height -> height } + fadeOut()
} else {
// New number enters from bottom
slideInVertically { height -> height } + fadeIn() togetherWith
slideOutVertically { height -> -height } + fadeOut()
}
direction.using(SizeTransform(clip = false))
},
label = "counter"
) { targetCount ->
Text(
text = "$targetCount",
style = MaterialTheme.typography.displayLarge,
fontWeight = FontWeight.Bold
)
}
}Il contenuto all'interno di AnimatedContent viene ricomposto a ogni cambio di targetState. Per contenuti complessi, conviene memoizzare gli elementi costosi o usare una strategia di chiavi adeguata.
Animazioni Infinite con rememberInfiniteTransition
Per animazioni in loop (indicatori di caricamento, effetti pulsanti), rememberInfiniteTransition fornisce un'API dedicata che non richiede gestione manuale del ciclo.
@Composable
fun PulsingDot(
color: Color = MaterialTheme.colorScheme.primary,
modifier: Modifier = Modifier
) {
val infiniteTransition = rememberInfiniteTransition(label = "pulse")
// Looping scale animation
val scale by infiniteTransition.animateFloat(
initialValue = 0.8f,
targetValue = 1.2f,
animationSpec = infiniteRepeatable(
animation = tween(600, easing = FastOutSlowInEasing),
repeatMode = RepeatMode.Reverse
),
label = "scale"
)
// Synchronized opacity animation
val alpha by infiniteTransition.animateFloat(
initialValue = 0.5f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(600, easing = FastOutSlowInEasing),
repeatMode = RepeatMode.Reverse
),
label = "alpha"
)
Box(
modifier = modifier
.size(24.dp)
.scale(scale)
.alpha(alpha)
.background(color = color, shape = CircleShape)
)
}Animazioni di Liste con LazyColumn
Le animazioni degli elementi di una lista richiedono attenzione particolare. Il modifier animateItem() (in passato animateItemPlacement) anima automaticamente i riordinamenti.
@Composable
fun AnimatedTaskList(
tasks: List<Task>,
onToggle: (Task) -> Unit,
onDelete: (Task) -> Unit,
modifier: Modifier = Modifier
) {
LazyColumn(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(
items = tasks,
key = { it.id } // Stable key required for animateItem
) { task ->
var isVisible by remember { mutableStateOf(true) }
// Exit animation before deletion
AnimatedVisibility(
visible = isVisible,
exit = shrinkVertically() + fadeOut()
) {
TaskItem(
task = task,
onToggle = { onToggle(task) },
onDelete = {
isVisible = false
// Delay to let animation complete
},
modifier = Modifier.animateItem(
fadeInSpec = tween(300),
fadeOutSpec = tween(300),
placementSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
)
)
)
}
// Trigger deletion after animation
LaunchedEffect(isVisible) {
if (!isVisible) {
delay(300)
onDelete(task)
}
}
}
}
}Senza un parametro key stabile, animateItem non riesce a tracciare gli elementi tra le ricomposizioni. Conviene usare un ID univoco invece dell'indice della lista.
Animazioni con Canvas
Per effetti visivi personalizzati, combinare Canvas con valori animati offre la massima flessibilità.
@Composable
fun AnimatedProgressRing(
progress: Float, // 0f to 1f
modifier: Modifier = Modifier
) {
// Progress animation with spring for natural feel
val animatedProgress by animateFloatAsState(
targetValue = progress,
animationSpec = spring(
dampingRatio = Spring.DampingRatioLowBouncy,
stiffness = Spring.StiffnessVeryLow
),
label = "progress"
)
// Continuous rotation animation
val infiniteTransition = rememberInfiniteTransition(label = "rotation")
val rotation by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 360f,
animationSpec = infiniteRepeatable(
animation = tween(2000, easing = LinearEasing)
),
label = "rotation"
)
val primaryColor = MaterialTheme.colorScheme.primary
val trackColor = MaterialTheme.colorScheme.surfaceVariant
Canvas(
modifier = modifier
.size(120.dp)
.rotate(rotation)
) {
val strokeWidth = 12.dp.toPx()
val radius = (size.minDimension - strokeWidth) / 2
// Background circle (track)
drawCircle(
color = trackColor,
radius = radius,
style = Stroke(width = strokeWidth, cap = StrokeCap.Round)
)
// Animated progress arc
drawArc(
color = primaryColor,
startAngle = -90f,
sweepAngle = animatedProgress * 360f,
useCenter = false,
style = Stroke(width = strokeWidth, cap = StrokeCap.Round),
topLeft = Offset(strokeWidth / 2, strokeWidth / 2),
size = Size(radius * 2, radius * 2)
)
}
}Pronto a superare i tuoi colloqui su Android?
Pratica con i nostri simulatori interattivi, flashcards e test tecnici.
Ottimizzazione delle Prestazioni delle Animazioni
Le animazioni mal ottimizzate possono provocare jank e scaricare la batteria. Ecco le buone pratiche per mantenere i 60 FPS.
La prima regola consiste nell'evitare le allocazioni durante l'animazione. Conviene usare graphicsLayer invece di modifier che innescano ricomposizioni.
@Composable
fun OptimizedAnimatedCard(
isExpanded: Boolean,
modifier: Modifier = Modifier
) {
val scale by animateFloatAsState(
targetValue = if (isExpanded) 1.1f else 1f,
label = "scale"
)
val alpha by animateFloatAsState(
targetValue = if (isExpanded) 1f else 0.8f,
label = "alpha"
)
Card(
modifier = modifier
// ✅ graphicsLayer: GPU modifications without recomposition
.graphicsLayer {
scaleX = scale
scaleY = scale
this.alpha = alpha
}
// ❌ Avoid: .scale(scale).alpha(alpha)
// These modifiers trigger recompositions
) {
Text("Card content")
}
}
// Example with remembered lambda to avoid allocations
@Composable
fun OptimizedClickableItem(
onClick: () -> Unit,
content: @Composable () -> Unit
) {
// ✅ Stable remembered lambda
val interactionSource = remember { MutableInteractionSource() }
Box(
modifier = Modifier
.clickable(
interactionSource = interactionSource,
indication = ripple(),
onClick = onClick
)
) {
content()
}
}Il secondo punto critico riguarda le animazioni nelle liste. Conviene limitare il numero di animazioni simultanee e usare derivedStateOf per i calcoli derivati.
@Composable
fun PerformantAnimatedList(
items: List<Item>,
modifier: Modifier = Modifier
) {
// Calculate once whether the list is empty
val isEmpty by remember {
derivedStateOf { items.isEmpty() }
}
LazyColumn(modifier = modifier) {
items(
items = items,
key = { it.id }
) { item ->
// Lightweight animation only on initial appearance
var hasAppeared by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
hasAppeared = true
}
val alpha by animateFloatAsState(
targetValue = if (hasAppeared) 1f else 0f,
animationSpec = tween(200),
label = "itemAlpha"
)
ItemCard(
item = item,
modifier = Modifier.graphicsLayer { this.alpha = alpha }
)
}
}
}Conclusione
Le animazioni in Jetpack Compose offrono un equilibrio tra facilità d'uso e controllo avanzato. Ecco i punti chiave da ricordare:
- ✅ Scegliere il livello di API adeguato in base alla complessità (AnimatedVisibility → animate*AsState → Animatable)
- ✅ Usare
updateTransitionper coordinare più animazioni correlate - ✅ Preferire
springper animazioni naturali etweenper durate precise - ✅ Fornire sempre un parametro
keystabile per le animazioni di lista - ✅ Ottimizzare con
graphicsLayerper evitare ricomposizioni inutili - ✅ Testare le animazioni su dispositivi reali per validarne le prestazioni
Padroneggiare le animazioni di Compose distingue le applicazioni Android professionali. Queste tecniche, unite all'attenzione per le prestazioni, permettono di creare esperienze utente fluide e coinvolgenti.
Inizia a praticare!
Metti alla prova le tue conoscenze con i nostri simulatori di colloquio e test tecnici.
Tag
Condividi
Articoli correlati

Le 20 domande più frequenti su Jetpack Compose nei colloqui 2026
Le 20 domande più frequenti su Jetpack Compose nei colloqui tecnici: recomposition, gestione dello stato, navigazione, performance e pattern architetturali.

MVVM vs MVI su Android: Quale Architettura Scegliere nel 2026?
Confronto approfondito tra MVVM e MVI su Android: vantaggi, limiti, casi d'uso e una guida pratica per scegliere l'architettura giusta nel 2026.

Kotlin Coroutines per Android: Guida Completa 2026
Guida approfondita alle coroutine Kotlin per lo sviluppo Android: funzioni suspend, scope, dispatcher, Flow e pattern avanzati.