Punti Chiave
Timeline è un modello temporale di animazione che definisce quando un'animazione inizia, quanto dura e come il progredire del tempo viene trasformato in cambiamenti del parametro animato. A differenza dell'animazione fotogramma per fotogramma (frame-by-frame), dove ogni fotogramma è impostato manualmente, l'animazione tramite timeline è descritta da parametri: duration — durata totale, startDelay — ritardo prima dell'inizio, repeatCount — numero di ripetizioni, e timingFunction — la curva di velocità.
La timeline opera in uno spazio normalizzato [0, 1] — tempo locale o progresso. Il valore zero corrisponde all'inizio dell'animazione, uno al completamento. Una funzione di easing (smussamento) converte il tempo locale lineare in progresso effettivo dell'animazione. Se repeatCount > 1, la timeline cicla (RESTART) o si inverte (REVERSE). In iOS, CAMediaTiming aggiunge speed (velocità di riproduzione, >1 accelera) e timeOffset (spostamento della timeline). In Android, Animator utilizza setCurrentPlayTime() per il controllo diretto della posizione sulla timeline. Secondo Material Design, le animazioni più corte di 100 ms non vengono percepite come animazioni, mentre quelle più lunghe di 500 ms iniziano a irritare. La durata ottimale per le animazioni di interfaccia è di 200–350 ms.
In iOS, la gestione della timeline di animazione è gestita attraverso il protocollo CAMediaTiming, implementato da CAAnimation e CALayer. Fornisce le proprietà: duration (in secondi), beginTime (tempo di inizio relativo alla timeline genitore), repeatCount (numero di ripetizioni), repeatDuration (durata totale delle ripetizioni), autoreverses (inversione automatica), fillMode (comportamento prima/dopo l'animazione), speed (velocità di riproduzione) e timeOffset (spostamento della timeline).
CACurrentMediaTime() è una funzione che restituisce il tempo assoluto in secondi dall'ultimo riavvio del dispositivo. Viene utilizzata come timestamp di base per sincronizzare più animazioni: impostare beginTime = CACurrentMediaTime() + offset per avviare un gruppo di animazioni con un ritardo preciso. CAMediaTiming supporta anche la gerarchia della timeline — un livello genitore può accelerare/rallentare tutte le animazioni figlie attraverso la proprietà speed. In UIKit, UIViewPropertyAnimator (iOS 10+) astrae la timeline attraverso le proprietà duration, delay e i callback startAnimation/pauseAnimation/stopAnimation. In SwiftUI, la struttura Animation fornisce duration come parte della specifica dell'animazione, ma il controllo diretto della timeline è nascosto — per una timeline personalizzata, utilizzare TimelineView (iOS 15+) con uno scheduler personalizzato.
In Android, la gestione della timeline è centralizzata nella classe Animator e nelle sue sottoclassi (ValueAnimator, ObjectAnimator). Le principali proprietà della timeline sono: duration (in millisecondi), startDelay (ritardo prima dell'inizio), repeatCount (numero di ripetizioni), repeatMode (RESTART o REVERSE). Il metodo setCurrentPlayTime(long) consente di navigare sulla timeline in qualsiasi posizione arbitraria.
ValueAnimator aggiunge un livello di astrazione: genera valori da 0 a 1 (tempo normalizzato) basati sulla duration e li passa a TimeInterpolator. AnimatorSet consente di combinare più Animator in un'unica timeline con esecuzione sequenziale (playSequentially) o parallela (playTogether). In Jetpack Compose, la timeline è gestita tramite AnimationSpec: tween (durationMillis, delayMillis, easing), spring (dampingRatio, stiffness) e keyframes (punti sulla timeline con easing personalizzato per ogni segmento). Compose AnimatedContent utilizza una timeline per transitionSpec — lo sviluppatore descrive come il contenuto appare/scompare con offset temporali. Secondo Android Performance, la timeline di animazione dovrebbe essere un multiplo di 16 ms (ciclo vsync) per 60fps — una durata errata può causare perdita di fotogrammi a causa della mancata corrispondenza con la frequenza di aggiornamento del display.
La timeline diventa particolarmente importante quando si compongono più animazioni. Tre modelli di base: cascata — le animazioni partono in sequenza con un offset (ogni successiva inizia dopo un offset dalla precedente), parallelo — tutte le animazioni iniziano simultaneamente con la stessa timeline, e stagger — le animazioni iniziano con sovrapposizione (la seconda inizia prima che la prima sia completata).
In iOS, la cascata è implementata tramite beginTime di ogni CAAnimation = beginTime precedente + duration precedente. Per lo stagger, CAAnimationGroup viene utilizzato con diversi valori di beginTime nelle animazioni annidate. In Android, AnimatorSet supporta playSequentially e playTogether; lo stagger viene creato impostando startDelay per ogni Animator. In Compose, AnimatedVisibility con enter/exitTransition utilizza lo stagger nella variante staggerChildren per l'apparizione a cascata degli elementi dell'elenco. Alla WWDC e Google I/O 2024, entrambe le piattaforme hanno presentato strumenti di visualizzazione della timeline: Xcode Animations Inspector e Android Studio Animations Timeline — mostrano la timeline effettiva e i fotogrammi persi in tempo reale.
Un esempio di animazione con timeline personalizzata: ritardo di 0,3 s, durata di 0,6 s, inversione automatica e ripetizione infinita. CAMediaTiming controlla tutti i parametri temporali.
import UIKit
func breathingAnimation(layer: CALayer) {
let animation = CABasicAnimation(keyPath: "opacity")
animation.duration = 0.6
animation.beginTime = CACurrentMediaTime() + 0.3
animation.fromValue = 1.0
animation.toValue = 0.3
animation.autoreverses = true
animation.repeatCount = .infinity
animation.speed = 1.0
animation.fillMode = .both
layer.add(animation, forKey: "breathing")
}
beginTime = CACurrentMediaTime() + 0,3 fornisce un ritardo di 300 ms. autoreverses = true fa eseguire l'animazione in direzione inversa dopo ogni ciclo. repeatCount = .infinity crea un ciclo infinito dell'effetto “respirazione”. speed = 1,0 — velocità normale; accelerare l'animazione 2x con un valore di 2,0.
SwiftUI TimelineView (iOS 15+) fornisce accesso alla timeline di sistema per animazioni personalizzate. La View si aggiorna a ogni tick dello scheduler.
import SwiftUI
struct TimelineViewExample: View {
var body: some View {
TimelineView(.periodic(from: .now, by: 0.1)) { context in
let seconds = context.date.timeIntervalSinceReferenceDate
let phase = sin(seconds * 2 * .pi)
Circle()
.fill(Color.blue)
.frame(width: 60 + 30 * phase)
.animation(.linear(duration: 0), value: phase)
}
}
}
TimelineView con scheduler .periodic genera tick ogni 0,1 s. Nella chiusura, context.date è l'ora corrente, che viene convertita in una fase d'onda sinusoidale. La dimensione del cerchio pulsa a una frequenza di 1 Hz. TimelineView è l'unico modo per creare un'animazione con la propria timeline in SwiftUI senza l'API Animation.
ValueAnimator con controllo completo della timeline: ritardo, durata, ripetizione. Il metodo setCurrentPlayTime consente di saltare lungo la timeline.
import android.animation.ValueAnimator
import android.view.animation.AccelerateDecelerateInterpolator
import android.view.View
fun View.timelineAnimation() {
ValueAnimator.ofFloat(0f, 1f).apply {
duration = 400
startDelay = 200
repeatCount = 2
repeatMode = ValueAnimator.REVERSE
interpolator = AccelerateDecelerateInterpolator()
addUpdateListener { animator ->
val progress = animator.animatedFraction
alpha = progress
translationX = progress * 100f
}
start()
}
}
startDelay = 200 ms — ritardo prima dell'inizio. repeatMode = REVERSE riporta l'animazione al valore iniziale. animatedFraction fornisce il progresso da 0 a 1 tenendo conto di repeatMode. addUpdateListener si attiva ogni fotogramma (in genere 16 ms). Utilizzare pause() / resume() per mettere in pausa.
AnimatorSet consente di creare timeline complesse da più animazioni. L'esempio dimostra l'esecuzione sequenziale con sovrapposizione (stagger).
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.view.View
fun View.staggerAnimation() {
val fadeIn = ObjectAnimator.ofFloat(this, "alpha", 0f, 1f).apply {
duration = 300
}
val slideUp = ObjectAnimator.ofFloat(this, "translationY", 100f, 0f).apply {
duration = 400
startDelay = 100
}
val scale = ObjectAnimator.ofFloat(this, "scaleX", 0.8f, 1f).apply {
duration = 250
startDelay = 200
}
AnimatorSet().apply {
playTogether(fadeIn, slideUp, scale)
start()
}
}
playTogether avvia tutte le animazioni in parallelo, ma ognuna ha il proprio startDelay — questo crea un effetto stagger: fadeIn parte per primo (0 ms), slideUp dopo 100 ms, scale dopo 200 ms. La timeline risultante: 0 — 100 — 200 — 600 ms. Per l'esecuzione sequenziale, utilizzare playSequentially.
Domande frequenti
Animator è una classe base astratta per tutte le animazioni. ValueAnimator è la sua sottoclasse che anima valori numerici senza legarsi a un oggetto. ObjectAnimator (una sottoclasse di ValueAnimator) anima una proprietà specifica di un oggetto (ad esempio alpha o translationY). Per la timeline, utilizzare ValueAnimator come timer universale con un callback tramite addUpdateListener.
Utilizzare orologi di sistema (CACurrentMediaTime in iOS, System.nanoTime in Android) per calcolare il progresso, non il numero di fotogrammi. Per la sincronizzazione di rete, utilizzare un timer basato su NTP con correzione dell'offset. iOS supporta AVAudioSession per la sincronizzazione della timeline audio-video con precisione millisecondo. Evitare la sincronizzazione basata sui fotogrammi — le frequenze dei fotogrammi variano da 30 a 120 fps.
startOffset è il ritardo prima dell'inizio dell'animazione in millisecondi. Utilizzato per effetti a cascata e stagger. repeatCount è il numero di ripetizioni: 0 = nessuna ripetizione, ValueAnimator.INFINITE = ciclo infinito. repeatMode determina la direzione: RESTART (reimposta all'inizio) o REVERSE (direzione inversa). In iOS, startOffset si chiama beginTime (in combinazione con CACurrentMediaTime), repeatCount è la stessa proprietà di CABasicAnimation.
Material Design consiglia 200–350 ms per le animazioni di interfaccia. Le animazioni più corte di 100 ms non vengono percepite come animazioni, mentre quelle più lunghe di 500 ms sono irritanti. Per ingresso/uscita degli elementi: 200–300 ms. Per transizioni tra schermate: 300–400 ms. Utilizzare durate più brevi per elementi piccoli (100–200 ms) e più lunghe per elementi grandi (400–500 ms).
Gli scatti (jank) si verificano a causa di: operazioni di lunga durata sul thread principale (bloccano il rendering), gerarchia complessa di View/Layer con overdraw, o uso errato della timeline (durata non multipla di vsync — 16ms per 60fps, 8ms per 120fps). Utilizzare Xcode Instruments Animations o Android Studio Profiler per rilevare i fotogrammi persi. In Compose, utilizzare Modifier.drawWithCache per memorizzare nella cache disegni complessi.
Riepilogo
Svilupperemo un'applicazione mobile chiavi in mano
IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.
Leggi anche