Puntos clave
Hero Animation es una técnica de animación de interfaz en la que un elemento compartido se mueve suavemente desde una pantalla de origen a una pantalla de destino, preservando su identidad. El nombre refleja la metáfora de un “héroe” — un elemento clave que atraviesa una transición guionizada entre pantallas, atrayendo la atención del usuario. A diferencia de las transiciones estándar, Hero Animation no rompe la conexión visual: el usuario ve una trayectoria continua del elemento, lo que mejora la comprensión de la jerarquía de navegación.
El concepto de Hero Animation está establecido en Material Design como recomendación para pantallas de lista-detalle, galerías y tarjetas. El algoritmo de animación consta de tres fases: en la pantalla de origen, el elemento se resalta y se fija en una posición absoluta; durante la transición, se escala, se mueve y cambia de forma según el contenedor de destino; en la pantalla final, el elemento se integra en el nuevo diseño. Según Material Design 3, una animación hero correctamente implementada reduce en un 22% el número de pulsaciones erróneas del botón “atrás”.
En Flutter, Hero Animation se implementa mediante el widget Hero, que recibe un parámetro obligatorio tag — un identificador único. Flutter encuentra automáticamente un par de widgets Hero con la misma etiqueta en las rutas de origen y destino e inicia la animación. El widget Hero no requiere gestión manual de la animación — el framework calcula independientemente la trayectoria y aplica la transformación a través del objeto interno FlightShuttle.
FlightShuttle es un “transbordador” invisible que, en el momento de la transición, crea una copia superpuesta del widget Hero, la posiciona en un sistema de coordenadas absoluto y anima la transformación (escala, traslación, rotación) desde la posición inicial hasta la final. Se pueden pasar curvas personalizadas a FlightShuttle mediante la propiedad flightShuttleBuilder, lo que permite controlar la trayectoria del vuelo: volar en arco, rebotar en los bordes o disminuir la velocidad antes de llegar. En Flutter 3.16+, Hero admite animación preservando clipRect y borderRadius, lo que proporciona el efecto de tarjetas redondeadas que se transforman en pantallas rectangulares.
En SwiftUI, el equivalente de Hero Animation es el modificador matchedGeometryEffect, introducido en iOS 14. Vincula dos elementos mediante un identificador (id) y un espacio de nombres (namespace). A diferencia de Flutter, donde Hero intercepta automáticamente la transición, SwiftUI requiere una declaración explícita de namespace y la aplicación del modificador a ambas partes de la escena — origen y destino — dentro de la misma jerarquía o mediante GeometryReader.
La animación de matchedGeometryEffect admite la transformación de posición, tamaño, rotación y radio de esquina. Para lograr suavidad, se especifica una Animation en withAnimation. SwiftUI interpola automáticamente los estados intermedios basándose en la curva predeterminada easeInOut. En iOS 17+, se agregó soporte para matchedTransitionSource en TabView y NavigationStack — un análogo de sistema de Hero Animation con una trayectoria predefinida para transiciones estándar.
En Android, Hero Animation se llama Shared Element Transition y se implementa mediante ActivityOptions.makeSceneTransitionAnimation o FragmentTransaction.addSharedElement. Cada elemento compartido se marca con un atributo transitionName en XML o mediante código, y Android maneja la animación entre actividades. A partir de Android 5.0 (API 21), las transiciones pasaron a formar parte del framework; antes de eso, había que escribir animaciones personalizadas mediante ValueAnimator.
En Jetpack Compose, Shared Element Transition se implementa mediante los modificadores Modifier.sharedElement y Modifier.sharedBounds junto con AnimatedContent o SharedTransitionLayout (disponible desde Compose BOM 2024.01). A diferencia del sistema View, Compose redibuja los elementos de manera perezosa, por lo que el elemento compartido se almacena en caché en CompositionLocal. Compose también admite la animación de lista a detalle con preservación de scrollOffset — una capacidad clave para las transiciones Hero. Según Google I/O 2024, Compose Shared Element Transition es un 15% más rápido que la versión View debido a la gestión directa de RenderNode.
Un ejemplo básico de Hero Animation en Flutter: una miniatura en la pantalla de lista y una imagen a pantalla completa en la pantalla de detalles. La etiqueta image-hero vincula los dos widgets, y FlightShuttle anima automáticamente la transición de la tarjeta a pantalla completa.
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,
),
),
),
);
}
}
El punto clave es la misma etiqueta (image-hero) en ambas pantallas. Flutter empareja los widgets Hero por etiqueta durante la transición de ruta. Si las etiquetas difieren, la animación no se iniciará — el elemento simplemente aparecerá en la nueva pantalla sin transición. Se puede usar cualquier tipo para la etiqueta, pero las cadenas son la opción más fiable.
Un ejemplo de Hero Animation en SwiftUI con NavigationStack. El modificador matchedGeometryEffect vincula la imagen en la lista y la pantalla de detalles mediante un namespace, y withAnimation desencadena una transición suave.
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 crea un espacio de nombres para el identificador de matchedGeometryEffect. Ambos elementos usan el mismo id (“hero”) y un namespace compartido. Al alternar isDetailShown, SwiftUI anima la transformación de tamaño, posición y radio de esquina entre las dos vistas, creando el efecto hero. Para un NavigationLink estándar con iOS 16+, use navigationTransition(.hero).
Un ejemplo de Shared Element Transition en Jetpack Compose con SharedTransitionLayout. El modificador sharedElement anima un elemento compartido entre pantallas durante la navegación.
@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+) envuelve la jerarquía, y el modificador sharedElement con la clave “hero” vincula los elementos inicial y final. AnimatedContent alterna el estado, y Compose anima automáticamente el tamaño, la posición y la forma de la imagen. Una ventaja clave es el soporte para la animación de bounds y clipBounds, lo que proporciona el efecto de redondeo de tarjeta durante la transición.
Preguntas frecuentes
Sí, Hero Animation está disponible en ambas plataformas. iOS usa matchedGeometryEffect en SwiftUI o UINavigationController.transitioningDelegate. Android usa ActivityOptions.makeSceneTransitionAnimation o Shared Element Transition en Jetpack Compose con Modifier.sharedElement. Flutter implementa Hero Animation mediante el widget Hero, que funciona de forma idéntica en ambas plataformas.
Sí, Hero Animation admite múltiples elementos compartidos. En Flutter, especifique varios widgets Hero con diferentes etiquetas. En SwiftUI, use matchedGeometryEffect con diferentes identificadores en cada elemento. Todas las animaciones se ejecutan en paralelo — cada elemento viaja por su propia trayectoria.
El rendimiento depende de la implementación. FlightShuttle de Flutter procesa imágenes a través de RenderObject, logrando 60fps. matchedGeometryEffect de SwiftUI está vectorizado. Para imágenes grandes, use precacheImage en Flutter o prefetch en SwiftUI — esto elimina los retrasos en la primera transición. Android Compose requiere carga diferida mediante AsyncImage.
Una transición push estándar desplaza toda la pantalla, creando la ilusión de una pila de tarjetas. Hero Animation fija la atención visual en un único elemento — el usuario ve su movimiento continuo, lo que mejora la comprensión de la relación entre las pantallas. Push es bueno para la navegación jerárquica, Hero para enfatizar el contenido.
No, Hero Animation se implementa con herramientas integradas. En Flutter, es el widget Hero del paquete flutter/material.dart. En SwiftUI, es el modificador matchedGeometryEffect. En Android Jetpack Compose, es SharedTransitionLayout y Modifier.sharedElement de Compose estándar. Solo se requieren bibliotecas adicionales para trayectorias no estándar o transformaciones complejas.
Resumen
Desarrollaremos una aplicación móvil llave en mano
IT Sectr crea aplicaciones para iOS y Android para startups y empresas desde 2017. Le asesoraremos y le propondremos la mejor solución.
Lea también