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
AnimationControllerpara gestionar el tiempo de la animación. - Se definen rangos de interpolación con
Tweeny curvas de animación conCurvedAnimation. - Se puede utilizar
AnimatedBuilderpara 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:
AnimatedContainer– Cambia de tamaño y color al presionar un botón.AnimatedOpacity– Ajusta la transparencia de un widget.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!