Principalele puncte
Hero Animation — o tehnică de animație a interfeței în care un element comun se deplasează lin de pe ecranul sursă pe ecranul țintă, păstrându-și identitatea. Numele reflectă metafora „eroului" — elementul cheie care trece printr-o tranziție scenică între ecrane, atrăgând atenția utilizatorului. Spre deosebire de tranzițiile standard, Hero Animation nu rupe legătura vizuală: utilizatorul vede traiectoria continuă a elementului, ceea ce îmbunătățește înțelegerea ierarhiei de navigare.
Conceptul Hero Animation este consacrat în Material Design ca recomandare pentru ecranele listă-detalii (list-detail), galerii și carduri. Algoritmul de animație constă în trei faze: pe ecranul sursă, elementul este evidențiat și fixat în poziție absolută; în timpul tranziției, este scalat, deplasat și își schimbă forma conform containerului țintă; pe ecranul final, elementul este integrat în noul layout. Conform Material Design 3, o animație a eroului implementată corect reduce numărul de clicuri greșite „înapoi" cu 22%.
În Flutter, Hero Animation este implementată prin widget-ul Hero, care acceptă parametrul obligatoriu tag — un identificator unic. Flutter găsește automat perechea Hero cu același tag pe rutele sursă și țintă și lansează animația. Widget-ul Hero nu necesită gestionare manuală a animației — framework-ul calculează singur traiectoria și aplică transformarea prin obiectul intern FlightShuttle.
FlightShuttle — o „navetă" invizibilă care în momentul tranziției creează o copie suprapusă a widget-ului Hero, o poziționează în sistemul de coordonate absolute și animează transformarea (scale, translate, rotate) de la poziția inițială la cea finală. În FlightShuttle pot fi transmise curbe personalizate prin proprietatea flightShuttleBuilder, permițând controlul traiectoriei de zbor: zbor în arc, sărituri de pe margini sau încetinire înainte de sosire. În Flutter 3.16+, Hero suportă animația cu păstrarea clipRect și borderRadius, oferind efectul de carduri rotunjite care se transformă în ecrane dreptunghiulare.
În SwiftUI, echivalentul Hero Animation este modificatorul matchedGeometryEffect, introdus în iOS 14. Acesta leagă două elemente printr-un identificator (id) și un spațiu de nume (namespace). Spre deosebire de Flutter, unde Hero interceptează automat tranziția, SwiftUI necesită declararea explicită a namespace-ului și aplicarea modificatorului ambelor părți ale scenei — sursă și țintă — în aceeași ierarhie sau prin GeometryReader.
Animația matchedGeometryEffect suportă transformarea poziției, dimensiunii, rotației și a razei colțurilor (cornerRadius). Pentru netezime, se setează Animation în withAnimation. SwiftUI interpolează automat stările intermediare pe baza curbei implicite easeInOut. În iOS 17+ a fost adăugat suportul pentru matchedTransitionSource pentru TabView și NavigationStack — un analog sistemic al Hero Animation cu o traiectorie predefinită pentru tranzițiile standard.
În Android, Hero Animation se numește Shared Element Transition și este implementată prin ActivityOptions.makeSceneTransitionAnimation sau FragmentTransaction.addSharedElement. Fiecare element comun este marcat cu atributul transitionName în XML sau prin cod, iar Android răspunde de animația între activități. Începând cu Android 5.0 (API 21), tranzițiile au devenit parte a framework-ului; înainte, trebuiau scrise animații personalizate prin ValueAnimator.
În Jetpack Compose, Shared Element Transition este implementat prin modificatorul Modifier.sharedElement și Modifier.sharedBounds în combinație cu AnimatedContent sau SharedTransitionLayout (disponibil din Compose BOM 2024.01). Spre deosebire de sistemul View, Compose redesenează elementele leneș, astfel încât elementul comun este stocat în cache în CompositionLocal. Compose suportă, de asemenea, animația listă→detalii (list→detail) cu păstrarea scrollOffset — o capacitate cheie pentru tranzițiile Hero. Conform Google I/O 2024, Compose Shared Element Transition funcționează cu 15% mai rapid decât versiunea View datorită gestionării directe a RenderNode.
Exemplu de bază Hero Animation în Flutter: o miniatură pe ecranul listei și o imagine pe ecran complet pe ecranul de detalii. Tag-ul image-hero leagă cele două widget-uri, iar FlightShuttle animează automat tranziția de la card la ecran complet.
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,
),
),
),
);
}
}
Punctul cheie — același tag (image-hero) pe ambele ecrane. Flutter potrivește widget-urile Hero după tag în timpul tranziției rutei. Dacă tagurile diferă, animația nu va porni — elementul va apărea pe noul ecran fără tranziție. Puteți folosi orice tip pentru tag, dar șirurile de caractere sunt cea mai sigură opțiune.
Exemplu Hero Animation în SwiftUI cu NavigationStack. Modificatorul matchedGeometryEffect leagă imaginea din listă și ecranul de detalii prin namespace, iar withAnimation lansează o tranziție lină.
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 creează un spațiu de nume pentru identificatorul matchedGeometryEffect. Ambele elemente folosesc același id (hero) și namespace comun. La comutarea isDetailShown, SwiftUI animează transformarea dimensiunii, poziției și cornerRadius între cele două view-uri, creând efectul de erou. Pentru NavigationLink standard cu iOS 16+, utilizați navigationTransition(.hero).
Exemplu Shared Element Transition în Jetpack Compose cu SharedTransitionLayout. Modificatorul sharedElement animează elementul comun între ecrane în timpul navigării.
@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+) înconjoară ierarhia, iar modificatorul sharedElement cu cheia hero leagă elementele de început și de sfârșit. AnimatedContent comută starea, iar Compose animează automat dimensiunea, poziția și forma imaginii. Avantajul cheie — suportul pentru animația bounds și clipBounds, care oferă efectul de rotunjire a cardului în timpul tranziției.
Întrebări frecvente
Da, Hero Animation este disponibilă pe ambele platforme. În iOS se utilizează matchedGeometryEffect în SwiftUI sau UINavigationController.transitioningDelegate. În Android — tranziția ActivityOptions.makeSceneTransitionAnimation sau Shared Element Transition în Jetpack Compose cu Modifier.sharedElement. Flutter implementează Hero Animation prin widget-ul Hero, care funcționează identic pe ambele platforme.
Da, Hero Animation suportă elemente comune multiple. În Flutter, specificați mai multe Hero cu taguri diferite. În SwiftUI, utilizați matchedGeometryEffect cu identificatori diferiți pe fiecare element. Toate animațiile se execută în paralel — fiecare element călătorește pe propria traiectorie.
Performanța depinde de implementare. Flutter FlightShuttle procesează imaginile prin RenderObject, oferind 60fps. SwiftUI matchedGeometryEffect este vectorizat. Pentru imagini mari, utilizați precacheImage în Flutter sau prefetch în SwiftUI — acest lucru elimină întârzierile la prima tranziție. Android Compose necesită încărcare leneșă prin AsyncImage.
Tranziția standard push deplasează întregul ecran, creând iluzia unui teanc de cărți. Hero Animation fixează atenția vizuală pe un singur element — utilizatorul vede mișcarea sa continuă, ceea ce îmbunătățește înțelegerea relației dintre ecrane. Push este bun pentru navigarea ierarhică, Hero — pentru accentuarea conținutului.
Nu, Hero Animation este implementată cu instrumente încorporate. În Flutter, este widget-ul Hero din pachetul flutter/material.dart. În SwiftUI — modificatorul matchedGeometryEffect. În Android Jetpack Compose — SharedTransitionLayout și Modifier.sharedElement din Compose standard. Bibliotecile suplimentare sunt necesare doar pentru traiectorii non-standard sau transformări complexe.
Rezumat
Vom dezvolta o aplicație mobilă la cheie
IT Sectr creează aplicații iOS și Android pentru startup-uri și afaceri din 2017. Vă vom consilia și vă vom propune cea mai bună soluție.
Citiți și