Flutter
Flutter Animasyon Sistemi: Implicit, Explicit ve Custom Animasyonlar
Animasyonlar, işlevsel bir uygulamayı canlı hissettiren bir uygulamadan ayıran en belirleyici faktördür. Flutter, mobil geliştirme dünyasındaki en güçlü animasyon sistemlerinden birini sunar; ancak katmanlı mimarisi, nereden başlayacağınızı bilmezseniz bunaltıcı olabilir. Neredeyse her ekranında koreografik hareket bulunan uygulamalar geliştirdikten sonra, her seferinde doğru animasyon yaklaşımını seçmemi sağlayan net bir zihinsel model oluşturdum.
Animasyon Katmanları
Flutter'ın animasyon sistemi katmanlar halinde inşa edilmiştir. En altta, her vsync sinyalinde (genellikle saniyede 60 veya 120 kez) tetiklenen Ticker bulunur. Üstünde zamanlama ve değer interpolasyonunu yöneten AnimationController yer alır. Bir üst katmanda Tween nesneleri aralıkları somut değerlere eşler. En üstte ise tüm bu karmaşıklığı tek satıra sığdıran implicit animasyon widget'ları bulunur.
Bu katmanları anlamak, pratik çözüm yetersiz kaldığında nereye uzanacağınızı her zaman bilmeniz demektir.
Implicit Animasyonlar: Kolay Kazanım
Implicit animasyonlar, Flutter'ın seremoni gerektirmeden hareket isteyen geliştiricilere sunduğu bir armağandır. Hedef durumu tanımlarsınız, framework geçişi sizin için halleder.
AnimatedContainer
Implicit animasyonların iş atı. Decoration, boyut, padding, margin, alignment ve transform değişikliklerinin tümü arasında interpolasyon yapar.
class ExpandableCard extends StatefulWidget {
const ExpandableCard({super.key});
@override
State<ExpandableCard> createState() => _ExpandableCardState();
}
class _ExpandableCardState extends State<ExpandableCard> {
bool _expanded = false;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => setState(() => _expanded = !_expanded),
child: AnimatedContainer(
duration: const Duration(milliseconds: 400),
curve: Curves.easeInOutCubic,
width: _expanded ? 300 : 150,
height: _expanded ? 200 : 80,
decoration: BoxDecoration(
color: _expanded ? Colors.indigo : Colors.indigo.shade200,
borderRadius: BorderRadius.circular(_expanded ? 24 : 12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(_expanded ? 0.3 : 0.1),
blurRadius: _expanded ? 20 : 8,
offset: Offset(0, _expanded ? 10 : 4),
),
],
),
child: Center(
child: Text(
_expanded ? 'Genişletildi' : 'Dokun',
style: const TextStyle(color: Colors.white, fontSize: 18),
),
),
),
);
}
}Bilmeniz Gereken Diğer Implicit Widget'lar
Her biri aynı kalıbı izler: bir özelliği değiştirin, widget yeni değere doğru animasyon yapar.
// Bir widget'ı gizleyip gösterme
AnimatedOpacity(
opacity: _visible ? 1.0 : 0.0,
duration: const Duration(milliseconds: 300),
child: const Text('Şimdi beni görüyorsun'),
)
// Bir widget'ı yeni konumuna kaydırma
AnimatedPositioned(
duration: const Duration(milliseconds: 500),
curve: Curves.elasticOut,
left: _moved ? 200 : 50,
top: _moved ? 100 : 50,
child: const CircleAvatar(radius: 30),
)
// İki widget arasında yumuşak geçiş
AnimatedSwitcher(
duration: const Duration(milliseconds: 400),
transitionBuilder: (child, animation) => ScaleTransition(
scale: animation,
child: child,
),
child: Text(
'$_count',
key: ValueKey<int>(_count),
style: const TextStyle(fontSize: 48),
),
)Yoğun animasyon içeren uygulamalarda öğrendiğim en önemli şey, implicit animasyonların ilk refleksiniz olması gerektiğidir. Tipik bir uygulamada ihtiyacınız olan hareketin yaklaşık %70'ini bir AnimationController'a dokunmadan gerçekleştirebilirsiniz.
Explicit Animasyonlar: Tam Kontrol
Hassas zamanlama, sıralama veya bir animasyonu oynatma, duraklatma, geri alma, tekrarlama yeteneğine ihtiyaç duyduğunuzda explicit animasyon alanına geçersiniz.
Temel Üçlü: AnimationController, Tween, CurvedAnimation
class PulsingButton extends StatefulWidget {
const PulsingButton({super.key});
@override
State<PulsingButton> createState() => _PulsingButtonState();
}
class _PulsingButtonState extends State<PulsingButton>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
late final Animation<double> _scaleAnimation;
late final Animation<Color?> _colorAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 1200),
vsync: this,
)..repeat(reverse: true);
final curvedAnimation = CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
);
_scaleAnimation = Tween<double>(begin: 1.0, end: 1.15).animate(curvedAnimation);
_colorAnimation = ColorTween(
begin: Colors.blue,
end: Colors.purple,
).animate(curvedAnimation);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: _colorAnimation.value,
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
),
onPressed: () {},
child: child,
),
);
},
child: const Text('Abone Ol', style: TextStyle(fontSize: 18)),
);
}
}Unutmamanız gerekenler: her zaman SingleTickerProviderStateMixin kullanın (birden fazla controller için TickerProviderStateMixin), controller'larınızı mutlaka dispose edin ve mümkün olduğunca AnimatedBuilder'a statik bir child geçirin ki alt ağaç her karede yeniden oluşturulmasın.
TweenAnimationBuilder: Orta Yol
Tween tabanlı kontrol istediğiniz ama AnimationController yönetmek istemediğiniz durumlarda TweenAnimationBuilder imdadınıza yetişir.
TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: _targetAngle),
duration: const Duration(milliseconds: 800),
curve: Curves.easeOutBack,
builder: (context, angle, child) {
return Transform.rotate(
angle: angle,
child: child,
);
},
child: const Icon(Icons.refresh, size: 48),
)Staggered Animasyonlar: Koreografik Hareket
Staggered animasyonlar, tek bir AnimationController ile birden fazla Interval tabanlı eğri kullanır. Her öğe zaman çizelgesinin farklı bir noktasında başlar ve kademeli bir efekt oluşturur.
class StaggeredListAnimation extends StatefulWidget {
const StaggeredListAnimation({super.key});
@override
State<StaggeredListAnimation> createState() => _StaggeredListAnimationState();
}
class _StaggeredListAnimationState extends State<StaggeredListAnimation>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
final int _itemCount = 5;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 1500),
vsync: this,
)..forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
Animation<double> _buildSlideAnimation(int index) {
final start = index * 0.1;
final end = start + 0.4;
return Tween<double>(begin: 80.0, end: 0.0).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(start, end.clamp(0.0, 1.0), curve: Curves.easeOutCubic),
),
);
}
Animation<double> _buildFadeAnimation(int index) {
final start = index * 0.1;
final end = start + 0.4;
return Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(start, end.clamp(0.0, 1.0), curve: Curves.easeOut),
),
);
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, _) {
return Column(
children: List.generate(_itemCount, (index) {
final slide = _buildSlideAnimation(index);
final fade = _buildFadeAnimation(index);
return Opacity(
opacity: fade.value,
child: Transform.translate(
offset: Offset(slide.value, 0),
child: ListTile(
leading: CircleAvatar(child: Text('${index + 1}')),
title: Text('Öğe ${index + 1}'),
),
),
);
}),
);
},
);
}
}Buradaki püf noktası, tüm interval'leri 0.0 ile 1.0 aralığında tutmak ve hareketin ardışık değil sürekli hissettirmesi için hafifçe örtüştürmektir.
CustomPainter ile Özel Animasyonlar
Widget'lar ihtiyacınızı karşılayamadığında, canvas seviyesine inersiniz. CustomPainter, her şeyi çizmenize ve kare kare animasyon yapmanıza olanak tanır.
class WaveAnimation extends StatefulWidget {
const WaveAnimation({super.key});
@override
State<WaveAnimation> createState() => _WaveAnimationState();
}
class _WaveAnimationState extends State<WaveAnimation>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 3),
vsync: this,
)..repeat();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, _) {
return CustomPaint(
size: const Size(double.infinity, 200),
painter: WavePainter(progress: _controller.value),
);
},
);
}
}
class WavePainter extends CustomPainter {
final double progress;
WavePainter({required this.progress});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.blue.withOpacity(0.6)
..style = PaintingStyle.fill;
final path = Path()..moveTo(0, size.height * 0.5);
for (double x = 0; x <= size.width; x++) {
final y = size.height * 0.5 +
sin((x / size.width * 2 * pi) + (progress * 2 * pi)) * 30 +
sin((x / size.width * 4 * pi) + (progress * 4 * pi)) * 15;
path.lineTo(x, y);
}
path.lineTo(size.width, size.height);
path.lineTo(0, size.height);
path.close();
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(WavePainter oldDelegate) {
return oldDelegate.progress != progress;
}
}shouldRepaint metodu kritik önem taşır. Sadece paint metodunuzun kullandığı veri gerçekten değiştiğinde true döndürün. Koşulsuz olarak true döndürmek, hiçbir şey değişmemiş olsa bile her karede yeniden çizimi zorlar ve GPU süresini boşa harcar.
Hero Animasyonları ve Sayfa Geçişleri
Hero animasyonları, navigasyon sırasında paylaşılan bir öğeyi dönüştürerek ekranlar arasında görsel süreklilik yaratır.
// Kaynak ekran
GestureDetector(
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => DetailScreen(item: item)),
),
child: Hero(
tag: 'item-${item.id}',
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.network(item.imageUrl, width: 100, height: 100, fit: BoxFit.cover),
),
),
)
// Hedef ekran
Hero(
tag: 'item-${item.id}',
child: ClipRRect(
borderRadius: BorderRadius.circular(0),
child: Image.network(item.imageUrl, width: double.infinity, height: 300, fit: BoxFit.cover),
),
)Özel Sayfa Geçişleri
Sayfaların nasıl girip çıktığı üzerinde daha fazla kontrol istediğinizde, kendi PageRouteBuilder'ınızı oluşturursunuz.
Route createFadeScaleRoute(Widget page) {
return PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) => page,
transitionDuration: const Duration(milliseconds: 500),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
final curved = CurvedAnimation(parent: animation, curve: Curves.easeInOutCubic);
return FadeTransition(
opacity: curved,
child: ScaleTransition(
scale: Tween<double>(begin: 0.92, end: 1.0).animate(curved),
child: child,
),
);
},
);
}
// Kullanım
Navigator.push(context, createFadeScaleRoute(const DetailScreen()));Animasyon Performans İpuçları
1. RepaintBoundary Kullanın
Ekranın sadece bir bölümü animasyon yapıyorsa, onu RepaintBoundary ile sarın. Bu, motora o alt ağacı kendi katmanında birleştirmesini söyler ve UI'ın geri kalanının yeniden çizilmesini engeller.
RepaintBoundary(
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform.rotate(
angle: _controller.value * 2 * pi,
child: child,
);
},
child: const Icon(Icons.sync, size: 64),
),
)2. shouldRepaint'i Doğru Uygulayın
CustomPainter'da yanlış bir shouldRepaint, sessiz bir performans katilidir. Yalnızca paint metodunuzun kullandığı alanları karşılaştırın.
@override
bool shouldRepaint(WavePainter oldDelegate) {
return oldDelegate.progress != progress;
// Her karede değiştiğinden emin olmadıkça her zaman true döndürmeyin
}3. Layout Değişiklikleri Yerine Transform Tercih Edin
Transform.translate, Transform.scale ve Transform.rotate birleştirme katmanında çalışır. Layout yeniden hesaplamasını tetiklemezler. Buna karşılık, width, height veya padding animasyonu tam bir layout geçişine neden olur.
4. AnimatedBuilder'da child Parametresini Kullanın
Animasyon sırasında değişmeyen her şey child parametresi olarak geçirilmelidir. Bu alt ağaç bir kez oluşturulur ve her karede yeniden kullanılır.
5. Mümkünse Opacity'den Kaçının
Opacity widget'ı bir offscreen buffer oluşturur. Basit fade-in'ler için explicit animasyonlu FadeTransition'ı tercih edin veya dahili olarak bunu optimize eden AnimatedOpacity kullanın.
Hangi Animasyon Türünü Ne Zaman Kullanmalı?
| Senaryo | Önerilen Yaklaşım |
|---|---|
| Dokunmayla özellik değişimi (renk, boyut, kenarlık) | AnimatedContainer veya diğer implicit widget |
| Widget'ı gizleme/gösterme | AnimatedOpacity, AnimatedSlide |
| İki widget arasında geçiş | AnimatedSwitcher |
| Döngüsel veya tekrarlayan animasyon | AnimationController.repeat() ile explicit |
| Hassas oynat/duraklat/geri al kontrolü | AnimationController ile explicit |
| Sıralı giriş yapan birden fazla öğe | Interval ile staggered animasyon |
| Şekil, grafik, parçacık çizimi | CustomPainter + AnimationController |
| Ekranlar arası paylaşılan öğe | Hero widget'ı |
| Özel sayfa giriş/çıkışı | PageRouteBuilder |
| Controller boilerplate'i olmadan basit tween | TweenAnimationBuilder |
Emin değilseniz implicit animasyonla başlayın. Widget yapınızı bozmadan daha sonra istediğiniz zaman explicit kontrole geçebilirsiniz.
Sık Yapılan Hatalar ve Tuzaklar
1. Controller'ları Dispose Etmeyi Unutmak
Her AnimationController, dispose() içinde temizlenmelidir. Sızdırılan bir controller, Ticker'ını canlı tutar ve artık var olmayan bir widget üzerinde callback tetikler. Sonuç: korkulan "setState called after dispose" hatası.
2. Yanlış Mixin Kullanmak
SingleTickerProviderStateMixin tek bir controller'ı destekler. İki veya daha fazla controller'a ihtiyacınız varsa TickerProviderStateMixin'e geçin. Tek sürümü birden fazla controller ile kullanmak, hemen fark edilmeyen bir runtime hatasına neden olur.
3. Tüm Alt Ağacı Yeniden Oluşturmak
Scaffold'unuzun tamamını AnimatedBuilder ile sarmak, tüm ekranın saniyede 60 kez yeniden oluşturulması demektir. Animasyonlu bölümü mümkün olduğunca dar tutun.
4. Listelerde Layout Özelliklerini Animasyonlamak
ListView içindeki öğelerin yüksekliğini animasyonlamak, her karede görünür tüm öğeler için layout tetikler. Bunun yerine bunu verimli şekilde ele alan SizeTransition veya AnimatedList kullanın.
5. Azaltılmış Hareket Tercihlerini Göz Ardı Etmek
Bazı kullanıcıların sistem düzeyinde azaltılmış hareket ayarı etkindir. Buna saygı gösterin.
final reduceMotion = MediaQuery.of(context).disableAnimations;
final duration = reduceMotion
? Duration.zero
: const Duration(milliseconds: 400);6. Süreleri Her Yere Sabit Kodlamak
Animasyon sürelerini merkezi bir konumda sabit olarak tanımlayın. Uygulamanızın hissini ayarlamanız gerektiğinde, düzinelerce dosyayı taramak yerine tek bir değeri değiştirirsiniz.
abstract class AppAnimations {
static const fast = Duration(milliseconds: 200);
static const medium = Duration(milliseconds: 400);
static const slow = Duration(milliseconds: 800);
static const pageTransition = Duration(milliseconds: 500);
}Kişisel Deneyimlerim
Yoğun animasyon içeren uygulamalarda öğrendiğim en büyük verimlilik artışı, yeniden kullanılabilir küçük bir animasyon wrapper kütüphanesi oluşturmaktan gelir. Controller yaşam döngüsünü yöneten ve delay, duration, offset parametreleri kabul eden bir FadeSlideIn widget'ı, aynı SingleTickerProviderStateMixin boilerplate'ini düzinelerce kez yazmaktan kurtarır.
Ayrıca acı bir deneyimle öğrendim ki animasyon performans sorunları neredeyse hiçbir zaman üst düzey geliştirme cihazlarında ortaya çıkmaz. Kullanıcılarınızın sahip olabileceği en düşük özellikli cihazda her zaman test edin. Pixel 8'inizde 60fps çalışan bir animasyon, bütçe dostu bir telefonda 30fps'de takılabilir ve o noktada RepaintBoundary yerleşimi kritik hale gelir.
Son bir not: eğriler, süreden daha önemlidir. Curves.easeOutCubic ile 400ms'lik bir animasyon, Curves.linear ile 200ms'lik bir animasyondan daha hızlı hissettirir. Daha kısa sürelere yönelmeden önce eğrilerle deneyler yapmaya zaman ayırın.
Sonuç
Flutter'ın animasyon sistemi, katmanlarını anladığınızda son derece esnektir. Hızlı kazanımlar için implicit animasyonlarla başlayın, zamanlama hassasiyeti gerektiğinde explicit kontrole uzanın ve widget sistemi vizyonunuzu ifade edemediğinde CustomPainter'a inin. Tüm bunları disiplinli performans pratikleriyle birleştirdiğinizde, uygulamalarınız göründüğü kadar duyarlı hissedilecektir.
Flutter uygulamanız için cilalanmış bir animasyon sistemi tasarlamak istiyorsanız benimle iletişime geçin.
İlgili Makaleler
Flutter Nedir? Kapsamlı Başlangıç Rehberi
Flutter'ın ne olduğunu, nasıl çalıştığını ve neden modern ürün ekiplerinin tercih ettiğini öğrenin. Dart, widget yapısı ve çoklu platform geliştirme sürecini keşfedin.
Flutter Performans Optimizasyonu: Kapsamlı Rehber
Flutter uygulamanızın performansını sistematik olarak artırın. Rebuild optimizasyonu, bellek yönetimi, lazy loading ve profiling tekniklerini öğrenin.
Flutter Widget Lifecycle: initState'den dispose'a
Stateful widget yaşam döngüsünü adım adım öğrenin ve sık yapılan lifecycle hatalarını önleyin.
Flutter Projeniz mi Var?
iOS, Android ve web için yüksek performanslı Flutter uygulamaları geliştiriyorum.
İletişime Geç