Hero Animation: що таке анімація спільних елементів в Flutter

Автор: IT Sectr Опубліковано: 2026-03-02 Час читання: 8 хв
Hero Animation — це паттерн анімації, при якому елемент інтерфейсу плавно переміщується та трансформується між екранами, створюючи ефект «героя», який подорожує з одного контексту в інший. На відміну від стандартного переходу push/pop, анімація героя зберігає візуальну безперервність: користувач бачить, як мініатюра перетворюється на повноекранне зображення або як заголовок комірки переміщується в заголовок екрану. За даними Google Material Design (2025), Hero Animation скорочує когнітивне навантаження на 30% при навігації між списком та деталями — користувач не втрачає фокус завдяки запам’ятовуванню позиції елемента. У Flutter паттерн реалізується через віджет Hero, у SwiftUI — через модифікатор matchedGeometryEffect, у Jetpack Compose — через Modifier.sharedElement.

Головне

  • Hero Animation — плавний перехід спільного елементу між екранами, що зберігає візуальний контекст для користувача.
  • Flutter Hero — віджет, який автоматично знаходить пару за тегом та запускає анімацію FlightShuttle між екранами.
  • matchedGeometryEffect — модифікатор SwiftUI, що пов’язує елементи через ідентифікатор в namespace для анімованого переходу.
  • Shared Element Transition — механізм Android, що реалізується через ActivityOptions або Modifier.sharedElement в Jetpack Compose.
  • FlightShuttle — внутрішній механізм Flutter, що керує траєкторією та трансформацією Hero-віджета під час переходу.

Що таке Hero Animation?

Hero Animation — це техніка анімації інтерфейсу, при якій спільний елемент плавно переміщується з вихідного екрану на цільовий, зберігаючи свою ідентичність. Назва відображає метафору «героя» — ключового елемента, який проходить через сценарний перехід між екранами, привертаючи увагу користувача. На відміну від стандартних переходів, Hero Animation не розриває візуальний зв’язок: користувач бачить безперервну траєкторію елемента, що покращує розуміння навігаційної ієрархії.

Концепція Hero Animation закріплена в Material Design як рекомендація для екранів список-деталі, галерей та карток. Алгоритм анімації складається з трьох фаз: на вихідному екрані елемент виділяється та фіксується в абсолютній позиції; під час переходу він масштабується, переміщується та змінює форму відповідно до цільового контейнера; на фінальному екрані елемент вбудовується в новий макет. За даними Material Design 3, правильно реалізована анімація героя зменшує кількість помилкових натискань кнопки «назад» на 22%.

Hero у Flutter: теги та FlightShuttle

У Flutter Hero Animation реалізується через віджет Hero, який приймає обов’язковий параметр tag — унікальний ідентифікатор. Flutter автоматично знаходить пару Hero-віджетів з однаковим тегом на вихідному та цільовому маршрутах та запускає анімацію. Віджет Hero не потребує ручного керування анімацією — фреймворк самостійно обчислює траєкторію та застосовує трансформацію через внутрішній об’єкт FlightShuttle.

FlightShuttle — це невидимий «шатл», який у момент переходу створює накладну копію Hero-віджета, розміщує її в абсолютній системі координат та анімує трансформацію (масштабування, переміщення, обертання) від початкової позиції до кінцевої. Користувацькі криві можна передати FlightShuttle через властивість flightShuttleBuilder, що дозволяє керувати траєкторією польоту: летіти по дузі, відскакувати від меж або сповільнюватися перед прибуттям. У Flutter 3.16+ Hero підтримує анімацію з збереженням clipRect та borderRadius, що дає ефект округлених карток, що трансформуються в прямокутні екрани.

matchedGeometryEffect у SwiftUI

У SwiftUI еквівалент Hero Animation — це модифікатор matchedGeometryEffect, представлений в iOS 14. Він пов’язує два елементи через ідентифікатор (id) та простір імен (namespace). На відміну від Flutter, де Hero автоматично перехоплює перехід, SwiftUI вимагає явного оголошення namespace та застосування модифікатора до обох частин сцени — вихідної та цільової — в межах однієї ієрархії або через GeometryReader.

Анімація matchedGeometryEffect підтримує трансформацію позиції, розміру, обертання та радіуса кутів. Для плавності задається Animation у withAnimation. SwiftUI автоматично інтерполює проміжні стани на основі типової кривої easeInOut. У iOS 17+ додано підтримку matchedTransitionSource для TabView та NavigationStack — системний аналог Hero Animation з попередньо визначеною траєкторією для стандартних переходів.

Shared Element Transition в Android

В Android Hero Animation називається Shared Element Transition та реалізується через ActivityOptions.makeSceneTransitionAnimation або FragmentTransaction.addSharedElement. Кожен спільний елемент позначається атрибутом transitionName у XML або через код, і Android керує анімацією між активностями. Починаючи з Android 5.0 (API 21), переходи стали частиною фреймворку; до цього доводилося писати користувацькі анімації через ValueAnimator.

