Saltar al contenido principal

Animaciones Hero

Cómo animar un widget para volar entre dos pantallas.

Probablemente has visto animaciones hero muchas veces. Por ejemplo, una pantalla muestra una lista de miniaturas que representan artículos a la venta. Al seleccionar un artículo, este vuela hacia una nueva pantalla que contiene más detalles y un botón de "Comprar". Hacer volar una imagen de una pantalla a otra se llama una animación hero en Flutter, aunque a veces se hace referencia al mismo movimiento como una transición de elemento compartido.

Es posible que desees ver este video de un minuto que presenta el widget Hero:

Ver en YouTube en una nueva pestaña: "Hero | Flutter widget of the week"

Esta guía demuestra cómo construir animaciones hero estándar y animaciones hero que transforman la imagen de una forma circular a una forma cuadrada durante el vuelo.

Puedes crear esta animación en Flutter con widgets Hero. A medida que el hero se anima desde la ruta de origen a la de destino, la ruta de destino (menos el hero) aparece gradualmente. Típicamente, los héroes son pequeñas partes de la UI, como imágenes, que ambas rutas tienen en común. Desde la perspectiva del usuario, el hero "vuela" entre las rutas. Esta guía muestra cómo crear las siguientes animaciones hero:

Standard hero animations

Una animación hero estándar hace volar al hero desde una ruta hacia una nueva ruta, generalmente aterrizando en una ubicación diferente y con un tamaño diferente.

El siguiente video (grabado a velocidad lenta) muestra un ejemplo típico. Tocar los aletas en el centro de la ruta las hace volar hacia la esquina superior izquierda de una nueva ruta azul, a un tamaño más pequeño. Tocar las aletas en la ruta azul (o usar el gesto del dispositivo para volver a la ruta anterior) hace que las aletas vuelen de regreso a la ruta original.

Ver en YouTube en una nueva pestaña: "Standard hero animation in Flutter"

Radial hero animations

En una animación hero radial, a medida que el hero vuela entre rutas, su forma parece cambiar de circular a rectangular.

El siguiente video (grabado a velocidad lenta), muestra un ejemplo de una animación hero radial. Al inicio, una fila de tres imágenes circulares aparece en la parte inferior de la ruta. Tocar cualquiera de las imágenes circulares hace volar esa imagen hacia una nueva ruta que la muestra con una forma cuadrada. Tocar la imagen cuadrada hace volar al hero de regreso a la ruta original, mostrada con una forma circular.

Ver en YouTube en una nueva pestaña: "Radial hero animation in Flutter"

Antes de pasar a las secciones específicas de animaciones hero estándar o radiales, lee estructura básica de una animación hero para aprender cómo estructurar el código de la animación hero, y detrás de escena para entender cómo Flutter realiza una animación hero.

Estructura básica de una animación hero

#

Las animaciones hero se implementan utilizando dos widgets Hero: uno describe el widget en la ruta de origen, y el otro describe el widget en la ruta de destino. Desde el punto de vista del usuario, el hero parece ser compartido, y solo el programador necesita entender este detalle de implementación. El código de la animación hero tiene la siguiente estructura:

  1. Define un widget Hero inicial, denominado hero de origen. El hero especifica su representación gráfica (típicamente una imagen), y un tag identificador, y se encuentra en el árbol de widgets actualmente mostrado según lo definido por la ruta de origen.
  2. Define un widget Hero final, denominado hero de destino. Este hero también especifica su representación gráfica, y el mismo tag que el hero de origen. Es esencial que ambos widgets hero se creen con el mismo tag, típicamente un objeto que representa los datos subyacentes. Para obtener los mejores resultados, los héroes deben tener árboles de widgets prácticamente idénticos.
  3. Crea una ruta que contenga el hero de destino. La ruta de destino define el árbol de widgets que existe al final de la animación.
  4. Activa la animación haciendo push de la ruta de destino en la pila del Navigator. Las operaciones de push y pop del Navigator activan una animación hero para cada par de héroes con tags coincidentes en las rutas de origen y destino.

