Crear una animación de menú escalonada
Cómo implementar una animación de menú escalonado.
Una sola pantalla de aplicación puede contener múltiples animaciones. Reproducir todas las animaciones al mismo tiempo puede resultar abrumador. Reproducir las animaciones una tras otra puede llevar demasiado tiempo. Una mejor opción es escalonar las animaciones. Cada animación comienza en un momento diferente, pero las animaciones se superponen para crear una duración total más corta. En esta receta, construirás un menú lateral (drawer) con contenido animado que se escalona y tiene un botón que aparece en la parte inferior.
La siguiente animación muestra el comportamiento de la app:
Crear el menú sin animaciones
#El menú lateral (drawer) muestra una lista de títulos, seguida de un botón de Comenzar (Get started) en la parte inferior del menú.
Define un Widget Stateful llamado Menu
que muestre la lista y el botón
en ubicaciones estáticas.
class Menu extends StatefulWidget {
const Menu({super.key});
@override
State<Menu> createState() => _MenuState();
}
class _MenuState extends State<Menu> {
static const _menuTitles = [
'Declarative Style',
'Premade Widgets',
'Stateful Hot Reload',
'Native Performance',
'Great Community',
];
@override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
child: Stack(
fit: StackFit.expand,
children: [_buildFlutterLogo(), _buildContent()],
),
);
}
Widget _buildFlutterLogo() {
// TODO: We'll implement this later.
return Container();
}
Widget _buildContent() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 16),
..._buildListItems(),
const Spacer(),
_buildGetStartedButton(),
],
);
}
List<Widget> _buildListItems() {
final listItems = <Widget>[];
for (var i = 0; i < _menuTitles.length; ++i) {
listItems.add(
Padding(
padding: const EdgeInsets.symmetric(horizontal: 36, vertical: 16),
child: Text(
_menuTitles[i],
textAlign: TextAlign.left,
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.w500),
),
),
);
}
return listItems;
}
Widget _buildGetStartedButton() {
return SizedBox(
width: double.infinity,
child: Padding(
padding: const EdgeInsets.all(24),
child: ElevatedButton(
style: ElevatedButton.styleFrom(
shape: const StadiumBorder(),
backgroundColor: Colors.blue,
padding: const EdgeInsets.symmetric(horizontal: 48, vertical: 14),
),
onPressed: () {},
child: const Text(
'Get Started',
style: TextStyle(color: Colors.white, fontSize: 22),
),
),
),
);
}
}
Prepararse para las animaciones
#El control del tiempo de la animación requiere un
AnimationController.
Añade el SingleTickerProviderStateMixin
a la clase MenuState. Luego, declara e
instancia un AnimationController.
class _MenuState extends State<Menu> with SingleTickerProviderStateMixin {
late AnimationController _staggeredController;
@override
void initState() {
super.initState();
_staggeredController = AnimationController(vsync: this);
}
@override
void dispose() {
_staggeredController.dispose();
super.dispose();
}
}
La duración del retraso antes de cada animación depende de ti. Define los retrasos de las animaciones, las duraciones de las animaciones individuales y la duración total de la animación.
class _MenuState extends State<Menu> with SingleTickerProviderStateMixin {
static const _initialDelayTime = Duration(milliseconds: 50);
static const _itemSlideTime = Duration(milliseconds: 250);
static const _staggerTime = Duration(milliseconds: 50);
static const _buttonDelayTime = Duration(milliseconds: 150);
static const _buttonTime = Duration(milliseconds: 500);
final Duration _animationDuration =
_initialDelayTime +
(_staggerTime * _menuTitles.length) +
_buttonDelayTime +
_buttonTime;
}
En este caso, todas las animaciones se retrasan 50 ms. Después de eso, los elementos de la lista comienzan a aparecer. La aparición de cada elemento de la lista se retrasa 50 ms después de que el elemento de lista anterior comience a deslizarse. Cada elemento de la lista tarda 250 ms en deslizarse de derecha a izquierda. Después de que el último elemento de la lista comience a deslizarse, el botón en la parte inferior espera otros 150 ms para aparecer. La animación del botón tarda 500 ms.
Con cada retraso y duración de animación definidos, se calcula la duración total para que pueda usarse para calcular los tiempos de animación individuales.
Los tiempos de animación deseados se muestran en el siguiente diagrama:
Para animar un valor durante una subsección de una animación más grande,
Flutter proporciona la clase Interval.
Un Interval toma un porcentaje de tiempo de inicio y un porcentaje
de tiempo de finalización. Ese Interval se puede usar luego para
animar un valor entre esos tiempos de inicio y finalización,
en lugar de usar los tiempos de inicio y finalización de toda
la animación. Por ejemplo, dada una animación que tarda 1 segundo,
un intervalo de 0.2 a 0.5 comenzaría a los 200 ms
(20%) y terminaría a los 500 ms (50%).
Declara y calcula el Interval de cada elemento de la lista
y el Interval del botón inferior.
class _MenuState extends State<Menu> with SingleTickerProviderStateMixin {
final List<Interval> _itemSlideIntervals = [];
late Interval _buttonInterval;
@override
void initState() {
super.initState();
_createAnimationIntervals();
_staggeredController = AnimationController(
vsync: this,
duration: _animationDuration,
);
}
void _createAnimationIntervals() {
for (var i = 0; i < _menuTitles.length; ++i) {
final startTime = _initialDelayTime + (_staggerTime * i);
final endTime = startTime + _itemSlideTime;
_itemSlideIntervals.add(
Interval(
startTime.inMilliseconds / _animationDuration.inMilliseconds,
endTime.inMilliseconds / _animationDuration.inMilliseconds,
),
);
}
final buttonStartTime =
Duration(milliseconds: _menuTitles.length * 50) + _buttonDelayTime;
final buttonEndTime = buttonStartTime + _buttonTime;
_buttonInterval = Interval(
buttonStartTime.inMilliseconds / _animationDuration.inMilliseconds,
buttonEndTime.inMilliseconds / _animationDuration.inMilliseconds,
);
}
}
Animar los elementos de la lista y el botón
#La animación escalonada se reproduce tan pronto como el menú se vuelve visible.
Inicia la animación en initState().
@override
void initState() {
super.initState();
_createAnimationIntervals();
_staggeredController = AnimationController(
vsync: this,
duration: _animationDuration,
)..forward();
}
Cada elemento de la lista se desliza de derecha a izquierda y se desvanece al mismo tiempo.
Utiliza el Interval del elemento de la lista y una curva easeOut
para animar los valores de opacidad y traslación
de cada elemento de la lista.
List<Widget> _buildListItems() {
final listItems = <Widget>[];
for (var i = 0; i < _menuTitles.length; ++i) {
listItems.add(
AnimatedBuilder(
animation: _staggeredController,
builder: (context, child) {
final animationPercent = Curves.easeOut.transform(
_itemSlideIntervals[i].transform(_staggeredController.value),
);
final opacity = animationPercent;
final slideDistance = (1.0 - animationPercent) * 150;
return Opacity(
opacity: opacity,
child: Transform.translate(
offset: Offset(slideDistance, 0),
child: child,
),
);
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 36, vertical: 16),
child: Text(
_menuTitles[i],
textAlign: TextAlign.left,
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.w500),
),
),
),
);
}
return listItems;
}
Utiliza el mismo enfoque para animar la opacidad y la
escala del botón inferior. Esta vez, utiliza una curva
elasticOut para darle al botón un efecto elástico.
Widget _buildGetStartedButton() {
return SizedBox(
width: double.infinity,
child: Padding(
padding: const EdgeInsets.all(24),
child: AnimatedBuilder(
animation: _staggeredController,
builder: (context, child) {
final animationPercent = Curves.elasticOut.transform(
_buttonInterval.transform(_staggeredController.value),
);
final opacity = animationPercent.clamp(0.0, 1.0);
final scale = (animationPercent * 0.5) + 0.5;
return Opacity(
opacity: opacity,
child: Transform.scale(scale: scale, child: child),
);
},
child: ElevatedButton(
style: ElevatedButton.styleFrom(
shape: const StadiumBorder(),
backgroundColor: Colors.blue,
padding: const EdgeInsets.symmetric(horizontal: 48, vertical: 14),
),
onPressed: () {},
child: const Text(
'Get Started',
style: TextStyle(color: Colors.white, fontSize: 22),
),
),
),
),
);
}
¡Felicitaciones! Tienes un menú animado donde la aparición de cada elemento de la lista está escalonada, seguido de un botón inferior que aparece en su lugar.
Ejemplo interactivo
#A menos que se indique lo contrario, la documentación de este sitio refleja Flutter 3.44.0. Página actualizada por última vez el 2026-05-05. Ver código fuente oreportar un problema.