Dale vida a tus aplicaciones Flutter con animaciones

feb. 1, 2025

Este artículo forma parte de una serie:

Las animaciones son un elemento clave en el desarrollo de aplicaciones modernas. No solo hacen que una interfaz sea más atractiva visualmente, sino que también mejoran la experiencia del usuario al proporcionar transiciones fluidas y efectos intuitivos. Siempre y cuándo estén bien diseñadas y no interfieran con la usabilidad de la aplicación.

A lo largo de la serie de Flutter hemos explorado diferentes conceptos, desde el lenguaje Dart, la creación de interfaces de usuario, los widgets más comunes, hasta asincronía. En esta ocasión veremos cómo crear animaciones para dotar a nuestras aplicaciones de un aspecto más dinámico y atractivo.

Flutter, gracias a su motor gráfico altamente optimizado, facilita la implementación de animaciones sin afectar el rendimiento de la aplicación. En este artículo, aprenderemos a utilizar tres tipos básicos de animaciones en Flutter, con ejemplos prácticos y fáciles de implementar:

  • Animaciones Implícitas – Flutter gestiona la interpolación de valores automáticamente.
  • Animaciones Explícitas – Control manual de la animación mediante AnimationController.
  • Hero Animation – Transición suave entre pantallas usando un elemento compartido.

Después, desarrollaremos una aplicación que nos permitirá mostrar cada uno de estos tipos de animaciones en una sola app, a la vez que analizamos cada uno de los casos más habituales y sus posibles usos.

Tipos de animaciones básicas en Flutter y sus usos

Flutter posee un sistema de animaciones potente y flexible que se adapta a diferentes necesidades. Para empezar, es fundamental conocer las tres animaciones más básicas que nos permitirán dar vida a nuestra UI de manera sencilla y efectiva.

A continuación, exploraremos cómo funcionan, en qué se diferencian y cuándo es recomendable usarlas.

Animaciones Implícitas

Las animaciones implícitas son las más sencillas de implementar en Flutter. Se utilizan cuando queremos que ciertos cambios en el estado de un widget se animen automáticamente sin necesidad de gestionar manualmente el tiempo o la interpolación.

¿Cómo funcionan?

  • Flutter detecta los cambios en las propiedades del widget y anima la transición entre los valores anteriores y nuevos.
  • No es necesario un AnimationController, ya que la animación es gestionada internamente.
  • Se definen mediante widgets específicos como:
    • AnimatedContainer – Anima propiedades como tamaño, color y bordes.
    • AnimatedOpacity – Cambia la transparencia de un widget.
    • AnimatedAlign – Modifica la alineación de un widget con suavidad.
    • AnimatedSwitcher – Anima la transición entre widgets cuando se reemplazan.

¿Cuándo utilizarlas?

  • Cuando se necesita una animación simple y automática sin mucho control.
  • Para efectos de UI como cambios de tamaño, color o alineación en respuesta a una interacción del usuario.
  • Para hacer transiciones suaves entre widgets sin escribir código complejo.

Casos de uso comunes

  • Un botón que cambia de color al ser presionado.
  • Un contenedor que crece o se encoge cuando el usuario interactúa con él.
  • Un icono que desaparece o aparece con una transición de opacidad.

Animaciones Explícitas

Las animaciones explícitas ofrecen un mayor control sobre el tiempo, la velocidad y el comportamiento de la animación. Son útiles cuando queremos definir transiciones más precisas o cuando las animaciones dependen de interacciones más complejas.

¿Cómo funcionan?

  • Se usa un AnimationController para gestionar el tiempo de la animación.
  • Se definen rangos de interpolación con Tween y curvas de animación con CurvedAnimation.
  • Se puede utilizar AnimatedBuilder para optimizar el rendimiento al reconstruir solo las partes necesarias del widget.

¿Cuándo utilizarlas?

  • Cuando se necesita control preciso sobre la duración y el estado de la animación.
  • Para animaciones que dependen de gestos del usuario o múltiples estados.
  • Para efectos más avanzados como animaciones en bucle, aceleraciones personalizadas o transformaciones más complejas.

Casos de uso comunes

  • Un botón que crece y se reduce de tamaño gradualmente.
  • Una barra de progreso animada controlada manualmente.
  • Un widget que rebota al final de una animación usando una curva personalizada.

Hero Animation

Las animaciones Hero permiten crear transiciones suaves entre pantallas cuando un elemento mantiene su apariencia visual en ambas vistas. Son ideales para mejorar la experiencia de navegación en una app.

