Головне
Hero Animation — це техніка анімації інтерфейсу, при якій спільний елемент плавно переміщується з вихідного екрану на цільовий, зберігаючи свою ідентичність. Назва відображає метафору «героя» — ключового елемента, який проходить через сценарний перехід між екранами, привертаючи увагу користувача. На відміну від стандартних переходів, Hero Animation не розриває візуальний зв’язок: користувач бачить безперервну траєкторію елемента, що покращує розуміння навігаційної ієрархії.
Концепція Hero Animation закріплена в Material Design як рекомендація для екранів список-деталі, галерей та карток. Алгоритм анімації складається з трьох фаз: на вихідному екрані елемент виділяється та фіксується в абсолютній позиції; під час переходу він масштабується, переміщується та змінює форму відповідно до цільового контейнера; на фінальному екрані елемент вбудовується в новий макет. За даними Material Design 3, правильно реалізована анімація героя зменшує кількість помилкових натискань кнопки «назад» на 22%.
У Flutter Hero Animation реалізується через віджет Hero, який приймає обов’язковий параметр tag — унікальний ідентифікатор. Flutter автоматично знаходить пару Hero-віджетів з однаковим тегом на вихідному та цільовому маршрутах та запускає анімацію. Віджет Hero не потребує ручного керування анімацією — фреймворк самостійно обчислює траєкторію та застосовує трансформацію через внутрішній об’єкт FlightShuttle.
FlightShuttle — це невидимий «шатл», який у момент переходу створює накладну копію Hero-віджета, розміщує її в абсолютній системі координат та анімує трансформацію (масштабування, переміщення, обертання) від початкової позиції до кінцевої. Користувацькі криві можна передати FlightShuttle через властивість flightShuttleBuilder, що дозволяє керувати траєкторією польоту: летіти по дузі, відскакувати від меж або сповільнюватися перед прибуттям. У Flutter 3.16+ Hero підтримує анімацію з збереженням clipRect та borderRadius, що дає ефект округлених карток, що трансформуються в прямокутні екрани.
У 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 з попередньо визначеною траєкторією для стандартних переходів.
В 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.
Базовий приклад Hero Animation у Flutter: мініатюра на екрані списку та повноекранне зображення на екрані деталей. Тег image-hero пов’язує два віджети, а FlightShuttle автоматично анімує перехід від картки до повного екрану.
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-віджети за тегом під час переходу маршруту. Якщо теги різні, анімація не запуститься — елемент просто з’явиться на новому екрані без переходу. Можна використовувати будь-який тип для тега, але рядки найнадійніший варіант.
Приклад Hero Animation у SwiftUI з NavigationStack. Модифікатор matchedGeometryEffect пов’язує зображення в списку та екран деталей через namespace, а withAnimation запускає плавний перехід.
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).
Приклад Shared Element Transition у Jetpack Compose з SharedTransitionLayout. Модифікатор sharedElement анімує спільний елемент між екранами під час навігації.
@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 використовує matchedGeometryEffect у SwiftUI або UINavigationController.transitioningDelegate. Android використовує ActivityOptions.makeSceneTransitionAnimation або Shared Element Transition у Jetpack Compose з Modifier.sharedElement. Flutter реалізує Hero Animation через віджет Hero, який працює однаково на обох платформах.
Так, Hero Animation підтримує кілька спільних елементів. У Flutter вкажіть кілька Hero з різними тегами. У SwiftUI використовуйте matchedGeometryEffect з різними ідентифікаторами на кожному елементі. Всі анімації виконуються паралельно — кожен елемент подорожує по своїй траєкторії.
Продуктивність залежить від реалізації. FlightShuttle у Flutter обробляє зображення через RenderObject, досягаючи 60fps. matchedGeometryEffect у SwiftUI векторизований. Для великих зображень використовуйте precacheImage у Flutter або prefetch у SwiftUI — це усуне затримки при першому переході. Android Compose потребує відстроченого завантаження через AsyncImage.
Стандартний push-перехід зсуває весь екран, створюючи ілюзію стопки карток. Hero Animation фіксує зорову увагу на одному елементі — користувач бачить його безперервний рух, що покращує розуміння зв’язку між екранами. Push підходить для ієрархічної навігації, Hero — для акцентування контенту.
Ні, Hero Animation реалізована вбудованими засобами. У Flutter це віджет Hero з пакета flutter/material.dart. У SwiftUI це модифікатор matchedGeometryEffect. У Android Jetpack Compose це SharedTransitionLayout та Modifier.sharedElement зі стандартного Compose. Додаткові бібліотеки потрібні лише для нестандартних траєкторій або складних трансформацій.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.