Flutter Animasyon Sistemi: Implicit, Explicit ve Custom Animasyonlar

14 dakika okuma9 Mart 2026
Flutter animationsFlutter AnimationControllerFlutter implicit animationsFlutter explicit animationsFlutter CustomPainterFlutter Hero animationFlutter TweenFlutter animation performance

# 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.

dart
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.

dart
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

dart
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.

dart
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.

dart
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.

dart
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.

dart
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.

dart
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.

dart
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.

dart
@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.

dart
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.

dart
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 Projeniz mi Var?

iOS, Android ve web için yüksek performanslı Flutter uygulamaları geliştiriyorum.

İletişime Geç