Najważniejsze
Hero Animation — technika animacji interfejsu, w której wspólny element płynnie przemieszcza się z ekranu źródłowego do docelowego, zachowując swoją tożsamość. Nazwa odzwierciedla metaforę „bohatera" — kluczowego elementu, który przechodzi przez sceniczne przejście między ekranami, przyciągając uwagę użytkownika. W przeciwieństwie do standardowych przejść, Hero Animation nie zrywa więzi wizualnej: użytkownik widzi ciągłą trajektorię elementu, co poprawia zrozumienie hierarchii nawigacyjnej.
Koncepcja Hero Animation jest ugruntowana w Material Design jako zalecenie dla ekranów lista-szczegóły (list-detail), galerii i kart. Algorytm animacji składa się z trzech faz: na ekranie źródłowym element jest podświetlany i ustalany w pozycji absolutnej; podczas przejścia jest skalowany, przemieszczany i zmienia kształt zgodnie z docelowym kontenerem; na ekranie końcowym element jest wbudowywany w nowy układ. Według danych Material Design 3, poprawnie zaimplementowana animacja bohatera zmniejsza liczbę błędnych kliknięć „wstecz" o 22%.
We Flutter Hero Animation jest realizowana przez widget Hero, który przyjmuje obowiązkowy parametr tag — unikalny identyfikator. Flutter automatycznie znajduje parę Hero z tym samym tagiem na trasie źródłowej i docelowej oraz uruchamia animację. Widget Hero nie wymaga ręcznego zarządzania animacją — framework sam oblicza trajektorię i stosuje transformację przez wewnętrzny obiekt FlightShuttle.
FlightShuttle — niewidoczny „prom", który w momencie przejścia tworzy nakładaną kopię widgetu Hero, pozycjonuje ją w systemie współrzędnych absolutnych i animuje transform (scale, translate, rotate) od pozycji początkowej do końcowej. Do FlightShuttle można przekazać niestandardowe krzywe przez właściwość flightShuttleBuilder, co pozwala kontrolować trajektorię lotu: lecieć po łuku, odbijać się od granic lub zwalniać przed przybyciem. We Flutter 3.16+ Hero wspiera animację z zachowaniem clipRect i borderRadius, co daje efekt zaokrąglonych kart transformujących się w prostokątne ekrany.
W SwiftUI odpowiednik Hero Animation to modyfikator matchedGeometryEffect, wprowadzony w iOS 14. Łączy on dwa elementy przez identyfikator (id) i przestrzeń nazw (namespace). W przeciwieństwie do Flutter, gdzie Hero automatycznie przechwytuje przejście, SwiftUI wymaga jawnego zadeklarowania namespace i zastosowania modyfikatora do obu części sceny — źródłowej i docelowej — w tej samej hierarchii lub przez GeometryReader.
Animacja matchedGeometryEffect wspiera transformację pozycji, rozmiaru, obrotu i zaokrąglenia rogów (cornerRadius). Dla osiągnięcia płynności ustawia się Animation w withAnimation. SwiftUI automatycznie interpoluje stany pośrednie na podstawie domyślnej krzywej easeInOut. W iOS 17+ dodano wsparcie dla matchedTransitionSource dla TabView i NavigationStack — systemowy odpowiednik Hero Animation z predefiniowaną trajektorią dla standardowych przejść.
W Android Hero Animation nazywa się Shared Element Transition i jest realizowana przez ActivityOptions.makeSceneTransitionAnimation lub FragmentTransaction.addSharedElement. Każdy wspólny element jest oznaczany atrybutem transitionName w XML lub przez kod, a Android odpowiada za animację między aktywnościami. Od Android 5.0 (API 21) przejścia stały się częścią frameworka; wcześniej trzeba było pisać niestandardowe animacje przez ValueAnimator.
W Jetpack Compose Shared Element Transition jest realizowany przez modyfikator Modifier.sharedElement i Modifier.sharedBounds w połączeniu z AnimatedContent lub SharedTransitionLayout (dostępny od Compose BOM 2024.01). W przeciwieństwie do systemu View, Compose leniwie przerysowuje elementy, więc wspólny element jest cachowany w CompositionLocal. Compose wspiera również animację listy do szczegółów (list→detail) z zachowaniem scrollOffset — kluczowa możliwość dla przejść Hero. Według Google I/O 2024, Compose Shared Element Transition działa o 15% szybciej niż wersja View dzięki bezpośredniemu zarządzaniu RenderNode.
Podstawowy przykład Hero Animation we Flutter: miniatura na ekranie listy i obraz pełnoekranowy na ekranie szczegółów. Tag image-hero łączy dwa widgety, a FlightShuttle automatycznie animuje przejście od karty do pełnego ekranu.
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,
),
),
),
);
}
}
Kluczowy moment — taki sam tag (image-hero) na obu ekranach. Flutter dopasowuje widgety Hero po tagu podczas przejścia trasy. Jeśli tagi się różnią, animacja nie uruchomi się — element pojawi się na nowym ekranie bez przejścia. Można użyć dowolnego typu dla tagu, ale ciągi znaków są najbardziej niezawodną opcją.
Przykład Hero Animation w SwiftUI z NavigationStack. Modyfikator matchedGeometryEffect łączy obraz na liście i ekranie szczegółów przez namespace, a withAnimation uruchamia płynne przejście.
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 tworzy przestrzeń nazw dla identyfikatora matchedGeometryEffect. Oba elementy używają tego samego id (hero) i wspólnego namespace. Podczas przełączania isDetailShown, SwiftUI animuje transformację rozmiaru, pozycji i cornerRadius między dwoma widokami, tworząc efekt bohatera. Dla standardowego NavigationLink z iOS 16+ użyj navigationTransition(.hero).
Przykład Shared Element Transition w Jetpack Compose z SharedTransitionLayout. Modyfikator sharedElement animuje wspólny element między ekranami podczas nawigacji.
@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+) otacza hierarchię, a modyfikator sharedElement z kluczem hero łączy element początkowy i końcowy. AnimatedContent przełącza stan, a Compose automatycznie animuje rozmiar, pozycję i kształt obrazu. Kluczową zaletą jest wsparcie dla animacji bounds i clipBounds, co daje efekt zaokrąglenia karty podczas przejścia.
Często zadawane pytania
Tak, Hero Animation jest dostępna na obu platformach. W iOS używa się matchedGeometryEffect w SwiftUI lub UINavigationController.transitioningDelegate. W Android — przejścia ActivityOptions.makeSceneTransitionAnimation lub Shared Element Transition w Jetpack Compose z Modifier.sharedElement. Flutter realizuje Hero Animation przez widget Hero, działający identycznie na obu platformach.
Tak, Hero Animation wspiera wiele wspólnych elementów. We Flutter wskaż kilka Hero z różnymi tagami. W SwiftUI użyj matchedGeometryEffect z różnymi identyfikatorami na każdym elemencie. Wszystkie animacje są wykonywane równolegle — każdy element podróżuje po swojej trajektorii.
Wydajność zależy od implementacji. Flutter FlightShuttle przetwarza obrazy przez RenderObject, co daje 60fps. SwiftUI matchedGeometryEffect jest wektoryzowany. Dla dużych obrazów użyj precacheImage we Flutter lub prefetch w SwiftUI — eliminuje to opóźnienia przy pierwszym przejściu. Android Compose wymaga leniwego ładowania przez AsyncImage.
Standardowe przejście push przesuwa cały ekran, tworząc iluzję stosu kart. Hero Animation utrwala uwagę wzrokową na jednym elemencie — użytkownik widzi jego ciągły ruch, co poprawia zrozumienie związku między ekranami. Push jest dobry dla nawigacji hierarchicznej, Hero — dla podkreślenia treści.
Nie, Hero Animation jest zaimplementowana wbudowanymi środkami. We Flutter jest to widget Hero z pakietu flutter/material.dart. W SwiftUI — modyfikator matchedGeometryEffect. W Android Jetpack Compose — SharedTransitionLayout i Modifier.sharedElement ze standardowego Compose. Dodatkowe biblioteki są wymagane tylko dla niestandardowych trajektorii lub złożonej transformacji.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również