Hero Animation: какво е, анимация на общи елементи в Flutter

Автор: IT Sectr Публикувано: 2026-03-02 Време за четене: 8 мин
Hero Animation — модел на анимация, при който елемент на интерфейса се придвижва плавно и трансформира между екрани, създавайки ефект на „герой", пътуващ от един контекст в друг. За разлика от стандартния push/pop преход, анимацията на герой запазва визуалната непрекъснатост: потребителят вижда как миниатюра се превръща в изображение на цял екран или как заглавка на клетка се премества в заглавка на екран. Според Google Material Design (2025), Hero Animation намалява когнитивното натоварване с 30% при навигация между списък и детайли — потребителят не губи фокус благодарение на запомняне на позицията на елемента. Във Flutter моделът се реализира чрез widget Hero, в SwiftUI — чрез модификатора matchedGeometryEffect, в Jetpack Compose — чрез Modifier.sharedElement.

Основни точки

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

Какво е Hero Animation?

Hero Animation — техника за анимация на интерфейс, при която общ елемент се придвижва плавно от изходния екран към целевия, запазвайки своята идентичност. Името отразява метафората на „герой" — ключовият елемент, който преминава през сценичен преход между екрани, привличайки вниманието на потребителя. За разлика от стандартните преходи, Hero Animation не прекъсва визуалната връзка: потребителят вижда непрекъсната траектория на елемента, което подобрява разбирането на навигационната йерархия.

Концепцията на Hero Animation е вкоренена в Material Design като препоръка за екрани списък-детайли (list-detail), галерии и карти. Алгоритъмът на анимация се състои от три фази: на изходния екран елементът се подчертава и фиксира в абсолютна позиция; по време на прехода той се мащабира, премества и променя форма според целевия контейнер; на финалния екран елементът се вгражда в новото оформление. Според Material Design 3, правилно реализираната анимация на герой намалява броя на грешните кликвания „назад" с 22%.

Hero във Flutter: тагове и FlightShuttle

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

FlightShuttle — невидима „совалка", която в момента на прехода създава насложено копие на Hero widget, позиционира го в абсолютната координатна система и анимира трансформацията (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) преходите станаха част от framework-а; преди това трябваше да се пишат персонализирани анимации чрез 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 свързва двата widget-а, а 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,
          ),
        ),
      ),
    );
  }
}

Ключовият момент — еднакъв tag (image-hero) на двата екрана. Flutter съпоставя Hero widget-овете по tag по време на прехода на маршрута. Ако tag-овете се различават, анимацията няма да стартира — елементът просто ще се появи на новия екран без преход. Може да се използва всякакъв тип за tag, но низовете са най-надеждният вариант.

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 чрез widget 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 това е widget Hero от пакета flutter/material.dart. В SwiftUI — модификаторът matchedGeometryEffect. В Android Jetpack Compose — SharedTransitionLayout и Modifier.sharedElement от стандартния Compose. Допълнителни библиотеки са необходими само за нестандартни траектории или сложна трансформация.

Резюме

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

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

IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.

Обсъдете проекта

Прочетете също