Jetpack Compose: Animazioni Avanzate Passo dopo Passo

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

Animazioni avanzate di Jetpack Compose per sviluppatori Android

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.

Prerequisiti

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

AnimationLevels.ktkotlin
// 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 +.

AnimatedVisibilityExample.ktkotlin
@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.

Combinare le transizioni

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.

AnimateAsStateExample.ktkotlin
@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.

TransitionExample.ktkotlin
// 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.

AnimatableExample.ktkotlin
@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.

AnimatedContentExample.ktkotlin
@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
        )
    }
}
Prestazioni con AnimatedContent

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.

InfiniteTransitionExample.ktkotlin
@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.

ListAnimationExample.ktkotlin
@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)
                }
            }
        }
    }
}
Chiavi stabili per le animazioni di lista

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à.

CanvasAnimationExample.ktkotlin
@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.

PerformanceOptimization.ktkotlin
@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.

ListPerformance.ktkotlin
@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 updateTransition per coordinare più animazioni correlate
  • ✅ Preferire spring per animazioni naturali e tween per durate precise
  • ✅ Fornire sempre un parametro key stabile per le animazioni di lista
  • ✅ Ottimizzare con graphicsLayer per 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

#jetpack compose
#android
#animations
#kotlin
#ui

Condividi

Articoli correlati