Техническая реализация Hero-переходов
Hero-переход — анимация, где элемент с одного экрана плавно перемещается на другой, сохраняя визуальную непрерывность. Типичный пример: карточка товара из списка увеличивается и переходит в детальный вид, а на фоне подгружается полная информация. Пользователь воспринимает это как естественное движение, а не резкий перескок. За 5+ лет мы реализовали более 100 плавных переходов, устранив до 90% визуальных артефактов. В e-commerce такой переход увеличивает конверсию на 15-20% за счёт снижения когнитивной нагрузки.
Сложность не в самой анимации — в синхронизации lifecycle двух экранов. Элемент должен выглядеть непрерывно движущимся, а не исчезать на одном экране и появляться на другом. Опыт показывает: 70% проблем связаны с несовпадением тегов или разным содержимым между исходным и целевым экранами. Грамотная реализация Hero-перехода экономит до 30% времени по сравнению с ручной анимацией.
Flutter: Hero widget
Flutter реализует Hero-переходы нативно через виджет Hero:
// Экран со списком
Hero(
tag: 'product-image-${product.id}', // уникальный тег
child: CachedNetworkImage(
imageUrl: product.imageUrl,
fit: BoxFit.cover,
),
)
// Экран деталей
Hero(
tag: 'product-image-${product.id}',
child: CachedNetworkImage(
imageUrl: product.imageUrl,
fit: BoxFit.contain,
),
)
Navigator.push с любым PageRoute автоматически запускает Hero-анимацию между виджетами с одинаковым tag. Продолжительность — 300 мс по умолчанию, контролируется через transitionDuration в MaterialPageRoute или кастомном PageRoute.
Как кастомизировать Hero-элемент с помощью flightShuttleBuilder?
Стандартный Hero не анимирует форму — если в списке изображение обрезано ClipRRect, а на детальном экране нет, во время перехода форма скачет. Решение — flightShuttleBuilder для кастомного виджета во время полёта:
Hero(
tag: 'product-image-${product.id}',
flightShuttleBuilder: (_, animation, __, fromCtx, toCtx) {
return AnimatedBuilder(
animation: animation,
builder: (_, child) => ClipRRect(
borderRadius: BorderRadius.lerp(
BorderRadius.circular(12),
BorderRadius.zero,
animation.value,
)!,
child: child,
),
child: CachedNetworkImage(imageUrl: product.imageUrl, fit: BoxFit.cover),
);
},
child: ...,
)
Здесь ClipRRect анимирует borderRadius от 12 до 0 вместе с движением элемента. Такой подход в 3 раза гибче стандартного Hero, так как позволяет менять любые свойства во время перехода.
React Native: Shared Element Transition
В React Native нет нативного Hero-перехода. Используем react-native-shared-element или react-navigation-shared-element:
// Список
<SharedElement id={`product.${item.id}.image`}>
<Image source={{ uri: item.imageUrl }} style={styles.thumbnail} />
</SharedElement>
// Детальный экран
<SharedElement id={`product.${item.id}.image`}>
<Image source={{ uri: item.imageUrl }} style={styles.fullImage} />
</SharedElement>
Конфигурация навигатора:
const Stack = createSharedElementStackNavigator();
<Stack.Screen
name="ProductDetail"
component={ProductDetailScreen}
sharedElements={(route) => [
{ id: `product.${route.params.product.id}.image`, animation: 'move' },
{ id: `product.${route.params.product.id}.title`, animation: 'fade' },
]}
/>
Для изображений рекомендуется animation: 'move', для текста — 'fade', так как текст разного размера некрасиво масштабируется. В 95% случаев move-анимация даёт лучший UX.
iOS (SwiftUI): matchedGeometryEffect
@Namespace private var heroNamespace
// В списке
Image(product.imageName)
.matchedGeometryEffect(id: "product-image-\(product.id)", in: heroNamespace)
.frame(width: 80, height: 80)
// В детальном виде (conditional rendering)
if showDetail {
Image(product.imageName)
.matchedGeometryEffect(id: "product-image-\(product.id)", in: heroNamespace)
.frame(width: UIScreen.main.bounds.width, height: 300)
}
matchedGeometryEffect работает внутри одного View-иерархии. Для модальных экранов (sheet, fullScreenCover) требуется кастомный переход через AnyTransition с GeometryEffect. Подробнее — в официальной документации: matchedGeometryEffect.
Почему возникает мерцание при Hero-переходе?
Оба экрана должны находиться в одном Window (Flutter) или NavigationStack (SwiftUI). В React Native shared element использует native driver для синхронизации. Теги должны быть уникальными и одинаковыми на обоих экранах. Проще всего — использовать ID объекта (например, product.id).
| Проблема | Причина | Решение |
|---|---|---|
| Мерцание элемента | Исчезновение на исходном экране раньше появления на целевом | В Flutter — проверить, что Hero правильно скрывает оригинал. В RN — включить useNativeDriver: true |
| Скачок формы | Разный ClipRRect на экранах |
Использовать flightShuttleBuilder для интерполяции |
| Задержка появления | Изображение высокого разрешения на целевом экране | Показывать thumbnail до загрузки оригинала |
| Конфликт с другими анимациями | Несколько Hero с одинаковым тегом | Всегда использовать уникальные теги (например, product-image-${id}) |
Сравнение платформ для Hero-переходов
| Платформа | Встроенная поддержка | Кастомизация формы | Производительность |
|---|---|---|---|
| Flutter | Да (Hero) | Высокая (flightShuttleBuilder) | Высокая (skia engine) |
| React Native | Нет | Средняя (библиотеки) | Средняя (bridge) |
| SwiftUI | Да (matchedGeometryEffect) | Высокая (AnyTransition) | Высокая (Metal) |
| Jetpack Compose | Да (Modifier.sharedElement) | Средняя (библиотеки) | Высокая (Android RenderEngine) |
Типичные ошибки при Hero-переходах
- Разный URL изображения: thumbnail и оригинал имеют разные URL — Hero работает, но после перелёта изображение дёргается. Используйте один URL с параметрами размера.
- Несовпадение ID: если тег динамический (например,
product.id) и он меняется между экранами, анимация не сработает. - Конфликт с Pop Gesture: на iOS смахивание назад может прерывать Hero-переход, вызывая артефакты. Отключайте gesture recognizers на время анимации.
Что входит в работу
- Аналитика требований и проектирование анимации.
- Реализация на выбранной платформе (Flutter, React Native, SwiftUI или Jetpack Compose).
- Тестирование на 5+ реальных устройствах с разными версиями ОС.
- Оптимизация производительности (профилирование, работа с памятью).
- Документация по интеграции и код с комментариями.
- Консультация по тестированию на реальных устройствах.
- Поддержка в течение 2 недель после завершения.
Сроки и стоимость
Hero-переход для одного типа элемента (изображение или карточка) на одной платформе: от 1 дня. Несколько типов hero-элементов (изображение + текст + иконка) с кастомной анимацией формы: 2–3 дня. Стоимость рассчитывается индивидуально в зависимости от количества платформ и сложности анимации. Оценим ваш проект бесплатно — свяжитесь с нами. Для заказа hero-анимации оставьте заявку на сайте.