Flutter calcula el tween que anima los límites del Hero desde el punto inicial hasta el punto final (interpolando tamaño y posición), y realiza la animación en un overlay.

La siguiente sección describe el proceso de Flutter con mayor detalle.

Detrás de escena

#

A continuación se describe cómo Flutter realiza la transición de una ruta a otra.

Antes de la transición, el hero de origen aparece en la ruta de origen

Antes de la transición, el hero de origen espera en el árbol de widgets de la ruta de origen. La ruta de destino aún no existe, y el overlay está vacío.


La transición comienza

Hacer push de una ruta al Navigator activa la animación. En t=0.0, Flutter hace lo siguiente:

  • Calcula la ruta del hero de destino, fuera de pantalla, usando el movimiento curvo como se describe en la especificación de motion de Material. Flutter ahora sabe dónde termina el hero.

  • Coloca el hero de destino en el overlay, en la misma ubicación y tamaño que el hero de origen. Agregar un hero al overlay cambia su orden Z para que aparezca sobre todas las rutas.

  • Mueve el hero de origen fuera de la pantalla.


El hero vuela en el overlay a su posición y tamaño finales

A medida que el hero vuela, sus límites rectangulares se animan usando Tween<Rect>, especificado en la propiedad createRectTween del Hero. Por defecto, Flutter utiliza una instancia de MaterialRectArcTween, que anima las esquinas opuestas del rectángulo a lo largo de una trayectoria curva. (Consulta Animaciones hero radiales para ver un ejemplo que utiliza una animación Tween diferente.)


Cuando la transición se completa, el hero se mueve desde el overlay a la ruta de destino

Cuando el vuelo se completa:

  • Flutter mueve el widget hero desde el overlay hacia la ruta de destino. El overlay ahora está vacío.

  • El hero de destino aparece en su posición final en la ruta de destino.

  • El hero de origen se restaura a su ruta.


Hacer pop de la ruta realiza el mismo proceso, animando al hero de regreso a su tamaño y ubicación en la ruta de origen.

Clases esenciales

#

Los ejemplos de esta guía utilizan las siguientes clases para implementar animaciones hero:

Hero

El widget que vuela de la ruta de origen a la de destino. Define un Hero para la ruta de origen y otro para la ruta de destino, y asigna a cada uno el mismo tag. Flutter anima pares de héroes con tags coincidentes.

InkWell

Especifica qué sucede al tocar el hero. El método onTap() del InkWell construye la nueva ruta y la añade (hace push) a la pila del Navigator.

Navigator

El Navigator gestiona una pila de rutas. Hacer push o pop de una ruta en la pila del Navigator activa la animación.

Route

Especifica una pantalla o página. La mayoría de las apps, más allá de las más básicas, tienen múltiples rutas.

Animaciones hero estándar

#

¿Qué está pasando?

#

Hacer volar una imagen de una ruta a otra es fácil de implementar usando el widget hero de Flutter. Al utilizar MaterialPageRoute para especificar la nueva ruta, la imagen vuela a lo largo de una trayectoria curva, como describe la especificación de motion de Material Design.

Crea una nueva app de Flutter y actualízala usando los archivos de hero_animation.

Para ejecutar el ejemplo:

  • Toca en la foto de la ruta de inicio para hacer volar la imagen hacia una nueva ruta que muestra la misma foto en una ubicación y escala diferentes.
  • Regresa a la ruta anterior tocando la imagen, o usando el gesto del dispositivo para volver a la ruta anterior.
  • Puedes ralentizar la transición aún más usando la propiedad timeDilation.

Clase PhotoHero

#

La clase personalizada PhotoHero mantiene el hero, y su tamaño, imagen y comportamiento al ser tocado. PhotoHero construye el siguiente árbol de widgets:

Árbol de widgets de la clase PhotoHero

Aquí está el código:

dart
class PhotoHero extends StatelessWidget {
  const PhotoHero({
    super.key,
    required this.photo,
    this.onTap,
    required this.width,
  });

