Flutter: Hero-Animationen und Seitenübergänge
Zuletzt aktualisiert:
11 Min. Lesezeit

Ist Ihnen schon aufgefallen, wie in einer Shopping-App das kleine Produktbild beim Antippen wächst und sich auf der Detailseite an seinen Platz setzt? Oder wie sich ein Foto in einer Galerie in einer einzigen fließenden Bewegung im Vollbild öffnet? Dieser Effekt heißt Hero-Animation.
In diesem Beitrag geht es darum, wie Hero zwei Seiten einander zuordnet, was Dauer und Kurve des Flugs bestimmt, welche Fehler am häufigsten vorkommen und wie Sie mit PageRouteBuilder eigene Seitenübergänge bauen. Wenn Sie implizite Animationen kennen, die bei einer Wertänderung von selbst laufen, ist Hero deren Gegenstück für den Seitenwechsel.
Was ist eine Hero-Animation?
Eine Hero-Animation lässt ein Widget, das auf beiden Seiten vorkommt, geschmeidig von einer Seite zur anderen wechseln. Das Widget „fliegt“ von seiner Position auf der ersten Seite zu seiner Position auf der zweiten.
Der Name stammt von den Helden der Mythologie: Wie ein Held reist das Widget zwischen zwei Welten (Seiten).
Warum Hero verwenden?
Beim Seitenwechsel verlieren Nutzer leicht aus den Augen, was sie angetippt haben und wo sie gelandet sind. Hero bewahrt diesen Kontext:
- Visuelle Kontinuität: Das angetippte Element erscheint auf der neuen Seite als dasselbe Element.
- Wenig Code: Zwei Widgets mit demselben Tag zu umschließen genügt, die Animation erzeugt Flutter.
Der Aufbau von Hero
Für eine Hero-Animation brauchen Sie zwei Dinge:
- Denselben Tag: Die Hero-Widgets auf beiden Seiten müssen denselben Wert für
taghaben - Das Hero-Widget: Umschließen Sie Ihr Widget auf beiden Seiten mit Hero
Hier das einfachste Beispiel:
// Erste Seite
Hero(
tag: 'profile-photo',
child: CircleAvatar(
backgroundImage: NetworkImage('https://picsum.photos/200'),
radius: 30,
),
)
// Zweite Seite
Hero(
tag: 'profile-photo', // Derselbe Tag!
child: CircleAvatar(
backgroundImage: NetworkImage('https://picsum.photos/200'),
radius: 100, // Andere Größe: Flutter erzeugt die Animation dazwischen selbst
),
)Beginnt der Seitenwechsel, ordnet Flutter die Heroes beider Seiten anhand des Tags einander zu und startet für jedes passende Paar einen „Flug“. Der Tag kann ein beliebiges Objekt sein (ein String, ein int oder Ihr eigener ID-Typ), solange beide Seiten per == gleich sind.
Zwei weitere Bedingungen gibt es. Erstens funktioniert Hero nur zwischen zwei PageRoutes: MaterialPageRoute, CupertinoPageRoute und PageRouteBuilder gehören dazu, die Routen von showDialog und showModalBottomSheet nicht. Zweitens sollten die Kinder auf beiden Seiten ähnlich aussehen. Laut Flutter-Dokumentation funktionieren Änderungen von Maßstab und Seitenverhältnis in Hero-Animationen gut, Änderungen an Layout oder Aufbau nicht. Ein kleines Bild, das zu einem großen Bild wird, ist ideal; ein Bild, das zu einer Karte wird, nicht.
Live-Demo
Sie können diese Widgets im interaktiven Beispiel unten ausprobieren:
💡 Falls das Beispiel oben nicht lädt, klicken Sie auf DartPad, um es in einem neuen Tab auszuführen.
Praxisbeispiel: von der Produktkarte zur Detailseite
Der häufigste Anwendungsfall in Shopping-Apps:
class ProductCard extends StatelessWidget {
final String productId;
final String imageUrl;
final String name;
const ProductCard({
super.key,
required this.productId,
required this.imageUrl,
required this.name,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ProductDetailPage(
productId: productId,
imageUrl: imageUrl,
name: name,
),
),
);
},
child: Card(
child: Column(
children: [
Hero(
tag: 'product-$productId', // Eindeutiger Tag
child: Image.network(
imageUrl,
height: 150,
width: double.infinity,
fit: BoxFit.cover,
),
),
Padding(
padding: const EdgeInsets.all(8),
child: Text(name, style: const TextStyle(fontWeight: FontWeight.bold)),
),
],
),
),
);
}
}
class ProductDetailPage extends StatelessWidget {
final String productId;
final String imageUrl;
final String name;
const ProductDetailPage({
super.key,
required this.productId,
required this.imageUrl,
required this.name,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(name)),
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Hero(
tag: 'product-$productId', // Derselbe Tag!
child: Image.network(
imageUrl,
height: 300,
width: double.infinity,
fit: BoxFit.cover,
),
),
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
name,
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
const Text(
'Hier steht die Produktbeschreibung...',
style: TextStyle(fontSize: 16),
),
],
),
),
],
),
);
}
}Die Produkt-ID im Tag erfüllt zwei Aufgaben: Jede Karte auf der Listenseite bleibt eindeutig, und die Detailseite weiß, mit welcher Karte sie verbunden wird. Beachten Sie außerdem, dass Hero nur das Bild umschließt, nicht die ganze Karte, und dass das Bild auf beiden Seiten von derselben URL kommt. Warum beides wichtig ist, erkläre ich im Abschnitt zu den Fehlern.
Den Hero-Flug anpassen
1. Die Dauer kommt von der Route, nicht von Hero
Hero hat keinen eigenen Parameter für die Dauer. Der Flug hängt an der Routenanimation, die den Seitenwechsel steuert: Beim Öffnen gilt transitionDuration der Route, beim Schließen reverseTransitionDuration. In PageRouteBuilder sind beide standardmäßig 300 ms:
Navigator.push(
context,
PageRouteBuilder(
transitionDuration: const Duration(milliseconds: 450),
reverseTransitionDuration: const Duration(milliseconds: 350),
pageBuilder: (context, animation, secondaryAnimation) => const DetailPage(),
),
);Da hier kein transitionsBuilder angegeben ist, erscheint die Seite ohne zusätzlichen Effekt; die einzige Bewegung auf dem Bildschirm ist der Hero-Flug. Mit MaterialPageRoute kommt die Dauer aus dem Seitenübergangsstil der Plattform und kann sich zwischen Flutter-Versionen ändern (der iOS-Übergang dauert zum Beispiel 500 ms). Wenn Sie die Dauer selbst festlegen möchten, verwenden Sie PageRouteBuilder oder eine eigene PageRoute-Klasse.
Welche Dauer ist sinnvoll? Die fertigen Plattform-Seitenübergänge von Flutter dauern zwischen 300 und 500 ms; dieser Bereich ist ein guter Ausgangspunkt. Ist die Dauer zu kurz, können Nutzer der Bewegung nicht folgen, ist sie zu lang, warten sie bei jedem Tippen. Entscheiden Sie auf einem echten Gerät und spielen Sie den Übergang mehrmals ab.
2. Die Kurve: Die Curve im transitionsBuilder ändert den Hero nicht
Das ist der häufigste Irrtum. Der folgende Code gibt dem Seitenübergang Curves.easeOutCubic:
Navigator.push(
context,
PageRouteBuilder(
transitionDuration: const Duration(milliseconds: 500),
pageBuilder: (context, animation, secondaryAnimation) => const DetailPage(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
final curved = CurvedAnimation(
parent: animation,
curve: Curves.easeOutCubic,
);
return FadeTransition(opacity: curved, child: child);
},
),
);Diese Kurve wirkt nur auf die FadeTransition, also darauf, wie die Seite selbst erscheint. Der Hero-Flug läuft nicht durch den transitionsBuilder: Flutter zeichnet das fliegende Widget im Overlay über den Seiten und legt eine eigene Kurve um die Routenanimation. Diese Kurve ist standardmäßig Curves.fastOutSlowIn. Im Beispiel oben blendet die Seite also mit easeOutCubic ein, während der Hero 500 ms lang weiterhin mit fastOutSlowIn fliegt; gemeinsam haben die beiden Animationen nur die Dauer.
Ab Flutter 3.44 hat Hero eigene Parameter curve und reverseCurve:
Hero(
tag: 'product-$productId',
curve: Curves.easeOutCubic, // Flutter 3.44+
child: Image.network(imageUrl, fit: BoxFit.cover),
)Für den Flug nimmt Flutter den Hero der oberen Seite: beim Öffnen den der neuen Seite, beim Schließen den der Seite, die geschlossen wird. Im Ablauf von Liste zu Detail ist das in beiden Fällen die Detailseite, setzen Sie diese Parameter also am Hero der Detailseite. Ohne reverseCurve wird auf dem Rückweg curve.flipped verwendet, der Rückflug wirkt dann wie das zeitliche Spiegelbild des Hinflugs. In älteren Versionen ist die Flugkurve fest fastOutSlowIn; ändern können Sie dort die Dauer, den Flugweg und das fliegende Widget.
3. Das fliegende Widget: flightShuttleBuilder
Standardmäßig wird während des Flugs das Kind des Ziel-Heroes angezeigt. Sehen die Kinder auf beiden Seiten unterschiedlich aus (ein kleines Vorschaubild mit abgerundeten Ecken in der Liste, ein großes eckiges Bild auf der Detailseite), können Sie während des Flugs zwischen ihnen überblenden:
Widget crossFadeShuttle(
BuildContext flightContext,
Animation<double> animation,
HeroFlightDirection direction,
BuildContext fromContext,
BuildContext toContext,
) {
// animation ist auf der unteren Seite 0 und auf der oberen Seite 1.
// Beim Push ist die obere Seite "to", beim Pop "from".
final isPush = direction == HeroFlightDirection.push;
final topHero = (isPush ? toContext : fromContext).widget as Hero;
final bottomHero = (isPush ? fromContext : toContext).widget as Hero;
return Stack(
fit: StackFit.expand,
children: [
FadeTransition(opacity: ReverseAnimation(animation), child: bottomHero.child),
FadeTransition(opacity: animation, child: topHero.child),
],
);
}
// Geben Sie beiden Heroes denselben Builder:
Hero(
tag: 'product-$productId',
flightShuttleBuilder: crossFadeShuttle,
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.network(imageUrl, fit: BoxFit.cover),
),
)Die animation, die der Builder bekommt, ist die Routenanimation mit bereits angewandter Hero-Kurve. Weil die obere Seite beim Öffnen auf der Seite von toContext und beim Schließen auf der von fromContext steht, müssen Sie direction auswerten; sonst läuft die Überblendung auf dem Rückweg verkehrt herum. Definieren beide Heroes einen Builder, gewinnt der des Ziel-Heroes des jeweiligen Flugs. Am sichersten geben Sie beiden Seiten dieselbe Funktion.
4. Der Flugweg: createRectTween
createRectTween bestimmt den Weg zwischen Start- und Zielrechteck des Heroes. Mit MaterialApp ist der Standardweg bereits gebogen: Der HeroController, den MaterialApp anlegt, verwendet MaterialRectArcTween. Für eine gerade Linie geben Sie RectTween an, damit sich der Mittelpunkt des Rechtecks auf einem Bogen bewegt, MaterialRectCenterArcTween:
Hero(
tag: 'avatar-${user.id}',
createRectTween: (begin, end) => MaterialRectCenterArcTween(begin: begin, end: end),
child: CircleAvatar(backgroundImage: NetworkImage(user.photoUrl)),
)Flutter verwendet das createRectTween des Ziel-Heroes des jeweiligen Flugs. Damit Hin- und Rückflug denselben Weg nehmen, geben Sie beiden Heroes dieselbe Funktion.
5. Die Zurück-Wischgeste auf iOS
Wischen Nutzer auf iOS vom Bildschirmrand aus zurück, fliegt Hero standardmäßig nicht. Um das einzuschalten, setzen Sie bei den Heroes auf beiden Seiten transitionOnUserGestures: true:
Hero(
tag: 'product-$productId',
transitionOnUserGestures: true,
child: Image.network(imageUrl, fit: BoxFit.cover),
)Ist der Wert nur auf einer Seite true, gibt es beim Zurückwischen keinen Flug.
6. Hero in einem Bereich abschalten: HeroMode
Heroes in einem Teilbaum, der mit HeroMode(enabled: false) umschlossen ist, nehmen an Routenwechseln nicht teil; die Widgets werden weiterhin normal angezeigt. Ein nützlicher Einsatz ist, Nutzer zu respektieren, die Animationen in den Systemeinstellungen abgeschaltet haben:
HeroMode(
enabled: !MediaQuery.disableAnimationsOf(context),
child: ProductGrid(products: products),
)Animationen beim Seitenwechsel
Hero allein kann genügen, aber mit PageRouteBuilder können Sie zusätzlich festlegen, wie sich die Seite selbst öffnet. Diese Effekte bewegen die Seite; der Hero fliegt, wie oben beschrieben, mit seiner eigenen Kurve darüber weiter. Die Grundlagen von Navigator.push finden Sie im Beitrag zur Seitennavigation mit Navigator.
SlideTransition: gleitender Übergang
Seiten können von rechts nach links, von links nach rechts oder von oben nach unten gleiten:
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) => NextPage(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
const begin = Offset(1.0, 0.0); // Rechts beginnen
const end = Offset.zero; // In der Mitte enden
const curve = Curves.easeInOut;
final tween = Tween(begin: begin, end: end).chain(
CurveTween(curve: curve),
);
final offsetAnimation = animation.drive(tween);
return SlideTransition(
position: offsetAnimation,
child: child,
);
},
),
);Beispiele für die Richtung:
// Von rechts nach links
const begin = Offset(1.0, 0.0);
// Von links nach rechts
const begin = Offset(-1.0, 0.0);
// Von unten nach oben
const begin = Offset(0.0, 1.0);
// Von oben nach unten
const begin = Offset(0.0, -1.0);
// Diagonal (von unten rechts)
const begin = Offset(1.0, 1.0);FadeTransition: Überblenden
Die Seite wird an Ort und Stelle von transparent zu sichtbar:
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) => NextPage(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(
opacity: animation,
child: child,
);
},
),
);ScaleTransition: Skalieren
Die Seite öffnet sich von klein nach groß:
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) => NextPage(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
final scaleTween = Tween<double>(begin: 0.0, end: 1.0).chain(
CurveTween(curve: Curves.elasticOut),
);
return ScaleTransition(
scale: animation.drive(scaleTween),
child: child,
);
},
),
);RotationTransition: Drehen
Die Seite kommt mit einer Drehung herein:
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) => NextPage(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return RotationTransition(
turns: animation,
child: child,
);
},
),
);Mehrere Animationen kombinieren
Übergänge lassen sich kombinieren, indem Sie sie ineinander verschachteln:
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) => NextPage(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
// Überblenden und Gleiten zugleich
return FadeTransition(
opacity: animation,
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0.0, 0.3),
end: Offset.zero,
).animate(animation),
child: child,
),
);
},
),
);Eigener Übergang: „Zoom Fade Slide“
Ein wiederverwendbares Übergangs-Widget, das Zoomen, Überblenden und Gleiten kombiniert:
class ZoomFadeSlideTransition extends StatelessWidget {
final Animation<double> animation;
final Widget child;
const ZoomFadeSlideTransition({
super.key,
required this.animation,
required this.child,
});
@override
Widget build(BuildContext context) {
final curved = CurvedAnimation(parent: animation, curve: Curves.easeOut);
return FadeTransition(
opacity: animation,
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 0.1),
end: Offset.zero,
).animate(curved),
child: ScaleTransition(
scale: Tween<double>(begin: 0.9, end: 1.0).animate(curved),
child: child,
),
),
);
}
}
// Verwendung:
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) => NextPage(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return ZoomFadeSlideTransition(
animation: animation,
child: child,
);
},
),
);Charakter durch Kurven
Eine Kurve legt fest, wie eine Animation beschleunigt und abbremst. Sie können diese Kurven mit CurveTween oder CurvedAnimation auf den Seitenübergang anwenden und ab Flutter 3.44 mit Hero.curve auf den Flug:
// Linear: wirkt mechanisch
Curves.linear
// Langsam starten, beschleunigen, langsam enden
Curves.easeInOut
// Langsam starten
Curves.easeIn
// Langsam enden
Curves.easeOut
// Schnell starten, kurz vor dem Ziel abbremsen; Standard des Hero-Flugs
Curves.fastOutSlowIn
// Schnell starten und bis zum Stillstand abbremsen
Curves.decelerate
// Schießt leicht über das Ziel hinaus und kehrt zurück
Curves.easeOutBack
// Elastisch: verspielt
Curves.elasticOut
// Federt aus: auffällig
Curves.bounceOutTipp: Für Oberflächenübergänge eignen sich ruhige Kurven wie Curves.easeInOut, Curves.easeOut oder Curves.fastOutSlowIn. Extreme Kurven wie Curves.bounceOut fallen auf, ermüden aber, wenn sie sich bei jedem Übergang wiederholen.
Tipps für die Performance
1. FadeTransition statt Opacity für animierte Deckkraft
// ❌ Schlecht: Der Controller löst in jedem Frame setState aus, der Teilbaum wird neu gebaut
Opacity(
opacity: _controller.value,
child: HeavyWidget(),
)
// ✅ Gut: FadeTransition aktualisiert nur die Deckkraft, das Kind wird nicht neu gebaut
FadeTransition(
opacity: _controller,
child: const HeavyWidget(),
)Für Deckkraft, die an einem booleschen Wert hängt, erledigt AnimatedOpacity dieselbe Aufgabe.
2. Das übergebene child im transitionsBuilder verwenden
pageBuilder baut die Seite einmal, transitionsBuilder kann dagegen erneut aufgerufen werden, sobald sich der Zustand der Route ändert. Statt schwere Inhalte im Builder neu aufzubauen, umschließen Sie immer das übergebene child:
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) => NextPage(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(
opacity: animation,
child: child, // die von pageBuilder gebaute Seite
);
},
),
);Einsatzfälle aus der Praxis
1. Fotogalerie
GridView.builder(
itemCount: photos.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 4,
mainAxisSpacing: 4,
),
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PhotoViewPage(
photoUrl: photos[index],
tag: 'photo-$index',
),
),
);
},
child: Hero(
tag: 'photo-$index',
child: Image.network(
photos[index],
fit: BoxFit.cover,
),
),
);
},
)2. Profilbild vergrößern
// Listeneintrag
ListTile(
leading: GestureDetector(
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ProfilePage(user: user),
),
),
child: Hero(
tag: 'avatar-${user.id}',
child: CircleAvatar(
backgroundImage: NetworkImage(user.photoUrl),
),
),
),
title: Text(user.name),
)
// Profilseite
Scaffold(
body: Column(
children: [
Hero(
tag: 'avatar-${user.id}',
child: CircleAvatar(
backgroundImage: NetworkImage(user.photoUrl),
radius: 80,
),
),
// Weitere Informationen...
],
),
)Häufige Fehler und ihre Lösungen
Fehler 1: Derselbe Tag zweimal auf einer Seite
Symptom: Beim Übergang erscheint der Fehler There are multiple heroes that share the same tag within a subtree. Ein Tag muss innerhalb einer Route eindeutig sein; derselbe Tag auf zwei verschiedenen Seiten ist dagegen genau das, was die Zuordnung ausmacht. Am häufigsten passiert das in Listen:
// ❌ Falsch
ListView.builder(
itemBuilder: (context, index) {
return Hero(
tag: 'item', // Alle haben denselben Tag!
child: ListTile(title: Text('Item $index')),
);
},
)Lösung: Geben Sie jedem Eintrag einen eindeutigen Tag.
// ✅ Richtig
ListView.builder(
itemBuilder: (context, index) {
return Hero(
tag: 'item-$index', // Jeder Eintrag ist eindeutig
child: ListTile(title: Text('Item $index')),
);
},
)Erscheint dasselbe Produkt zweimal auf einer Seite (etwa in „Empfohlen“ und in „Alle Produkte“), reicht die ID allein nicht. Nehmen Sie den Abschnitt in den Tag auf und übergeben Sie der Detailseite den Tag, mit dem sie geöffnet wurde, als Parameter:
// Liste
Hero(tag: 'featured-${product.id}', child: ProductImage(product))
// Beim Öffnen der Detailseite
ProductDetailPage(product: product, heroTag: 'featured-${product.id}')Fehler 2: Mehrere FloatingActionButtons auf einer Seite
FloatingActionButton umschließt sich bereits selbst mit einem Hero, und ohne heroTag teilen sich alle FABs denselben Standard-Tag. Zwei FABs auf derselben Seite führen zum vorigen Fehler. Geben Sie jedem einen eigenen heroTag oder setzen Sie heroTag: null, wenn er nicht fliegen muss:
FloatingActionButton(
heroTag: 'add-note', // Ohne Flug: heroTag: null
onPressed: () {},
child: const Icon(Icons.add),
)Fehler 3: Ein Hero in einem Hero
Symptom: A Hero widget cannot be the descendant of another Hero widget. Das Kind eines Heroes darf keinen weiteren Hero enthalten. Da ein FAB selbst ein Hero ist, löst es diesen Fehler ebenfalls aus, wenn Sie das Icon des FAB zusätzlich mit Hero umschließen. Sollen zwei Elemente getrennt fliegen, platzieren Sie sie nebeneinander statt verschachtelt.
Fehler 4: Gelbe doppelte Unterstreichung unter Text während des Flugs
Enthält das Kind des Heroes Text, sehen Sie während des Flugs womöglich großen roten Text mit gelber doppelter Unterstreichung. Das fliegende Widget wird nicht innerhalb von Scaffold und Material der Seite gezeichnet, sondern im Overlay des Navigators; dort findet der Text keinen Theme-Stil und fällt auf den Warnstil „Material fehlt“ von MaterialApp zurück. Die Lösung: das Kind in ein transparentes Material legen und dem Text seinen Stil ausdrücklich geben:
Hero(
tag: 'title-$productId',
child: Material(
type: MaterialType.transparency,
child: Text(
name,
style: Theme.of(context).textTheme.titleLarge,
),
),
)Verwenden Sie dieselbe Umhüllung im Hero auf beiden Seiten.
Fehler 5: Overflow während des Flugs
Das fliegende Widget wird bei jeder Zwischengröße zwischen Start- und Zielgröße neu angeordnet. Ist das Kind ein festes Layout wie eine Column oder Row, passt es womöglich in eine dieser Größen nicht hinein und meldet RenderFlex overflowed. Legen Sie den Hero auf den Teil, der sich skalieren lässt (meist das Bild), nicht auf die ganze Karte, und zeigen Sie den Rest der Karte auf der Detailseite ganz normal. Braucht der Flug wirklich ein anderes Aussehen, liefern Sie mit flightShuttleBuilder ein einfacheres Widget.
Fehler 6: Eine leere Fläche während des Flugs
Standardmäßig wird während des Flugs das Kind der Zielseite gezeigt. Lädt die Detailseite eine größere Version des Bildes von einer anderen URL, ist dieses Bild noch nicht geladen, und im Flug sehen Sie eine leere Fläche. Verwenden beide Seiten dieselbe URL, liest Image.network aus dem Bild-Cache von Flutter, und das auf der Listenseite bereits geladene Bild steht für den Flug bereit. Brauchen Sie die große Version, fliegen Sie mit dem kleinen Bild (zum Beispiel, indem ein flightShuttleBuilder das Kind des Quell-Heroes zeigt) und laden das große Bild anschließend auf der Detailseite.
Bonus: ein wiederverwendbarer Übergangs-Helfer
Eine Hilfsklasse, die Sie im ganzen Projekt nutzen können:
enum PageTransitionType { fade, slide, scale, rotation }
class PageTransitionHelper {
static Route<T> createRoute<T>(
Widget page, {
PageTransitionType type = PageTransitionType.fade,
Duration duration = const Duration(milliseconds: 300),
}) {
return PageRouteBuilder<T>(
transitionDuration: duration,
reverseTransitionDuration: duration,
pageBuilder: (context, animation, secondaryAnimation) => page,
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return switch (type) {
PageTransitionType.fade => FadeTransition(opacity: animation, child: child),
PageTransitionType.slide => SlideTransition(
position: Tween<Offset>(
begin: const Offset(1.0, 0.0),
end: Offset.zero,
).animate(animation),
child: child,
),
PageTransitionType.scale => ScaleTransition(
scale: CurvedAnimation(parent: animation, curve: Curves.easeOut),
child: child,
),
PageTransitionType.rotation => RotationTransition(turns: animation, child: child),
};
},
);
}
}
// Verwendung:
Navigator.push(
context,
PageTransitionHelper.createRoute(
NextPage(),
type: PageTransitionType.slide,
),
);Da der switch-Ausdruck über das Enum alle Fälle abdeckt, ist kein default-Zweig nötig; fügen Sie einen neuen Übergangstyp hinzu, zeigt Ihnen der Compiler den fehlenden Zweig.
Zusammenfassung
- Hero: Widgets mit demselben
tag„fliegen“ zwischen zweiPageRoutes; ein Tag muss innerhalb einer Seite eindeutig sein. - Dauer: Kommt von der Route (
transitionDuration,reverseTransitionDuration). - Kurve: Die Curve im
transitionsBuilderwirkt nur auf die Seite; die Standardkurve des Flugs istfastOutSlowInund lässt sich ab Flutter 3.44 mitHero.curveändern. flightShuttleBuilderändert das fliegende Widget,createRectTweenden Flugweg; geben Sie beides dem Hero auf beiden Seiten.transitionOnUserGestureshilft bei der Zurück-Wischgeste auf iOS,HeroModeschaltet einen Bereich ab.- Mit
SlideTransition,FadeTransition,ScaleTransition,RotationTransitionundPageRouteBuilderpassen Sie den Seitenübergang zusätzlich an. - Performance:
FadeTransitionfür animierte Deckkraft und das übergebenechildimtransitionsBuilder.
Häufig gestellte Fragen
Warum passiert bei meiner Hero-Animation nichts?
Es gibt drei häufige Ursachen: Die tag-Werte der beiden Seiten sind per == nicht gleich, einer der Heroes steht unter HeroMode(enabled: false), oder die neue Seite ist keine PageRoute. showDialog und showModalBottomSheet verwenden keine PageRoute, daher fliegt Hero dort nicht. Möchten Sie einen Hero auf einer dialogartigen Seite, öffnen Sie eine eigene Route mit PageRouteBuilder(opaque: false, barrierColor: Colors.black54, ...).
Wie ändere ich die Dauer eines Hero-Übergangs?
Die Dauer kommt von der Route, nicht von Hero selbst. Verwenden Sie PageRouteBuilder und setzen Sie transitionDuration für das Öffnen und reverseTransitionDuration für das Schließen.
Warum ändert die Curve im transitionsBuilder den Hero nicht?
Diese Kurve gilt nur für die Widgets, die den Seitenübergang zeichnen. Der Hero-Flug nutzt die Routenanimation mit seiner eigenen Kurve (standardmäßig Curves.fastOutSlowIn). Ab Flutter 3.44 ändern Sie sie, indem Sie dem Hero auf der Detailseite curve: geben.
Kann ich jedem Eintrag einer ListView einen Hero geben?
Ja, sofern jeder Eintrag einen eindeutigen Tag erhält; die Produkt-ID eignet sich gut. Erscheint derselbe Tag bei mehr als einem Hero auf derselben Seite, wirft der Übergang einen Fehler.
Wann sollte ich auf Hero verzichten?
Hero ergibt Sinn, wenn das Element auf beiden Seiten dasselbe darstellt. Zeigt die Detailseite dieses Element nicht oder hat es dort eine andere Bedeutung (etwa ein Icon aus der Liste, das auf der Detailseite zu einem Button wird), führt der Flug Nutzer in die Irre; ein normaler Seitenübergang genügt.
Verwandte Artikel
Flutter: Maximale Wirkung mit impliziten Animationen
Mit AnimatedContainer, AnimatedOpacity, AnimatedPositioned und weiteren impliziten Animations-Widgets fast ohne Code animieren.
Flutter: Explizite Animationen mit AnimationController
Mit AnimationController, Tween und AnimatedBuilder professionelle Animationen in Flutter erstellen. Alle Details zu expliziten Animationen.
Flutter: Helles und dunkles Theme mit ThemeData
Ein Flutter-Theme mit ThemeData aufbauen: ColorScheme.fromSeed, helles und dunkles Theme, ThemeMode, Theme.of, TextTheme, Komponenten-Themes, ThemeExtension.