Punti chiave
Hero Animation è una tecnica di animazione dell’interfaccia in cui un elemento condiviso si muove dolcemente da uno schermo sorgente a uno schermo di destinazione, preservando la sua identità. Il nome riflette la metafora di un “eroe” — un elemento chiave che attraversa una transizione scriptata tra gli schermi, attirando l’attenzione dell’utente. A differenza delle transizioni standard, Hero Animation non rompe la connessione visiva: l’utente vede una traiettoria continua dell’elemento, migliorando la comprensione della gerarchia di navigazione.
Il concetto di Hero Animation è stabilito in Material Design come raccomandazione per schermi lista-dettaglio, gallerie e carte. L’algoritmo di animazione comprende tre fasi: sullo schermo sorgente, l’elemento viene evidenziato e fissato in una posizione assoluta; durante la transizione, si ridimensiona, si sposta e cambia forma in base al contenitore di destinazione; sullo schermo finale, l’elemento viene incorporato nel nuovo layout. Secondo Material Design 3, un’animazione hero correttamente implementata riduce il numero di pressioni errate del pulsante “indietro” del 22%.
In Flutter, Hero Animation è implementata tramite il widget Hero, che accetta un parametro obbligatorio tag — un identificatore univoco. Flutter trova automaticamente una coppia di widget Hero con lo stesso tag sulle route sorgente e destinazione e avvia l’animazione. Il widget Hero non richiede gestione manuale dell’animazione — il framework calcola indipendentemente la traiettoria e applica la trasformazione tramite l’oggetto interno FlightShuttle.
FlightShuttle è una “navetta” invisibile che, al momento della transizione, crea una copia sovrapposta del widget Hero, la posiziona in un sistema di coordinate assoluto e anima la trasformazione (scala, traslazione, rotazione) dalla posizione iniziale a quella finale. Curve personalizzate possono essere passate a FlightShuttle tramite la proprietà flightShuttleBuilder, consentendo di controllare la traiettoria di volo: volare in arco, rimbalzare sui bordi o rallentare prima dell’arrivo. In Flutter 3.16+, Hero supporta l’animazione preservando clipRect e borderRadius, dando l’effetto di carte arrotondate che si trasformano in schermi rettangolari.
In SwiftUI, l’equivalente di Hero Animation è il modificatore matchedGeometryEffect, introdotto in iOS 14. Collega due elementi tramite un identificatore (id) e un namespace. A differenza di Flutter, dove Hero intercetta automaticamente la transizione, SwiftUI richiede una dichiarazione esplicita del namespace e l’applicazione del modificatore a entrambe le parti della scena — sorgente e destinazione — all’interno della stessa gerarchia o tramite GeometryReader.
L’animazione matchedGeometryEffect supporta la trasformazione di posizione, dimensione, rotazione e raggio d’angolo. Per la fluidità, viene specificata un’Animation in withAnimation. SwiftUI interpola automaticamente gli stati intermedi in base alla curva predefinita easeInOut. In iOS 17+, è stato aggiunto il supporto per matchedTransitionSource per TabView e NavigationStack — un analogo di sistema di Hero Animation con traiettoria predefinita per transizioni standard.
In Android, Hero Animation è chiamata Shared Element Transition ed è implementata tramite ActivityOptions.makeSceneTransitionAnimation o FragmentTransaction.addSharedElement. Ogni elemento condiviso viene contrassegnato con un attributo transitionName in XML o tramite codice, e Android gestisce l’animazione tra le attività. A partire da Android 5.0 (API 21), le transizioni sono diventate parte del framework; prima di ciò, animazioni personalizzate dovevano essere scritte usando ValueAnimator.
In Jetpack Compose, Shared Element Transition è implementata tramite i modificatori Modifier.sharedElement e Modifier.sharedBounds in combinazione con AnimatedContent o SharedTransitionLayout (disponibile da Compose BOM 2024.01). A differenza del sistema View, Compose ridisegna gli elementi pigramente, quindi l’elemento condiviso viene memorizzato nella cache in CompositionLocal. Compose supporta anche l’animazione da lista a dettaglio con conservazione dello scrollOffset — una capacità chiave per le transizioni Hero. Secondo Google I/O 2024, Compose Shared Element Transition è il 15% più veloce della versione View grazie alla gestione diretta di RenderNode.
Un esempio di base di Hero Animation in Flutter: una miniatura sullo schermo elenco e un’immagine a schermo intero sullo schermo dettagli. Il tag image-hero collega i due widget, e FlightShuttle anima automaticamente la transizione dalla carta allo schermo intero.
import 'package:flutter/material.dart';
class ListScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView(
children: [
Hero(
tag: 'image-hero',
child: GestureDetector(
onTap: () {
Navigator.push(context,
MaterialPageRoute(
builder: (_) => DetailScreen(),
),
);
},
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.network(
'https://picsum.photos/200',
width: 100, height: 100,
fit: BoxFit.cover,
),
),
),
),
],
),
);
}
}
class DetailScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Detail')),
body: Center(
child: Hero(
tag: 'image-hero',
child: Image.network(
'https://picsum.photos/800',
width: 300, height: 400,
fit: BoxFit.cover,
),
),
),
);
}
}
Il punto chiave è lo stesso tag (image-hero) su entrambi gli schermi. Flutter abbina i widget Hero per tag durante la transizione di route. Se i tag differiscono, l’animazione non partirà — l’elemento apparirà semplicemente sul nuovo schermo senza transizione. È possibile utilizzare qualsiasi tipo per il tag, ma le stringhe sono l’opzione più affidabile.
Un esempio di Hero Animation in SwiftUI con NavigationStack. Il modificatore matchedGeometryEffect collega l’immagine nell’elenco e lo schermo dettagli tramite un namespace, e withAnimation attiva una transizione fluida.
import SwiftUI
struct HeroExampleView: View {
@Namespace private var heroNamespace
@State private var isDetailShown = false
var body: some View {
VStack {
if isDetailShown {
DetailView(namespace: heroNamespace)
.transition(.identity)
} else {
ListView(namespace: heroNamespace)
.transition(.identity)
}
}
.animation(Animation.easeInOut(duration: 0.4),
value: isDetailShown)
}
}
struct ListView: View {
let namespace: Namespace.ID
var body: some View {
Image("thumbnail")
.resizable()
.frame(width: 100, height: 100)
.cornerRadius(12)
.matchedGeometryEffect(id: "hero", in: namespace)
}
}
struct DetailView: View {
let namespace: Namespace.ID
var body: some View {
Image("thumbnail")
.resizable()
.frame(width: 300, height: 400)
.cornerRadius(0)
.matchedGeometryEffect(id: "hero", in: namespace)
}
}
@Namespace crea uno spazio dei nomi per l’identificatore matchedGeometryEffect. Entrambi gli elementi usano lo stesso id (“hero”) e un namespace condiviso. Quando isDetailShown viene commutato, SwiftUI anima la trasformazione di dimensione, posizione e raggio d’angolo tra le due viste, creando l’effetto hero. Per un NavigationLink standard con iOS 16+, utilizzare navigationTransition(.hero).
Un esempio di Shared Element Transition in Jetpack Compose con SharedTransitionLayout. Il modificatore sharedElement anima un elemento condiviso tra schermi durante la navigazione.
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun HeroExample() {
SharedTransitionLayout {
var isDetail by remember { mutableStateOf(false) }
AnimatedContent(
targetState = isDetail,
transitionSpec = {
fadeIn() togetherWith fadeOut()
}
) { detail ->
if (detail) {
Box(
modifier = Modifier
.fillMaxSize()
.sharedElement(
state = rememberSharedContentState(key = "hero"),
animatedVisibilityScope = this
)
) {
AsyncImage(
model = "https://picsum.photos/800",
contentDescription = null,
modifier = Modifier.fillMaxSize()
)
}
} else {
Box(
modifier = Modifier
.size(100.dp)
.sharedElement(
state = rememberSharedContentState(key = "hero"),
animatedVisibilityScope = this
)
) {
AsyncImage(
model = "https://picsum.photos/200",
contentDescription = null,
modifier = Modifier.fillMaxSize()
)
}
}
}
}
}
Compose SharedTransitionLayout (Compose BOM 2024.01+) avvolge la gerarchia, e il modificatore sharedElement con la chiave “hero” collega gli elementi iniziale e finale. AnimatedContent commuta lo stato, e Compose anima automaticamente dimensione, posizione e forma dell’immagine. Un vantaggio chiave è il supporto per l’animazione di bounds e clipBounds, che offre l’effetto di arrotondamento della carta durante la transizione.
Domande frequenti
Sì, Hero Animation è disponibile su entrambe le piattaforme. iOS utilizza matchedGeometryEffect in SwiftUI o UINavigationController.transitioningDelegate. Android utilizza ActivityOptions.makeSceneTransitionAnimation o Shared Element Transition in Jetpack Compose con Modifier.sharedElement. Flutter implementa Hero Animation tramite il widget Hero, che funziona in modo identico su entrambe le piattaforme.
Sì, Hero Animation supporta elementi condivisi multipli. In Flutter, specificare più widget Hero con tag diversi. In SwiftUI, utilizzare matchedGeometryEffect con identificatori diversi su ciascun elemento. Tutte le animazioni vengono eseguite in parallelo — ogni elemento viaggia lungo la propria traiettoria.
Le prestazioni dipendono dall’implementazione. FlightShuttle di Flutter elabora le immagini tramite RenderObject, raggiungendo 60fps. matchedGeometryEffect di SwiftUI è vettorizzato. Per immagini grandi, utilizzare precacheImage in Flutter o prefetch in SwiftUI — questo elimina i ritardi nella prima transizione. Android Compose richiede caricamento differito tramite AsyncImage.
Una transizione push standard sposta l’intero schermo, creando l’illusione di una pila di carte. Hero Animation fissa l’attenzione visiva su un singolo elemento — l’utente vede il suo movimento continuo, migliorando la comprensione della relazione tra gli schermi. Push è adatto per la navigazione gerarchica, Hero per enfatizzare il contenuto.
No, Hero Animation è implementata con strumenti integrati. In Flutter, è il widget Hero del pacchetto flutter/material.dart. In SwiftUI, è il modificatore matchedGeometryEffect. In Android Jetpack Compose, sono SharedTransitionLayout e Modifier.sharedElement di Compose standard. Librerie aggiuntive sono necessarie solo per traiettorie non standard o trasformazioni complesse.
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