Основни точки
Hero Animation — техника за анимация на интерфейс, при която общ елемент се придвижва плавно от изходния екран към целевия, запазвайки своята идентичност. Името отразява метафората на „герой" — ключовият елемент, който преминава през сценичен преход между екрани, привличайки вниманието на потребителя. За разлика от стандартните преходи, Hero Animation не прекъсва визуалната връзка: потребителят вижда непрекъсната траектория на елемента, което подобрява разбирането на навигационната йерархия.
Концепцията на Hero Animation е вкоренена в Material Design като препоръка за екрани списък-детайли (list-detail), галерии и карти. Алгоритъмът на анимация се състои от три фази: на изходния екран елементът се подчертава и фиксира в абсолютна позиция; по време на прехода той се мащабира, премества и променя форма според целевия контейнер; на финалния екран елементът се вгражда в новото оформление. Според Material Design 3, правилно реализираната анимация на герой намалява броя на грешните кликвания „назад" с 22%.
Във 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, което дава ефект на заоблени карти, трансформиращи се в правоъгълни екрани.
В 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 с предварително дефинирана траектория за стандартни преходи.
В 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.
Основен пример за Hero Animation във Flutter: миниатюра на екрана със списък и изображение на цял екран на екрана с детайли. Тагът image-hero свързва двата widget-а, а 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,
),
),
),
);
}
}
Ключовият момент — еднакъв tag (image-hero) на двата екрана. Flutter съпоставя Hero widget-овете по tag по време на прехода на маршрута. Ако tag-овете се различават, анимацията няма да стартира — елементът просто ще се появи на новия екран без преход. Може да се използва всякакъв тип за tag, но низовете са най-надеждният вариант.
Пример за 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 анимира трансформацията на размер, позиция и cornerRadius между двата изгледа, създавайки ефект на герой. За стандартен 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 чрез widget Hero, работещ идентично на двете платформи.
Да, Hero Animation поддържа множество общи елементи. Във Flutter посочете няколко Hero с различни тагове. В SwiftUI използвайте matchedGeometryEffect с различни идентификатори на всеки елемент. Всички анимации се изпълняват паралелно — всеки елемент пътува по своя траектория.
Производителността зависи от реализацията. Flutter FlightShuttle обработва изображения чрез RenderObject, което осигурява 60fps. SwiftUI matchedGeometryEffect е векторизиран. За големи изображения използвайте precacheImage във Flutter или prefetch в SwiftUI — това елиминира закъсненията при първия преход. Android Compose изисква лениво зареждане чрез AsyncImage.
Стандартният push преход премества целия екран, създавайки илюзия за купчина карти. Hero Animation фиксира визуалното внимание върху един елемент — потребителят вижда непрекъснатото му движение, което подобрява разбирането на връзката между екраните. Push е добър за йерархична навигация, Hero — за подчертаване на съдържание.
Не, Hero Animation е реализирана с вградени средства. Във Flutter това е widget Hero от пакета flutter/material.dart. В SwiftUI — модификаторът matchedGeometryEffect. В Android Jetpack Compose — SharedTransitionLayout и Modifier.sharedElement от стандартния Compose. Допълнителни библиотеки са необходими само за нестандартни траектории или сложна трансформация.
Резюме
Ще разработим мобилно приложение под ключ
IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също