Comprende la mecánica interna de las animaciones implícitas y controla con precisión tu propio AnimationController.
Abrir esta lección en KodokonAnimatedContainer pertenece a la familia de las animaciones implícitas: cambias un valor objetivo y el framework anima la transición. Por dentro, cada ImplicitlyAnimatedWidget guarda un AnimationController oculto en su estado. Cuando llega una nueva configuración, didUpdateWidget recorre las propiedades mediante forEachTween y reapunta cada tween: el límite begin se convierte en el valor interpolado actual, el límite end en el nuevo objetivo, y luego el controlador reinicia desde cero. Una consecuencia valiosa: cambiar el objetivo a mitad de animación no provoca ningún salto visual: la animación retoma exactamente donde estaba.
import 'package:flutter/material.dart';
class SelectableCard extends StatefulWidget {
const SelectableCard({super.key});
@override
State<SelectableCard> createState() =>
_SelectableCardState();
}
class _SelectableCardState extends State<SelectableCard> {
bool _selected = false;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () =>
setState(() => _selected = !_selected),
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeOutCubic,
width: _selected ? 220 : 160,
height: 96,
decoration: BoxDecoration(
color: _selected
? Colors.indigo
: Colors.indigo.shade200,
borderRadius: BorderRadius.circular(
_selected ? 28 : 12,
),
),
),
);
}
}Las implícitas cubren las propiedades que el framework anticipa, y TweenAnimationBuilder generaliza el proceso a cualquier valor interpolable. En cuanto necesites controlar la animación (pausar, invertir, repetir, reaccionar al estado), pasa al AnimationController explícito. El parámetro vsync espera un TickerProvider: el mixin crea un Ticker, un callback invocado en cada frame por el SchedulerBinding. Una sutileza poco conocida: TickerMode silencia los tickers de un subárbol oculto (por ejemplo, bajo una ruta tapada). El tiempo sigue corriendo, pero ya no se ejecuta ningún callback: cero CPU desperdiciada en animaciones invisibles.
import 'package:flutter/material.dart';
class RevealPanel extends StatefulWidget {
const RevealPanel({super.key, required this.child});
final Widget child;
@override
State<RevealPanel> createState() => _RevealPanelState();
}
class _RevealPanelState extends State<RevealPanel>
with SingleTickerProviderStateMixin {
late final AnimationController _controller =
AnimationController(
vsync: this,
duration: const Duration(milliseconds: 400),
reverseDuration: const Duration(milliseconds: 200),
);
late final Animation<double> _opacity = _controller
.drive(CurveTween(curve: Curves.easeOutCubic));
void toggle() {
switch (_controller.status) {
case AnimationStatus.forward:
case AnimationStatus.completed:
_controller.reverse();
case AnimationStatus.reverse:
case AnimationStatus.dismissed:
_controller.forward();
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: toggle,
child: FadeTransition(
opacity: _opacity,
child: widget.child,
),
);
}
}Un controlador no es más que una Animation<double> que va de 0 a 1: compónla en lugar de consumirla en crudo. controller.drive(CurveTween(...)) produce una animación derivada sin estado adicional. Para aplicar el valor al árbol, prefiere los widgets Transition (FadeTransition, RotationTransition): escuchan la animación directamente a nivel del render object, sin reconstruir ni un solo widget por frame. Cuando la transformación es arbitraria, AnimatedBuilder cumple la función, siempre que pases el subárbol estático a través de child: se construye una sola vez y luego se reinyecta en builder en cada frame. Esa es la optimización más rentable para las animaciones explícitas.
import 'dart:math' as math;
import 'package:flutter/material.dart';
class Orbit extends StatefulWidget {
const Orbit({super.key});
@override
State<Orbit> createState() => _OrbitState();
}
class _OrbitState extends State<Orbit>
with SingleTickerProviderStateMixin {
late final AnimationController _controller =
AnimationController(
vsync: this,
duration: const Duration(seconds: 4),
)..repeat();
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
child: const Icon(Icons.satellite_alt, size: 32),
builder: (context, child) {
final angle = _controller.value * 2 * math.pi;
return Transform.translate(
offset: Offset(
math.cos(angle) * 80,
math.sin(angle) * 80,
),
child: child,
);
},
);
}
}