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 как рекомендация для экранов списка-детали (list-detail), галерей и карточек. Алгоритм анимации состоит из трёх фаз: на исходном экране элемент выделяется и фиксируется в абсолютной позиции; во время перехода он масштабируется, перемещается и изменяет форму в соответствии с целевым контейнером; на финальном экране элемент встраивается в новую раскладку. По данным Material Design 3, корректно реализованная герой-анимация уменьшает количество ошибочных нажатий «назад» на 22%.

Hero во Flutter: теги и FlightShuttle

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

FlightShuttle — невидимый «шаттл», который в момент перехода создаёт оверлейную копию Hero-виджета, позиционирует её в системе абсолютных координат и анимирует transform (scale, translate, rotate) от начального положения до конечного. В FlightShuttle можно передать кастомные кривые через свойство flightShuttleBuilder, что позволяет управлять траекторией полёта: лететь по дуге, отскакивать от границ или замедляться перед прибытием. Во Flutter 3.16+ Hero поддерживает анимацию с сохранением clipRect и borderRadius, что даёт эффект скруглённых карточек, трансформирующихся в прямоугольные экраны.

matchedGeometryEffect в SwiftUI

В SwiftUI эквивалент Hero Animation — модификатор matchedGeometryEffect, представленный в iOS 14. Он связывает два элемента через идентификатор (id) и пространство имён (namespace). В отличие от Flutter, где Hero автоматически перехватывает переход, SwiftUI требует явного объявления namespace и применения модификатора к обеим частям сцены — исходной и целевой — внутри одной иерархии или через GeometryReader.

Анимация matchedGeometryEffect поддерживает трансформацию позиции, размера, поворота и скругления углов (cornerRadius). Для достижения плавности задаётся 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 также поддерживает анимацию списка в детали (list→detail) с сохранением scrollOffset — ключевая возможность для Hero-переходов. По данным Google I/O 2024, Compose Shared Element Transition работает на 15% быстрее View-версии за счёт прямого управления RenderNode.

Примеры кода

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

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

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 анимирует трансформацию размера, позиции и cornerRadius между двумя вью, создавая эффект героя. Для стандартного 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 тормозит на больших изображениях?

Производительность зависит от реализации. Flutter FlightShuttle обрабатывает изображения через RenderObject, что даёт 60fps. SwiftUI matchedGeometryEffect векторизован. Для больших изображений используйте 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 анимирует позицию, размер и cornerRadius элемента между двумя состояниями.
  • Android SharedTransitionLayout с Modifier.sharedElement в Jetpack Compose обеспечивает переход с кэшированием RenderNode.
  • Все три фреймворка поддерживают множественные общие элементы с параллельной анимацией.
  • FlightShuttle позволяет кастомизировать траекторию полёта через flightShuttleBuilder в Flutter.
  • Hero Animation рекомендуется Material Design для экранов типа список-детали, галерей и карточек.

Мы разработаем мобильное приложение под ключ

IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.

Обсудить проект

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