¿Cómo funcionan?

  • Se usa el widget Hero, que asigna una etiqueta compartida (tag) a un widget en ambas pantallas.
  • Cuando se cambia de pantalla, Flutter anima automáticamente la transformación del widget entre su posición de origen y destino.

Vemos que en nuestro ejemplo práctico hay un pequeño error con el tamaño del texto al hacer la animación de Hero, sería recomendable investigar el porqué y arreglarlo, probablemente se deba a cómo se está manejando el cambio de tamaño del widget al hacer la transición.

¿Cuándo utilizarlas?

  • Para hacer que las transiciones entre pantallas sean más fluidas.
  • Cuando queremos destacar un elemento en una nueva pantalla (por ejemplo, al abrir una imagen en detalle).
  • Para mejorar la navegación entre vistas relacionadas.

Casos de uso comunes

  • Un usuario selecciona una imagen en una galería y esta se expande en una nueva pantalla con detalles.
  • Un producto en una lista se anima hacia una vista detallada al tocarlo.
  • Un avatar en una lista de contactos se agranda en la pantalla de perfil del usuario.

En la siguiente parte del artículo, crearemos una aplicación en Flutter que muestre cada una de estas animaciones con ejemplos prácticos.

Aplicación de ejemplo con animaciones

Esta aplicación en Flutter nos permite explorar los tres tipos de animaciones mencionados anteriormente:

  • Animaciones Implícitas – Cambios suaves de propiedades como tamaño, color y alineación.
  • Animaciones Explícitas – Control manual de la animación con AnimationController.
  • Hero Animation – Transición fluida de un widget entre pantallas.

A continuación, explicaremos el propósito de cada pantalla y los widgets utilizados.

Pantalla Principal

La pantalla principal actúa como un menú para acceder a las diferentes animaciones. Contiene:

  • Un título que introduce las animaciones.
  • Tres tarjetas (Card) que sirven como botones para navegar a las pantallas de cada animación.
  • Cada tarjeta incluye un ícono, un título y una breve descripción.

Widgets utilizados

  • Scaffold: Estructura base de la pantalla.
  • Column: Organización de los elementos en una lista vertical.
  • Card: Diseño de botones con una apariencia atractiva.
  • InkWell: Permite detectar clics en cada tarjeta.
  • Navigator: Navegación a las pantallas de animaciones.

Código fuente


  class HomeScreen extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
      return Scaffold(
        appBar: AppBar(
          title: Text('Animation Demo'),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Text(
                'Explore Different Types of Animations',
                style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
              ),
              SizedBox(height: 30),
              _buildAnimationButton(
                context,
                'Implicit Animations',
                'Automatically animate size, color, opacity, and alignment.',
                Icons.animation,
                ImplicitAnimationsScreen(),
              ),
              SizedBox(height: 20),
              _buildAnimationButton(
                context,
                'Explicit Animations',
                'Manually control scaling animations with start/reverse buttons.',
                Icons.touch_app,
                ExplicitAnimationsScreen(),
              ),
              SizedBox(height: 20),
              _buildAnimationButton(
                context,
                'Hero Animation',
                'Tap the widget to see a smooth transition between screens.',
                Icons.flight_takeoff,
                HeroAnimationScreen(),
              ),
            ],
          ),
        ),
      );
    }
  
    Widget _buildAnimationButton(BuildContext context, String title, String description, IconData icon, Widget screen) {
      return Card(
        elevation: 4,
        child: InkWell(
          onTap: () {
            Navigator.push(
              context,
              MaterialPageRoute(builder: (context) => screen),
            );
          },
          child: Padding(
            padding: const EdgeInsets.all(16.0),
            child: Column(
              children: [
                Icon(icon, size: 40, color: Colors.blue),
                SizedBox(height: 10),
                Text(
                  title,
                  style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                ),
                SizedBox(height: 5),
                Text(
                  description,
                  style: TextStyle(fontSize: 12, color: Colors.grey),
                  textAlign: TextAlign.center,
                ),
              ],
            ),
          ),
        ),
      );
    }
  }
    

Pantalla de animaciones implícitas

Esta pantalla muestra cómo usar animaciones implícitas en Flutter. Los cambios de estado en los widgets activan automáticamente las transiciones de tamaño, color y alineación.

Incluye tres ejemplos de animaciones:

  1. AnimatedContainer – Cambia de tamaño y color al presionar un botón.
  2. AnimatedOpacity – Ajusta la transparencia de un widget.
  3. AnimatedAlign – Modifica la alineación de un widget dentro de su contenedor.

Cada animación se activa al presionar un botón, cambiando el estado de la pantalla.

