Flutter: Explicit Animations ile Tam Kontrol - AnimationController Rehberi
Implicit animasyonlar harika ama bazen daha fazla kontrole ihtiyacınız var. Animasyonu istediğiniz zaman başlatmak, durdurmak, geri sarmak veya hızını değiştirmek istiyorsanız, Explicit Animations (belirgin animasyonlar) devreye giriyor.
AnimationController, Flutter'ın animasyon motorunun kalbi. Her kare için değer üreten, istediğiniz gibi yönlendirebileceğiniz bir orkestra şefi gibi.
Implicit vs Explicit: Hangisi Ne Zaman?
Hızlı bir hatırlatma:
| Özellik | Implicit | Explicit |
|---|---|---|
| Kullanım | setState ile otomatik | Manuel kontrol |
| Kod Miktarı | Az (3-5 satır) | Fazla (15-30 satır) |
| Kontrol | Sınırlı | Tam kontrol |
| Başlat/Durdur | Otomatik | Manuel |
| Kullanım Alanı | Basit geçişler | Karmaşık animasyonlar |
| Örnek | AnimatedContainer | AnimationController |
Ne Zaman Explicit Kullanmalısınız?
- Animasyonu butona basınca başlatmak istiyorsanız
- Animasyonu sonsuz döngüde çalıştırmak istiyorsanız
- Birden fazla animasyonu senkronize etmek istiyorsanız
- Kullanıcı input'una göre animasyonu kontrol etmek istiyorsanız (örn: kaydırma mesafesine göre)
- Animasyonu geri sarmak istiyorsanız
AnimationController Temelleri
AnimationController, 0.0'dan 1.0'a (veya istediğiniz aralığa) doğru sürekli artan bir değer üretir.
Temel Kullanım
class BasicAnimationDemo extends StatefulWidget {
@override
_BasicAnimationDemoState createState() => _BasicAnimationDemoState();
}
class _BasicAnimationDemoState extends State<BasicAnimationDemo>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(seconds: 2),
vsync: this, // TickerProvider gerekli
);
}
@override
void dispose() {
_controller.dispose(); // Hafıza sızıntısını önlemek için!
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform.rotate(
angle: _controller.value * 2 * 3.14159, // 360 derece
child: child,
);
},
child: Icon(Icons.star, size: 100, color: Colors.amber),
),
SizedBox(height: 40),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () => _controller.forward(),
child: Text('Başlat'),
),
SizedBox(width: 10),
ElevatedButton(
onPressed: () => _controller.reverse(),
child: Text('Geri Sar'),
),
SizedBox(width: 10),
ElevatedButton(
onPressed: () => _controller.reset(),
child: Text('Sıfırla'),
),
],
),
],
);
}
}Canlı Demo
Aşağıdaki interaktif örnekte explicit animation'ları deneyebilirsiniz:
💡 Yukarıdaki örnek yüklenmiyorsa, yeni sekmede çalıştırmak için DartPad linkine tıklayın.
TickerProvider Nedir?
vsync parametresi zorunludur. Peki bu TickerProvider ne işe yarar?
Neden Gerekli?
Flutter, her saniye 60 kare (60 FPS) çizmeye çalışır. AnimationController, her kare için yeni bir değer üretir. Ancak widget ekranda görünmüyorsa (örneğin başka sayfaya geçtiniz), animasyon gereksiz yere CPU harcar.
vsync, widget ekranda olmadığında animasyonu otomatik olarak durdurur. Böylece:
- Batarya tasarrufu
- CPU tasarrufu
- Performans artışı
Hangi Mixin'i Kullanmalısınız?
// Tek animasyon varsa
class _MyWidgetState extends State<MyWidget>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
}
// Birden fazla animasyon varsa
class _MyWidgetState extends State<MyWidget>
with TickerProviderStateMixin {
late AnimationController _controller1;
late AnimationController _controller2;
late AnimationController _controller3;
}AnimationController Metodları
AnimationController bir VCR (video kaset oynatıcı) gibi düşünün:
1. forward() - Oynat
Animasyonu baştan sona çalıştırır (0.0 → 1.0):
_controller.forward();
// Belirli bir değerden başlat
_controller.forward(from: 0.5); // 0.5'ten başla2. reverse() - Geri Sar
Animasyonu sondan başa çalıştırır (1.0 → 0.0):
_controller.reverse();
// Belirli bir değerden geri sar
_controller.reverse(from: 0.8);3. reset() - Sıfırla
Animasyonu 0.0'a döndürür (animasyon çalışmaz):
_controller.reset();4. stop() - Durdur
Animasyonu mevcut değerde durdurur:
_controller.stop();5. repeat() - Sonsuz Döngü
Animasyonu sürekli tekrarlar:
// Sonsuz döngü (0 → 1 → 0 → 1 ...)
_controller.repeat();
// Geri dönüşlü (0 → 1 → 0 → 1 ...)
_controller.repeat(reverse: true);
// Belirli sayıda tekrar
_controller.repeat(reverse: true, period: Duration(seconds: 1));6. animateTo() - Belirli Değere Git
Mevcut değerden belirli bir değere animasyon yapar:
_controller.animateTo(0.7); // Mevcut değerden 0.7'ye git
// Custom duration ile
_controller.animateTo(
0.5,
duration: Duration(milliseconds: 500),
curve: Curves.easeInOut,
);7. animateBack() - Geri Git
Mevcut değerden belirli bir değere geri animasyon yapar:
_controller.animateBack(0.3);Tween: Değer Dönüşümü
AnimationController sadece 0.0 - 1.0 arası değer üretir. Peki 0 - 360 (derece) veya 50 - 200 (piksel) arasında değer istersek?
Tween (between kısaltması) değerleri dönüştürür.
Temel Tween Kullanımı
class TweenDemo extends StatefulWidget {
@override
_TweenDemoState createState() => _TweenDemoState();
}
class _TweenDemoState extends State<TweenDemo>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _sizeAnimation;
late Animation<Color?> _colorAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(seconds: 2),
vsync: this,
);
// Boyut animasyonu: 50 → 200
_sizeAnimation = Tween<double>(
begin: 50,
end: 200,
).animate(_controller);
// Renk animasyonu: Kırmızı → Mavi
_colorAnimation = ColorTween(
begin: Colors.red,
end: Colors.blue,
).animate(_controller);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Container(
width: _sizeAnimation.value,
height: _sizeAnimation.value,
decoration: BoxDecoration(
color: _colorAnimation.value,
borderRadius: BorderRadius.circular(20),
),
);
},
),
SizedBox(height: 40),
ElevatedButton(
onPressed: () {
if (_controller.status == AnimationStatus.completed) {
_controller.reverse();
} else {
_controller.forward();
}
},
child: Text('Animasyonu Çalıştır'),
),
],
);
}
}Yaygın Tween Türleri
// Sayı (double)
Tween<double>(begin: 0, end: 100)
// Sayı (int)
IntTween(begin: 0, end: 255)
// Renk
ColorTween(begin: Colors.red, end: Colors.blue)
// Offset (konum)
Tween<Offset>(begin: Offset.zero, end: Offset(1.0, 0.0))
// Size (boyut)
Tween<Size>(begin: Size(50, 50), end: Size(200, 200))
// BorderRadius
Tween<BorderRadius>(
begin: BorderRadius.circular(0),
end: BorderRadius.circular(50),
)
// TextStyle
TextStyleTween(
begin: TextStyle(fontSize: 14),
end: TextStyle(fontSize: 32, fontWeight: FontWeight.bold),
)CurvedAnimation: Animasyona Karakter Katmak
Tween değerleri dönüştürür, CurvedAnimation ise animasyonun karakterini belirler:
_controller = AnimationController(
duration: Duration(seconds: 2),
vsync: this,
);
// Curve uygulama
final curvedAnimation = CurvedAnimation(
parent: _controller,
curve: Curves.elasticOut, // Elastik efekt
);
// Tween ile birlikte kullanım
_sizeAnimation = Tween<double>(
begin: 50,
end: 200,
).animate(curvedAnimation);Farklı Curve'ler
// Yumuşak giriş-çıkış
Curves.easeInOut
// Elastik (yay gibi)
Curves.elasticOut
// Zıplama
Curves.bounceOut
// Aşırı gitme (overshoot)
Curves.anticipate
// Hızlanma
Curves.accelerate
// Yavaşlama
Curves.decelerateAnimatedBuilder: Performans Dostu Rebuild
AnimatedBuilder, sadece gerekli widget'ı yeniden oluşturur:
AnimatedBuilder(
animation: _controller,
builder: (context, child) {
// Bu kısım her frame'de rebuild edilir
return Transform.rotate(
angle: _controller.value * 2 * 3.14159,
child: child, // Bu rebuild edilmez!
);
},
child: Icon(Icons.star, size: 100), // Sabit, cache'lenir
)Performans İpucu: child parametresi, animasyon boyunca değişmeyen widget'lar için kullanılır. Flutter bunu cache'ler ve her frame'de yeniden oluşturmaz.
Animation Status: Durumu Dinleme
Animasyonun durumunu takip edebilirsiniz:
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(seconds: 2),
vsync: this,
);
_controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
print('Animasyon tamamlandı!');
_controller.reverse(); // Otomatik geri sar
} else if (status == AnimationStatus.dismissed) {
print('Animasyon başa döndü!');
}
});
}Status Türleri
AnimationStatus.forward // İleri gidiyor (0 → 1)
AnimationStatus.reverse // Geri gidiyor (1 → 0)
AnimationStatus.completed // Tamamlandı (1.0'da)
AnimationStatus.dismissed // Başa döndü (0.0'da)Gerçek Dünya Örnekleri
1. Sonsuz Dönme (Loading Spinner)
class LoadingSpinner extends StatefulWidget {
@override
_LoadingSpinnerState createState() => _LoadingSpinnerState();
}
class _LoadingSpinnerState extends State<LoadingSpinner>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(seconds: 2),
vsync: this,
)..repeat(); // Sonsuz döngü
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Center(
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform.rotate(
angle: _controller.value * 2 * 3.14159,
child: child,
);
},
child: Icon(Icons.refresh, size: 50, color: Colors.blue),
),
);
}
}2. Kalp Atışı Animasyonu (Pulsing Heart)
class PulsingHeart extends StatefulWidget {
@override
_PulsingHeartState createState() => _PulsingHeartState();
}
class _PulsingHeartState extends State<PulsingHeart>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _scaleAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(milliseconds: 800),
vsync: this,
)..repeat(reverse: true); // Gidip gel
_scaleAnimation = Tween<double>(
begin: 0.8,
end: 1.2,
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
));
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Center(
child: AnimatedBuilder(
animation: _scaleAnimation,
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value,
child: child,
);
},
child: Icon(Icons.favorite, size: 100, color: Colors.red),
),
);
}
}3. Fade In ve Slide Up Kombinasyonu
class FadeSlideIn extends StatefulWidget {
final Widget child;
FadeSlideIn({required this.child});
@override
_FadeSlideInState createState() => _FadeSlideInState();
}
class _FadeSlideInState extends State<FadeSlideIn>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _opacityAnimation;
late Animation<Offset> _slideAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(milliseconds: 600),
vsync: this,
);
_opacityAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.easeOut,
));
_slideAnimation = Tween<Offset>(
begin: Offset(0, 0.3), // Aşağıdan
end: Offset.zero,
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.easeOut,
));
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return FadeTransition(
opacity: _opacityAnimation,
child: SlideTransition(
position: _slideAnimation,
child: child,
),
);
},
child: widget.child,
);
}
}
// Kullanım:
FadeSlideIn(
child: Text('Merhaba Dünya!', style: TextStyle(fontSize: 32)),
)4. Progress Bar Animasyonu
class AnimatedProgressBar extends StatefulWidget {
final double progress; // 0.0 - 1.0
AnimatedProgressBar({required this.progress});
@override
_AnimatedProgressBarState createState() => _AnimatedProgressBarState();
}
class _AnimatedProgressBarState extends State<AnimatedProgressBar>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _progressAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(milliseconds: 500),
vsync: this,
);
_progressAnimation = Tween<double>(
begin: 0,
end: widget.progress,
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.easeOut,
));
_controller.forward();
}
@override
void didUpdateWidget(AnimatedProgressBar oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.progress != oldWidget.progress) {
_progressAnimation = Tween<double>(
begin: _progressAnimation.value,
end: widget.progress,
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.easeOut,
));
_controller.forward(from: 0);
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
height: 20,
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(10),
),
child: AnimatedBuilder(
animation: _progressAnimation,
builder: (context, child) {
return FractionallySizedBox(
alignment: Alignment.centerLeft,
widthFactor: _progressAnimation.value,
child: Container(
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(10),
),
),
);
},
),
);
}
}5. Shake (Titreme) Animasyonu
class ShakeWidget extends StatefulWidget {
final Widget child;
final bool shake;
ShakeWidget({required this.child, this.shake = false});
@override
_ShakeWidgetState createState() => _ShakeWidgetState();
}
class _ShakeWidgetState extends State<ShakeWidget>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _shakeAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(milliseconds: 500),
vsync: this,
);
_shakeAnimation = Tween<double>(
begin: 0,
end: 10,
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.elasticIn,
));
}
@override
void didUpdateWidget(ShakeWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.shake && !oldWidget.shake) {
_controller.forward(from: 0).then((_) => _controller.reverse());
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _shakeAnimation,
builder: (context, child) {
return Transform.translate(
offset: Offset(_shakeAnimation.value * (_controller.value > 0.5 ? -1 : 1), 0),
child: child,
);
},
child: widget.child,
);
}
}
// Kullanım:
class LoginScreen extends StatefulWidget {
@override
_LoginScreenState createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
bool _showError = false;
void _login() {
// Hatalı giriş
setState(() => _showError = true);
Future.delayed(Duration(milliseconds: 500), () {
setState(() => _showError = false);
});
}
@override
Widget build(BuildContext context) {
return ShakeWidget(
shake: _showError,
child: TextField(
decoration: InputDecoration(
labelText: 'Şifre',
errorText: _showError ? 'Hatalı şifre!' : null,
),
),
);
}
}Birden Fazla Animasyonu Senkronize Etme
Tek controller ile birden fazla animasyon:
class MultiAnimationDemo extends StatefulWidget {
@override
_MultiAnimationDemoState createState() => _MultiAnimationDemoState();
}
class _MultiAnimationDemoState extends State<MultiAnimationDemo>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _rotationAnimation;
late Animation<double> _scaleAnimation;
late Animation<Color?> _colorAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(seconds: 3),
vsync: this,
);
// 0.0 - 0.5 arası dönme
_rotationAnimation = Tween<double>(
begin: 0,
end: 2 * 3.14159,
).animate(CurvedAnimation(
parent: _controller,
curve: Interval(0.0, 0.5, curve: Curves.easeInOut),
));
// 0.5 - 1.0 arası büyüme
_scaleAnimation = Tween<double>(
begin: 1.0,
end: 2.0,
).animate(CurvedAnimation(
parent: _controller,
curve: Interval(0.5, 1.0, curve: Curves.easeOut),
));
// 0.0 - 1.0 arası renk değişimi
_colorAnimation = ColorTween(
begin: Colors.blue,
end: Colors.red,
).animate(_controller);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform.rotate(
angle: _rotationAnimation.value,
child: Transform.scale(
scale: _scaleAnimation.value,
child: Container(
width: 100,
height: 100,
decoration: BoxDecoration(
color: _colorAnimation.value,
borderRadius: BorderRadius.circular(20),
),
),
),
);
},
),
SizedBox(height: 40),
ElevatedButton(
onPressed: () {
if (_controller.status == AnimationStatus.completed) {
_controller.reverse();
} else {
_controller.forward();
}
},
child: Text('Animasyonu Başlat'),
),
],
);
}
}TweenSequence: Adım Adım Animasyon
Farklı aşamalarda farklı değerler:
final _colorAnimation = TweenSequence<Color?>([
TweenSequenceItem(
tween: ColorTween(begin: Colors.red, end: Colors.blue),
weight: 33.3, // %33.3'lük kısım
),
TweenSequenceItem(
tween: ColorTween(begin: Colors.blue, end: Colors.green),
weight: 33.3,
),
TweenSequenceItem(
tween: ColorTween(begin: Colors.green, end: Colors.red),
weight: 33.4,
),
]).animate(_controller);Performans İpuçları
1. dispose() Unutmayın
@override
void dispose() {
_controller.dispose(); // Zorunlu!
super.dispose();
}Unutursanız hafıza sızıntısı olur.
2. AnimatedBuilder Kullanın
// ❌ Kötü - Tüm widget rebuild edilir
@override
Widget build(BuildContext context) {
return Transform.rotate(
angle: _controller.value * 2 * 3.14159,
child: ExpensiveWidget(),
);
}
// ✅ İyi - Sadece gerekli kısım rebuild edilir
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform.rotate(
angle: _controller.value * 2 * 3.14159,
child: child,
);
},
child: ExpensiveWidget(), // Cache'lenir
);
}3. Gereksiz Animasyon Yapmayın
// ❌ 100 widget aynı anda animasyon yapıyor
ListView.builder(
itemCount: 100,
itemBuilder: (context, index) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform.scale(
scale: 1.0 + _controller.value * 0.2,
child: ListTile(title: Text('Item $index')),
);
},
);
},
)
// ✅ İyi - Sadece görünür olanlar animasyon yapsın (lazy loading)4. const Widget'ları Cache'leyin
AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform.rotate(
angle: _controller.value * 2 * 3.14159,
child: child,
);
},
child: const Icon(Icons.star, size: 100), // const!
)Yaygın Hatalar ve Çözümleri
Hata 1: vsync Unutmak
// ❌ Hata: vsync gerekli
_controller = AnimationController(
duration: Duration(seconds: 2),
);
// ✅ Doğru
class _MyWidgetState extends State<MyWidget>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(seconds: 2),
vsync: this, // this, TickerProvider'dır
);
}
}Hata 2: dispose() Unutmak
// ❌ Hafıza sızıntısı!
@override
void dispose() {
super.dispose();
}
// ✅ Doğru
@override
void dispose() {
_controller.dispose();
super.dispose();
}Hata 3: Yanlış Mixin Seçimi
// ❌ 3 animasyon var ama Single kullanılmış
class _MyWidgetState extends State<MyWidget>
with SingleTickerProviderStateMixin {
late AnimationController _controller1;
late AnimationController _controller2;
late AnimationController _controller3; // Hata!
}
// ✅ Doğru
class _MyWidgetState extends State<MyWidget>
with TickerProviderStateMixin {
late AnimationController _controller1;
late AnimationController _controller2;
late AnimationController _controller3;
}Hata 4: Duration Çok Uzun
// ❌ 10 saniye çok uzun
AnimationController(
duration: Duration(seconds: 10),
vsync: this,
)
// ✅ 2-3 saniye ideal
AnimationController(
duration: Duration(seconds: 2),
vsync: this,
)Özet
- AnimationController: 0.0 - 1.0 arası değer üreten kontrol mekanizması
- vsync: Ekranda olmayan animasyonları otomatik durdurur (batarya tasarrufu)
- Tween: Değer dönüşümü (0-1 → 50-200 gibi)
- CurvedAnimation: Animasyona karakter katar (easeInOut, elasticOut, vb.)
- AnimatedBuilder: Performans için sadece gerekli widget'ı rebuild eder
- Status Listener: Animasyon durumunu dinler (completed, dismissed, vb.)
- Metodlar: forward(), reverse(), repeat(), reset(), stop()
- Mixin: SingleTickerProviderStateMixin (1 animasyon) veya TickerProviderStateMixin (birden fazla)
- dispose(): Hafıza sızıntısını önlemek için zorunlu
- Performans: const child, AnimatedBuilder kullanın
Explicit animations, Flutter'da animasyon konusunda size tam kontrol verir. Öğrenme eğrisi biraz daha dik olsa da, yaratacağınız etkileyici animasyonlar buna değer!