  final String photo;
  final VoidCallback? onTap;
  final double width;

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: width,
      child: Hero(
        tag: photo,
        child: Material(
          color: Colors.transparent,
          child: InkWell(
            onTap: onTap,
            child: Image.asset(
              photo,
              fit: BoxFit.contain,
            ),
          ),
        ),
      ),
    );
  }
}

Información clave:

  • La ruta de inicio se añade (push) implícitamente por MaterialApp cuando se proporciona HeroAnimation como la propiedad home de la app.
  • Un InkWell envuelve la imagen, haciendo que sea sencillo agregar un gesto de toque tanto al hero de origen como al de destino.
  • Definir el widget Material con un color transparente permite que la imagen "sobresalga" del fondo a medida que vuela a su destino.
  • El SizedBox especifica el tamaño del hero al inicio y al final de la animación.
  • Establecer la propiedad fit de la Image en BoxFit.contain, garantiza que la imagen sea lo más grande posible durante la transición sin cambiar su relación de aspecto.

Clase HeroAnimation

#

La clase HeroAnimation crea los PhotoHeroes de origen y destino, y configura la transición.

Aquí está el código:

dart
class HeroAnimation extends StatelessWidget {
  const HeroAnimation({super.key});

  Widget build(BuildContext context) {
    timeDilation = 5.0; // 1.0 means normal animation speed.

    return Scaffold(
      appBar: AppBar(
        title: const Text('Basic Hero Animation'),
      ),
      body: Center(
        child: PhotoHero(
          photo: 'images/flippers-alpha.png',
          width: 300.0,
          onTap: () {
            Navigator.of(context).push(MaterialPageRoute<void>(
              builder: (context) {
                return Scaffold(
                  appBar: AppBar(
                    title: const Text('Flippers Page'),
                  ),
                  body: Container(
                    // Set background to blue to emphasize that it's a new route.
                    color: Colors.lightBlueAccent,
                    padding: const EdgeInsets.all(16),
                    alignment: Alignment.topLeft,
                    child: PhotoHero(
                      photo: 'images/flippers-alpha.png',
                      width: 100.0,
                      onTap: () {
                        Navigator.of(context).pop();
                      },
                    ),
                  ),
                );
              }
            ));
          },
        ),
      ),
    );
  }
}

Información clave:

  • Cuando el usuario toca el InkWell que contiene el hero de origen, el código crea la ruta de destino utilizando MaterialPageRoute. Hacer push de la ruta de destino a la pila del Navigator activa la animación.
  • El Container posiciona al PhotoHero en la esquina superior izquierda de la ruta de destino, debajo del AppBar.
  • El método onTap() para el PhotoHero de destino hace pop de la pila del Navigator, activando la animación que hace volar al Hero de regreso a la ruta original.
  • Usa la propiedad timeDilation para ralentizar la transición mientras depuras.

Animaciones hero radiales

#

Hacer volar a un hero de una ruta a otra mientras se transforma de una forma circular a una forma rectangular es un efecto elegante que puedes implementar usando widgets Hero. Para lograr esto, el código anima la intersección de dos formas de recorte (clip): un círculo y un cuadrado. A lo largo de la animación, el recorte circular (y la imagen) se escala de minRadius a maxRadius, mientras que el recorte cuadrado mantiene un tamaño constante. Al mismo tiempo, la imagen vuela desde su posición en la ruta de origen hacia su posición en la ruta de destino. Para ver ejemplos visuales de esta transición, consulta Transformación radial en la especificación de motion de Material.

Esta animación puede parecer compleja (y lo es), pero puedes personalizar el ejemplo proporcionado según tus necesidades. El trabajo pesado ya está hecho por ti.

¿Qué está pasando?

#

El siguiente diagrama muestra la imagen recortada al inicio (t = 0.0) y al final (t = 1.0) de la animación.

Transformación radial de principio a fin