Widgets utilizados

  • AnimatedContainer: Para transiciones suaves en propiedades visuales.
  • AnimatedOpacity: Para efectos de desvanecimiento.
  • AnimatedAlign: Para animar el posicionamiento de un widget.
  • FloatingActionButton: Para regresar a la pantalla principal.

Código fuente

  
  class _ImplicitAnimationsScreenState extends State<ImplicitAnimationsScreen> {
    bool _expanded = false;
    bool _visible = true;
    Alignment _alignment = Alignment.topLeft;
  
    void _toggleExpanded() {
      setState(() {
        _expanded = !_expanded;
      });
    }
  
    void _toggleVisibility() {
      setState(() {
        _visible = !_visible;
      });
    }
  
    void _changeAlignment() {
      setState(() {
        _alignment = _alignment == Alignment.topLeft
            ? Alignment.bottomRight
            : Alignment.topLeft;
      });
    }
  
    @override
    Widget build(BuildContext context) {
      return Scaffold(
        appBar: AppBar(
          title: Text('Implicit Animations'),
        ),
        body: SingleChildScrollView(
          padding: const EdgeInsets.all(16.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text(
                '1. AnimatedContainer',
                style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
              ),
              Text('Tap the button to toggle size and color.'),
              SizedBox(height: 10),
              Center(
                child: AnimatedContainer(
                  duration: Duration(seconds: 1),
                  width: _expanded ? 200 : 100,
                  height: _expanded ? 200 : 100,
                  color: _expanded ? Colors.blue : Colors.red,
                  child: Center(
                    child: Text(
                      _expanded ? 'Expanded' : 'Small',
                      style: TextStyle(color: Colors.white),
                    ),
                  ),
                ),
              ),
              SizedBox(height: 20),
              ElevatedButton(
                onPressed: _toggleExpanded,
                child: Text('Toggle Container'),
              ),
              SizedBox(height: 30),
              Text(
                '2. AnimatedOpacity',
                style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
              ),
              Text('Tap the button to toggle visibility.'),
              SizedBox(height: 10),
              Center(
                child: AnimatedOpacity(
                  opacity: _visible ? 1.0 : 0.0,
                  duration: Duration(seconds: 1),
                  child: Container(
                    width: 100,
                    height: 100,
                    color: Colors.green,
                    child: Center(
                      child: Text(
                        _visible ? 'Visible' : 'Invisible',
                        style: TextStyle(color: Colors.white),
                      ),
                    ),
                  ),
                ),
              ),
              SizedBox(height: 20),
              ElevatedButton(
                onPressed: _toggleVisibility,
                child: Text('Toggle Opacity'),
              ),
              SizedBox(height: 30),
              Text(
                '3. AnimatedAlign',
                style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
              ),
              Text('Tap the button to change alignment.'),
              SizedBox(height: 10),
              Container(
                height: 200,
                width: double.infinity,
                color: Colors.grey[200],
                child: AnimatedAlign(
                  alignment: _alignment,
                  duration: Duration(seconds: 1),
                  child: Container(
                    width: 100,
                    height: 100,
                    color: Colors.orange,
                    child: Center(
                      child: Text(
                        _alignment == Alignment.topLeft
                            ? 'Top Left'
                            : 'Bottom Right',
                        style: TextStyle(color: Colors.white),
                      ),
                    ),
                  ),
                ),
              ),
              SizedBox(height: 20),
              ElevatedButton(
                onPressed: _changeAlignment,
                child: Text('Change Alignment'),
              ),
            ],
          ),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () {
            Navigator.pop(context);
          },
          child: Icon(Icons.arrow_back),
        ),
      );
    }
  }
  

Pantalla de animaciones explícitas

En esta vemos el uso de animaciones explícitas, donde el desarrollador controla la animación manualmente utilizando un AnimationController.

Incluye un ejemplo donde un Container cambia de tamaño con un efecto de suavizado.

  • Un botón inicia la animación.
  • Otro botón revierte la animación.

Widgets utilizados

  • AnimationController: Controla la duración y el estado de la animación.
  • Tween<double>: Define un rango de valores para interpolar la animación.
  • CurvedAnimation: Aplica una curva de aceleración y desaceleración a la animación.
  • AnimatedBuilder: Optimiza el rendimiento evitando reconstrucciones innecesarias.
  • FloatingActionButton: Botón flotante clásico, en este caso, para regresar a la pantalla principal.

