Flutter'da Animasyonlar: AnimationController, Tween ve Hero
Flutter'ın animasyon sistemi iki katmana ayrılır: Explicit (açık) animasyonlar — AnimationController ile her kareyi elle yönettiğiniz; ve Implicit (örtük) animasyonlar — bir değer değiştiğinde Flutter'ın geçişi otomatik yönettiği. Her iki yaklaşımı ve Hero geçişlerini bu yazıda inceleyeceğiz.
AnimationController ve Tween
AnimationController, 0.0 ile 1.0 arasında değişen ham bir sayısal değer üretir. Tween bu değeri istediğiniz tipte bir aralığa dönüştürür. Animasyon kodunuzu doğrudan build'e koyarsanız her frame'de tüm widget ağacı yeniden oluşur. Bunu önlemek için AnimatedBuilder kullanın:
class SlideInCard extends StatefulWidget {
final Widget child;
const SlideInCard({super.key, required this.child});
@override
State<SlideInCard> createState() => _SlideInCardState();
}
class _SlideInCardState extends State<SlideInCard>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<Offset> _slideAnim;
late Animation<double> _fadeAnim;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 600),
);
_slideAnim = Tween<Offset>(
begin: const Offset(0, 0.3),
end: Offset.zero,
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic));
_fadeAnim = Tween<double>(begin: 0, end: 1)
.animate(CurvedAnimation(parent: _controller, curve: Curves.easeIn));
_controller.forward();
}
@override
void dispose() {
_controller.dispose(); // bellek sızıntısını önler
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) => FadeTransition(
opacity: _fadeAnim,
child: SlideTransition(position: _slideAnim, child: child),
),
child: widget.child, // sabit alt widget — her frame'de yeniden oluşmaz
);
}
}
Stagger Animasyon: Sıralı Gecikmeli Animasyonlar
Birden fazla öğeyi art arda canlandırmak için tek bir controller üzerinde Interval ile farklı aralıklar tanımlayın:
class StaggeredList extends StatefulWidget {
final List<Widget> items;
const StaggeredList({super.key, required this.items});
@override
State<StaggeredList> createState() => _StaggeredListState();
}
class _StaggeredListState extends State<StaggeredList>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: Duration(milliseconds: 400 + widget.items.length * 80),
)..forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
Animation<double> _itemAnim(int index) {
final start = index * 0.1;
final end = (start + 0.5).clamp(0.0, 1.0);
return CurvedAnimation(
parent: _controller,
curve: Interval(start, end, curve: Curves.easeOutBack),
);
}
@override
Widget build(BuildContext context) {
return Column(
children: [
for (var i = 0; i < widget.items.length; i++)
AnimatedBuilder(
animation: _itemAnim(i),
builder: (context, child) => Transform.translate(
offset: Offset(0, 30 * (1 - _itemAnim(i).value)),
child: Opacity(
opacity: _itemAnim(i).value.clamp(0.0, 1.0),
child: child,
),
),
child: widget.items[i],
),
],
);
}
}
Hero Animasyonu: Sayfalar Arası Geçiş
İki sayfada aynı tag'e sahip iki Hero widget'ı varsa Flutter otomatik geçiş animasyonu oluşturur:
// Liste sayfası
GestureDetector(
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => ProductDetailPage(product: product)),
),
child: Hero(
tag: 'product-image-${product.id}',
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.network(product.imageUrl, width: 100, height: 100,
fit: BoxFit.cover),
),
),
)
// Detay sayfası — aynı tag
Hero(
tag: 'product-image-${product.id}',
child: Image.network(product.imageUrl, width: double.infinity, height: 280,
fit: BoxFit.cover),
)
// Geçişin eğrisini özelleştirme
PageRouteBuilder(
transitionDuration: const Duration(milliseconds: 500),
pageBuilder: (_, __, ___) => ProductDetailPage(product: product),
transitionsBuilder: (_, animation, __, child) =>
FadeTransition(opacity: animation, child: child),
)
Implicit Animasyonlar: AnimatedContainer ve TweenAnimationBuilder
Durum değişikliklerinde otomatik geçiş için implicit animasyon widget'larını tercih edin. Çok daha az kod gerektirir:
// AnimatedContainer: özellikler değişince otomatik geçiş yapar
class ThemeToggleCard extends StatefulWidget {
const ThemeToggleCard({super.key});
@override
State<ThemeToggleCard> createState() => _ThemeToggleCardState();
}
class _ThemeToggleCardState extends State<ThemeToggleCard> {
bool _isDark = false;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => setState(() => _isDark = !_isDark),
child: AnimatedContainer(
duration: const Duration(milliseconds: 400),
curve: Curves.easeInOut,
width: 200,
height: 100,
decoration: BoxDecoration(
color: _isDark ? Colors.grey[900] : Colors.amber[100],
borderRadius: BorderRadius.circular(_isDark ? 30 : 8),
boxShadow: [
BoxShadow(
color: _isDark ? Colors.black54 : Colors.orange.withOpacity(0.3),
blurRadius: _isDark ? 20 : 5,
),
],
),
child: Center(
child: AnimatedDefaultTextStyle(
duration: const Duration(milliseconds: 400),
style: TextStyle(
fontSize: _isDark ? 28 : 16,
color: _isDark ? Colors.white : Colors.orange[800],
),
child: Text(_isDark ? '🌙' : '☀️'),
),
),
),
);
}
}
// TweenAnimationBuilder: herhangi bir değeri animasyonla değiştirin
TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: progress),
duration: const Duration(milliseconds: 800),
curve: Curves.easeOutCubic,
builder: (context, value, _) => LinearProgressIndicator(value: value),
)
AnimationController Durumlarını Yönetmek
// forward: 0 → 1
// reverse: 1 → 0
// repeat: sürekli döngü
// stop: dur
// Animasyon tamamlanınca callback
_controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_controller.reverse();
} else if (status == AnimationStatus.dismissed) {
_controller.forward();
}
});
// Pulse efekti için tekrarlayan animasyon
_controller.repeat(reverse: true);
// Belirli bir değere atla
_controller.animateTo(0.5, duration: const Duration(milliseconds: 200));
Flutter animasyon sistemi güçlü ama başlangıçta karmaşık görünebilir. Kuralı şu şekilde belirleyin: değer değişince otomatik geçiş yeterliyse implicit widget kullanın; zamanlama ve sırayı kontrol etmeniz gerekiyorsa explicit AnimationController tercih edin.