El gradiente azul (que representa la imagen) indica dónde se intersectan las formas de recorte. Al comienzo de la transición, el resultado de la intersección es un recorte circular (ClipOval). Durante la transformación, el ClipOval se escala de minRadius a maxRadius mientras que el ClipRect mantiene un tamaño constante. Al final de la transición, la intersección de los recortes circular y rectangular produce un rectángulo del mismo tamaño que el widget hero. En otras palabras, al final de la transición la imagen ya no está recortada.

Crea una nueva app de Flutter y actualízala usando los archivos del directorio de GitHub radial_hero_animation.

Para ejecutar el ejemplo:

  • Toca en una de las tres miniaturas circulares para animar la imagen hacia un cuadrado más grande posicionado en el medio de una nueva ruta que oculta la ruta original.
  • Regresa a la ruta anterior tocando la imagen, o usando el gesto del dispositivo para volver a la ruta anterior.
  • Puedes ralentizar la transición aún más usando la propiedad timeDilation.

Clase Photo

#

La clase Photo construye el árbol de widgets que contiene la imagen:

dart
class Photo extends StatelessWidget {
  const Photo({super.key, required this.photo, this.color, this.onTap});

  final String photo;
  final Color? color;
  final VoidCallback onTap;

  Widget build(BuildContext context) {
    return Material(
      // Slightly opaque color appears where the image has transparency.
      color: Theme.of(context).primaryColor.withValues(alpha: 0.25),
      child: InkWell(
        onTap: onTap,
        child: Image.asset(
          photo,
          fit: BoxFit.contain,
        ),
      ),
    );
  }
}

Información clave:

  • El InkWell captura el gesto de toque. La función que lo llama pasa la función onTap() al constructor de Photo.
  • Durante el vuelo, el InkWell dibuja su efecto splash sobre su primer ancestro Material.
  • El widget Material tiene un color ligeramente opaco, por lo que las partes transparentes de la imagen se renderizan con color. Esto asegura que la transición de círculo a cuadrado sea fácil de ver, incluso para imágenes con transparencia.
  • La clase Photo no incluye al Hero en su árbol de widgets. Para que la animación funcione, el hero envuelve al widget RadialExpansion.

Clase RadialExpansion

#

El widget RadialExpansion, el núcleo de la demostración, construye el árbol de widgets que recorta la imagen durante la transición. La forma recortada resulta de la intersección de un recorte circular (que crece durante el vuelo), con un recorte rectangular (que mantiene un tamaño constante durante todo el proceso).

Para hacer esto, construye el siguiente árbol de widgets:

Árbol de widgets de RadialExpansion

Aquí está el código:

dart
class RadialExpansion extends StatelessWidget {
  const RadialExpansion({
    super.key,
    required this.maxRadius,
    this.child,
  }) : clipRectSize = 2.0 * (maxRadius / math.sqrt2);

  final double maxRadius;
  final double clipRectSize;
  final Widget? child;

  @override
  Widget build(BuildContext context) {
    return ClipOval(
      child: Center(
        child: SizedBox(
          width: clipRectSize,
          height: clipRectSize,
          child: ClipRect(
            child: child, // Photo
          ),
        ),
      ),
    );
  }
}

Información clave:

  • El hero envuelve al widget RadialExpansion.

  • A medida que el hero vuela, su tamaño cambia y, debido a que limita el tamaño de su hijo, el widget RadialExpansion cambia de tamaño para coincidir.

  • La animación RadialExpansion se crea mediante dos recortes superpuestos.

  • El ejemplo define la interpolación tweening utilizando MaterialRectCenterArcTween. La trayectoria de vuelo por defecto para una animación hero interpola los tweens usando las esquinas de los héroes. Este enfoque afecta la relación de aspecto del hero durante la transformación radial, por lo que la nueva trayectoria de vuelo utiliza MaterialRectCenterArcTween para interpolar los tweens usando el punto central de cada hero.

    Aquí está el código:

    dart
    static RectTween _createRectTween(Rect? begin, Rect? end) {
      return MaterialRectCenterArcTween(begin: begin, end: end);
    }
    

    La trayectoria de vuelo del hero sigue siguiendo un arco, pero la relación de aspecto de la imagen permanece constante.