Código fuente

  
  class _ExplicitAnimationsScreenState extends State<ExplicitAnimationsScreen> with SingleTickerProviderStateMixin {
    late AnimationController _controller;
    late Animation<double> _animation;
    bool _isAnimating = false;
  
    @override
    void initState() {
      super.initState();
      _controller = AnimationController(
        duration: Duration(seconds: 1),
        vsync: this,
      );
      _animation = Tween<double>(begin: 100, end: 200).animate(
        CurvedAnimation(
          parent: _controller,
          curve: Curves.easeInOut,
        ),
      );
    }
  
    void _startAnimation() {
      _controller.forward();
      setState(() {
        _isAnimating = true;
      });
    }
  
    void _reverseAnimation() {
      _controller.reverse();
      setState(() {
        _isAnimating = false;
      });
    }
  
    @override
    void dispose() {
      _controller.dispose();
      super.dispose();
    }
  
    @override
    Widget build(BuildContext context) {
      return Scaffold(
        appBar: AppBar(
          title: Text('Explicit Animations'),
        ),
        body: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text(
                '1. Scaling Animation',
                style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
              ),
              Text('Use the buttons to start or reverse the animation.'),
              SizedBox(height: 20),
              Center(
                child: AnimatedBuilder(
                  animation: _animation,
                  builder: (context, child) {
                    return Container(
                      width: _animation.value,
                      height: _animation.value,
                      color: Colors.purple,
                      child: Center(
                        child: Text(
                          _isAnimating ? 'Expanding' : 'Shrinking',
                          style: TextStyle(color: Colors.white),
                        ),
                      ),
                    );
                  },
                ),
              ),
              SizedBox(height: 20),
              Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  ElevatedButton(
                    onPressed: _startAnimation,
                    child: Text('Start Animation'),
                  ),
                  SizedBox(width: 20),
                  ElevatedButton(
                    onPressed: _reverseAnimation,
                    child: Text('Reverse Animation'),
                  ),
                ],
              ),
            ],
          ),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () {
            Navigator.pop(context);
          },
          child: Icon(Icons.arrow_back),
        ),
      );
    }
  }
    

Pantalla de animación Hero

Para terminar, programamos una animación Hero para una transición fluida entre pantallas.

  • Un usuario toca un widget en la pantalla principal.
  • El widget se expande y se mueve suavemente a la segunda pantalla.
  • Al regresar, el widget se contrae volviendo a su posición original.

Widgets utilizados

  • Hero: Define el widget que se animará entre pantallas.
  • Navigator: Gestiona la navegación entre pantallas.
  • GestureDetector: Detecta el toque del usuario para activar la animación.

Código fuente

  
  class HeroAnimationScreen extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
      return Scaffold(
        appBar: AppBar(
          title: Text('Hero Animation'),
        ),
        body: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text(
                '1. Hero Transition',
                style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
              ),
              Text('Tap the widget to see a smooth transition to the next screen.'),
              SizedBox(height: 20),
              Center(
                child: GestureDetector(
                  onTap: () {
                    Navigator.push(
                      context,
                      MaterialPageRoute(builder: (context) => HeroDetailScreen()),
                    );
                  },
                  child: Hero(
                    tag: 'heroTag',
                    child: Container(
                      width: 100,
                      height: 100,
                      color: Colors.teal,
                      child: Center(
                        child: Text(
                          'Tap Me!',
                          style: TextStyle(color: Colors.white),
                        ),
                      ),
                    ),
                  ),
                ),
              ),
            ],
          ),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () {
            Navigator.pop(context);
          },
          child: Icon(Icons.arrow_back),
        ),
      );
    }
  }
  

Cómo decíamos anteriormente, existe un pequeño error en la animación de Hero, el texto no se ajusta correctamente al cambiar de tamaño, sería un buen punto de partida para seguir investigando y mejorando nuestros conocimientos sobre este tipo de animaciones.

Así cómo hemos comentado las ventajas de utilizar animaciones en nuestras aplicaciones, también es importante tener en cuenta que un uso excesivo de animaciones puede resultar en una experiencia de usuario confusa o desagradable. Es fundamental equilibrar la cantidad y la intensidad de las animaciones para garantizar una experiencia de usuario agradable y efectiva.

Hemos tratado los conceptos básicos de animaciones en Flutter y creado una aplicación de ejemplo que muestra cada uno de estos tipos de animaciones en acción. Ahora, puedes aplicar estos conocimientos a tus propios proyectos y dar vida a tus interfaces de usuario con animaciones atractivas y efectivas. Espero que te haya resultado útil y sobre todo desearte un muy Happy Coding!

Artículos relacionados

Quizá te puedan interesar

September 15, 2024

Asincronía en Flutter

Introducción a la Asincronía en Programación En el mundo de la programación, uno de los mayores …

leer más