Flutter Animasyon Sistemi: Implicit, Explicit ve Custom Animasyonlar
# Flutter Animasyon Sistemi: Derinlemesine Bir İnceleme
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: class="code-number">400),
curve: Curves.easeInOutCubic,
width: _expanded ? class="code-number">300 : class="code-number">150,
height: _expanded ? class="code-number">200 : class="code-number">80,
decoration: BoxDecoration(
color: _expanded ? Colors.indigo : Colors.indigo.shade200,
borderRadius: BorderRadius.circular(_expanded ? class="code-number">24 : class="code-number">12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(_expanded ? class="code-number">0.3 : class="code-number">0.1),
blurRadius: _expanded ? class="code-number">20 : class="code-number">8,
offset: Offset(class="code-number">0, _expanded ? class="code-number">10 : class="code-number">4),
),
],
),
child: Center(
child: Text(
_expanded ? class="code-string">'Genişletildi' : class="code-string">'Dokun',
style: const TextStyle(color: Colors.white, fontSize: class="code-number">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.
class=class="code-string">"code-comment">// Bir widgetclass="code-string">'ı gizleyip gösterme
AnimatedOpacity(
opacity: _visible ? class="code-number">1.0 : class="code-number">0.0,
duration: const Duration(milliseconds: class="code-number">300),
child: const Text('Şimdi beni görüyorsunclass="code-string">'),
)
class=class="code-string">"code-comment">// Bir widget'ı yeni konumuna kaydırma
AnimatedPositioned(
duration: const Duration(milliseconds: class="code-number">500),
curve: Curves.elasticOut,
left: _moved ? class="code-number">200 : class="code-number">50,
top: _moved ? class="code-number">100 : class="code-number">50,
child: const CircleAvatar(radius: class="code-number">30),
)
class=class="code-string">"code-comment">// İki widget arasında yumuşak geçiş
AnimatedSwitcher(
duration: const Duration(milliseconds: class="code-number">400),
transitionBuilder: (child, animation) => ScaleTransition(
scale: animation,
child: child,
),
child: Text(
class="code-string">'$_count',
key: ValueKey<int>(_count),
style: const TextStyle(fontSize: class="code-number">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: class="code-number">1200),
vsync: this,
)..repeat(reverse: true);
final curvedAnimation = CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
);
_scaleAnimation = Tween<double>(begin: class="code-number">1.0, end: class="code-number">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: class="code-number">32, vertical: class="code-number">16),
),
onPressed: () {},
child: child,
),
);
},
child: const Text(class="code-string">'Abone Ol', style: TextStyle(fontSize: class="code-number">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: class="code-number">0.0, end: _targetAngle),
duration: const Duration(milliseconds: class="code-number">800),
curve: Curves.easeOutBack,
builder: (context, angle, child) {
return Transform.rotate(
angle: angle,
child: child,
);
},
child: const Icon(Icons.refresh, size: class="code-number">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 = class="code-number">5;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: class="code-number">1500),
vsync: this,
)..forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
Animation<double> _buildSlideAnimation(int index) {
final start = index * class="code-number">0.1;
final end = start + class="code-number">0.4;
return Tween<double>(begin: class="code-number">80.0, end: class="code-number">0.0).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(start, end.clamp(class="code-number">0.0, class="code-number">1.0), curve: Curves.easeOutCubic),
),
);
}
Animation<double> _buildFadeAnimation(int index) {
final start = index * class="code-number">0.1;
final end = start + class="code-number">0.4;
return Tween<double>(begin: class="code-number">0.0, end: class="code-number">1.0).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(start, end.clamp(class="code-number">0.0, class="code-number">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, class="code-number">0),
child: ListTile(
leading: CircleAvatar(child: Text(class="code-string">'${index + class="code-number">1}')),
title: Text(class="code-string">'Öğe ${index + class="code-number">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: class="code-number">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, class="code-number">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(class="code-number">0.6)
..style = PaintingStyle.fill;
final path = Path()..moveTo(class="code-number">0, size.height * class="code-number">0.5);
for (double x = class="code-number">0; x <= size.width; x++) {
final y = size.height * class="code-number">0.5 +
sin((x / size.width * class="code-number">2 * pi) + (progress * class="code-number">2 * pi)) * class="code-number">30 +
sin((x / size.width * class="code-number">4 * pi) + (progress * class="code-number">4 * pi)) * class="code-number">15;
path.lineTo(x, y);
}
path.lineTo(size.width, size.height);
path.lineTo(class="code-number">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.
class=class="code-string">"code-comment">// Kaynak ekran
GestureDetector(
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => DetailScreen(item: item)),
),
child: Hero(
tag: class="code-string">'item-${item.id}',
child: ClipRRect(
borderRadius: BorderRadius.circular(class="code-number">12),
child: Image.network(item.imageUrl, width: class="code-number">100, height: class="code-number">100, fit: BoxFit.cover),
),
),
)
class=class="code-string">"code-comment">// Hedef ekran
Hero(
tag: class="code-string">'item-${item.id}',
child: ClipRRect(
borderRadius: BorderRadius.circular(class="code-number">0),
child: Image.network(item.imageUrl, width: double.infinity, height: class="code-number">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: class="code-number">500),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
final curved = CurvedAnimation(parent: animation, curve: Curves.easeInOutCubic);
return FadeTransition(
opacity: curved,
child: ScaleTransition(
scale: Tween<double>(begin: class="code-number">0.92, end: class="code-number">1.0).animate(curved),
child: child,
),
);
},
);
}
class=class="code-string">"code-comment">// 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 * class="code-number">2 * pi,
child: child,
);
},
child: const Icon(Icons.sync, size: class="code-number">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;
class=class="code-string">"code-comment">// 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: class="code-number">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: class="code-number">200);
static const medium = Duration(milliseconds: class="code-number">400);
static const slow = Duration(milliseconds: class="code-number">800);
static const pageTransition = Duration(milliseconds: class="code-number">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ç