Реализация анимаций Hero-переходов между экранами мобильного приложения
Hero-переход — когда элемент с одного экрана «летит» на другой экран, становясь его частью. Карточка товара раскрывается в полноэкранный детальный вид, аватар из списка плавно перемещается в шапку профиля. Пользователь понимает пространственную связь между экранами без объяснений.
Сложность не в самой анимации — в правильной синхронизации lifecycle двух экранов, чтобы элемент выглядел непрерывно движущимся, а не исчезающим на одном экране и появляющимся на другом.
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-анимацию между Hero виджетами с одинаковым tag. Продолжительность — 300 мс по умолчанию, контролируется через transitionDuration в MaterialPageRoute или кастомном PageRoute.
Проблемы: если изображение в списке обрезается ClipRRect, а на экране деталей нет — во время перехода форма скачет. Решение: Hero c 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: ...,
)
Это анимирует borderRadius от 12 (карточка) до 0 (полный экран) во время перехода.
React Native: Shared Element Transition
В React Native нет нативного hero-перехода. Используем react-native-shared-element (реального нативного уровня) или react-navigation v6 с createSharedElementStackNavigator от 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' — cross-fade на месте. 'fade-in' — появляется на новом месте. Для изображений — 'move', для текста — 'fade' (текст разного размера некрасиво масштабируется).
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. Подробнее — см. услугу по Matched Geometry Effect.
Типичные проблемы
Мерцание в начале перехода: элемент на исходном экране мгновенно исчезает при старте анимации. В Flutter — Hero по умолчанию скрывает оригинал через Opacity, это нормально. В React Native — SharedElement иногда показывает оба элемента одновременно. Исправляется через useNativeDriver: true и проверкой версий библиотеки.
Разное содержимое: изображение в списке — cached thumbnail, на детальном экране — оригинал в высоком разрешении. Пока грузится оригинал, hero должен показывать thumbnail. В Flutter — Hero всегда использует виджет из исходного экрана во время полёта, потом переключается. Убедитесь, что CachedNetworkImage на обоих экранах использует один и тот же URL для одинакового размера, или явно указывайте memCacheWidth.
Сроки
Hero-переход для одного типа элемента (изображение или карточка) на одной платформе: 1 день. Несколько типов hero-элементов (изображение + текст + иконка) с кастомной анимацией формы: 2–3 дня. Стоимость рассчитывается индивидуально.







