Hero Animation: co to jest, animacja wspólnych elementów we Flutter

Autor: IT Sectr Opublikowano: 2026-03-02 Czas czytania: 8 min
Hero Animation — wzorzec animacji, w którym element interfejsu płynnie przemieszcza się i transformuje między ekranami, tworząc efekt „bohatera" podróżującego z jednego kontekstu do drugiego. W przeciwieństwie do standardowego przejścia push/pop, animacja bohatera zachowuje ciągłość wizualną: użytkownik widzi, jak miniatura zamienia się w obraz pełnoekranowy lub jak nagłówek komórki przemieszcza się do nagłówka ekranu. Według Google Material Design (2025), Hero Animation zmniejsza obciążenie poznawcze o 30% podczas nawigacji między listą a szczegółami — użytkownik nie traci skupienia dzięki zapamiętaniu pozycji elementu. We Flutter wzorzec jest realizowany przez widget Hero, w SwiftUI — przez modyfikator matchedGeometryEffect, w Jetpack Compose — przez Modifier.sharedElement.

Najważniejsze

  • Hero Animation — płynne przejście wspólnego elementu między ekranami, zachowujące kontekst wizualny dla użytkownika.
  • Flutter Hero — widget, który automatycznie znajduje parę po tagu i uruchamia animację FlightShuttle między ekranami.
  • matchedGeometryEffect — modyfikator SwiftUI, łączący elementy przez identyfikator w namespace dla animowanego przejścia.
  • Shared Element Transition — mechanizm Android, realizowany przez ActivityOptions lub Modifier.sharedElement w Jetpack Compose.
  • FlightShuttle — wewnętrzny mechanizm Flutter, zarządzający trajektorią i transformacją widgetu Hero podczas przejścia.

Czym jest Hero Animation?

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%.

Hero we Flutter: tagi i FlightShuttle

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.

matchedGeometryEffect w SwiftUI

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ść.

Shared Element Transition w Android

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.

Przykłady kodu

Flutter: Hero z obrazem

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.

dart
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ą.

SwiftUI: matchedGeometryEffect między ekranami

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.

swift
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).

Android: Shared Element Transition z Jetpack Compose

Przykład Shared Element Transition w Jetpack Compose z SharedTransitionLayout. Modyfikator sharedElement animuje wspólny element między ekranami podczas nawigacji.

kotlin
@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

Hero Animation działa na iOS i Android?

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.

Czy można animować wiele elementów jednocześnie?

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.

Czy Hero Animation zwalnia na dużych obrazach?

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.

Czym Hero różni się od standardowego przejścia push?

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.

Czy potrzebne są dodatkowe biblioteki do Hero Animation?

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

  • Hero Animation — animacja wspólnego elementu między ekranami, zachowująca ciągłość wizualną i zmniejszająca obciążenie poznawcze.
  • Flutter Hero z tagiem i FlightShuttle — najprostszy sposób na zaimplementowanie animacji bohatera bez ręcznego zarządzania animacją.
  • SwiftUI matchedGeometryEffect z @Namespace.id animuje pozycję, rozmiar i cornerRadius elementu między dwoma stanami.
  • Android SharedTransitionLayout z Modifier.sharedElement w Jetpack Compose zapewnia przejście z cachowaniem RenderNode.
  • Wszystkie trzy frameworki wspierają wiele wspólnych elementów z równoległą animacją.
  • FlightShuttle pozwala dostosować trajektorię lotu przez flightShuttleBuilder we Flutter.
  • Hero Animation jest zalecana przez Material Design dla ekranów typu lista-szczegóły, galerii i kart.

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.

Omów projekt

Przeczytaj również