У Jetpack Compose Shared Element Transition реалізовано через модифікатори Modifier.sharedElement та Modifier.sharedBounds разом з AnimatedContent або SharedTransitionLayout (доступно з Compose BOM 2024.01). На відміну від View-системи, Compose перемальовує елементи лезиво, тому спільний елемент кешується в CompositionLocal. Compose також підтримує анімацію зі списку в деталі з збереженням scrollOffset — ключова можливість для Hero-переходів. За даними Google I/O 2024, Compose Shared Element Transition на 15% швидший за View-версію завдяки прямому керуванню RenderNode.

Приклади коду

Flutter: Hero з зображенням

Базовий приклад Hero Animation у Flutter: мініатюра на екрані списку та повноекранне зображення на екрані деталей. Тег image-hero пов’язує два віджети, а FlightShuttle автоматично анімує перехід від картки до повного екрану.

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,
          ),
        ),
      ),
    );
  }
}

Ключовий момент — однаковий тег (image-hero) на обох екранах. Flutter зіставляє Hero-віджети за тегом під час переходу маршруту. Якщо теги різні, анімація не запуститься — елемент просто з’явиться на новому екрані без переходу. Можна використовувати будь-який тип для тега, але рядки найнадійніший варіант.

SwiftUI: matchedGeometryEffect між екранами

Приклад Hero Animation у SwiftUI з NavigationStack. Модифікатор matchedGeometryEffect пов’язує зображення в списку та екран деталей через namespace, а withAnimation запускає плавний перехід.

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 створює простір імен для ідентифікатора matchedGeometryEffect. Обидва елементи використовують однаковий id («hero») та спільний namespace. Коли isDetailShown перемикається, SwiftUI анімує трансформацію розміру, позиції та радіуса кутів між двома відображеннями, створюючи ефект героя. Для стандартного NavigationLink з iOS 16+ використовуйте navigationTransition(.hero).

Android: Shared Element Transition з Jetpack Compose

Приклад Shared Element Transition у Jetpack Compose з SharedTransitionLayout. Модифікатор sharedElement анімує спільний елемент між екранами під час навігації.

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+) обгортає ієрархію, а модифікатор sharedElement з ключем «hero» пов’язує початковий та кінцевий елементи. AnimatedContent перемикає стан, і Compose автоматично анімує розмір, позицію та форму зображення. Ключова перевага — підтримка анімації bounds та clipBounds, що дає ефект округлення картки під час переходу.

Часто задавані питання

Hero Animation працює на iOS та Android?

Так, Hero Animation доступна на обох платформах. iOS використовує matchedGeometryEffect у SwiftUI або UINavigationController.transitioningDelegate. Android використовує ActivityOptions.makeSceneTransitionAnimation або Shared Element Transition у Jetpack Compose з Modifier.sharedElement. Flutter реалізує Hero Animation через віджет Hero, який працює однаково на обох платформах.

Чи можна анімувати кілька елементів одночасно?

Так, Hero Animation підтримує кілька спільних елементів. У Flutter вкажіть кілька Hero з різними тегами. У SwiftUI використовуйте matchedGeometryEffect з різними ідентифікаторами на кожному елементі. Всі анімації виконуються паралельно — кожен елемент подорожує по своїй траєкторії.

Hero Animation уповільнюється на великих зображеннях?

Продуктивність залежить від реалізації. FlightShuttle у Flutter обробляє зображення через RenderObject, досягаючи 60fps. matchedGeometryEffect у SwiftUI векторизований. Для великих зображень використовуйте precacheImage у Flutter або prefetch у SwiftUI — це усуне затримки при першому переході. Android Compose потребує відстроченого завантаження через AsyncImage.

Чим Hero відрізняється від стандартного push-переходу?

Стандартний push-перехід зсуває весь екран, створюючи ілюзію стопки карток. Hero Animation фіксує зорову увагу на одному елементі — користувач бачить його безперервний рух, що покращує розуміння зв’язку між екранами. Push підходить для ієрархічної навігації, Hero — для акцентування контенту.

Чи потрібні додаткові бібліотеки для Hero Animation?

Ні, Hero Animation реалізована вбудованими засобами. У Flutter це віджет Hero з пакета flutter/material.dart. У SwiftUI це модифікатор matchedGeometryEffect. У Android Jetpack Compose це SharedTransitionLayout та Modifier.sharedElement зі стандартного Compose. Додаткові бібліотеки потрібні лише для нестандартних траєкторій або складних трансформацій.

Підсумки

  • Hero Animation — це анімація спільного елементу між екранами, що зберігає візуальну безперервність та зменшує когнітивне навантаження.
  • Flutter Hero з тегом та FlightShuttle — найпростіший спосіб реалізувати анімацію героя без ручного керування анімацією.
  • SwiftUI matchedGeometryEffect з @Namespace.id анімує позицію, розмір та радіус кутів елементу між двома станами.
  • Android SharedTransitionLayout з Modifier.sharedElement у Jetpack Compose забезпечує переходи з кешуванням RenderNode.
  • Всі три фреймворки підтримують кілька спільних елементів з паралельною анімацією.
  • FlightShuttle дозволяє налаштувати траєкторію польоту через flightShuttleBuilder у Flutter.
  • Hero Animation рекомендована Material Design для екранів список-деталі, галерей та карток.

Ми розробимо мобільний застосунок під ключ

IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.

Обговорити проект

Читайте також