Ahmet Balaman LogoAhmet Balaman

Flutter: Hero Animasyonları ve Sayfa Geçiş Efektleri ile Kullanıcı Deneyimini Zirveye Taşımak

personAhmet Balaman
calendar_today
FlutterAnimationHeroPage TransitionUI/UX

Hiç bir e-ticaret uygulamasında ürün görseline dokunduğunuzda, o küçük resmin detay sayfasında büyüyerek göründüğünü fark ettiniz mi? Ya da Instagram'da bir fotoğrafa tıkladığınızda nasıl akıcı bir şekilde tam ekran açıldığını? İşte bu sihir, Hero animasyonları ile mümkün.

Flutter'da animasyonlar, kullanıcı deneyimini sıradanlıktan sıra dışılığa taşıyan en güçlü araçlardan biri. Bu yazıda, uygulamalarınıza sinema kalitesinde geçişler kazandıracak teknikleri keşfedeceğiz.

Hero Animasyonu Nedir?

Hero animasyonu, iki sayfa arasında paylaşılan bir widget'ın akıcı bir şekilde geçiş yapmasını sağlar. Widget, ilk sayfadaki konumundan ikinci sayfadaki konumuna sorunsuzca "uçar".

Adını mitolojideki kahramanlardan alır - tıpkı kahramanlar gibi, widget de iki dünya (sayfa) arasında yolculuk yapar.

Neden Hero Kullanmalısınız?

Kullanıcılar sayfalar arası geçişlerde bağlam kaybı yaşar. Hero animasyonları bu sorunu çözer:

  • Görsel Süreklilik: Kullanıcı nerede olduğunu her zaman bilir
  • Profesyonel Görünüm: Uygulamanız sektör lideri gibi görünür
  • Düşük Efor, Yüksek Etki: Birkaç satır kod, muazzam sonuç

Hero'nun Anatomisi

Hero animasyonu için iki şey gerekir:

  1. Aynı tag: İki sayfadaki Hero widget'ları aynı tag değerine sahip olmalı
  2. Hero widget: Her iki sayfada da widget'ınızı Hero ile sarmalayın

İşte en basit örnek:

// İlk sayfa
Hero(
  tag: 'profile-photo',
  child: CircleAvatar(
    backgroundImage: NetworkImage('https://picsum.photos/200'),
    radius: 30,
  ),
)

// İkinci sayfa
Hero(
  tag: 'profile-photo', // Aynı tag!
  child: CircleAvatar(
    backgroundImage: NetworkImage('https://picsum.photos/200'),
    radius: 100, // Farklı boyut - Flutter otomatik animasyon yapar
  ),
)

Tag, animasyonun DNA'sı gibidir. Flutter, aynı tag'e sahip widget'ları bulur ve aralarında animasyon oluşturur.

Canlı Demo

Aşağıdaki interaktif örnekte bu widget'ları deneyebilirsiniz:

💡 Yukarıdaki örnek yüklenmiyorsa, yeni sekmede çalıştırmak için DartPad linkine tıklayın.

Gerçek Dünya Örneği: Ürün Kartından Detay Sayfasına

E-ticaret uygulamalarında en yaygın kullanım senaryosu:

class ProductCard extends StatelessWidget {
  final String productId;
  final String imageUrl;
  final String name;
  
  const ProductCard({
    required this.productId,
    required this.imageUrl,
    required this.name,
  });

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        Navigator.push(
          context,
          MaterialPageRoute(
            builder: (context) => ProductDetailPage(
              productId: productId,
              imageUrl: imageUrl,
              name: name,
            ),
          ),
        );
      },
      child: Card(
        child: Column(
          children: [
            Hero(
              tag: 'product-$productId', // Unique tag
              child: Image.network(
                imageUrl,
                height: 150,
                width: double.infinity,
                fit: BoxFit.cover,
              ),
            ),
            Padding(
              padding: EdgeInsets.all(8),
              child: Text(name, style: TextStyle(fontWeight: FontWeight.bold)),
            ),
          ],
        ),
      ),
    );
  }
}

class ProductDetailPage extends StatelessWidget {
  final String productId;
  final String imageUrl;
  final String name;
  
  const ProductDetailPage({
    required this.productId,
    required this.imageUrl,
    required this.name,
  });

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(name)),
      body: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Hero(
            tag: 'product-$productId', // Aynı tag!
            child: Image.network(
              imageUrl,
              height: 300,
              width: double.infinity,
              fit: BoxFit.cover,
            ),
          ),
          Padding(
            padding: EdgeInsets.all(16),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  name,
                  style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
                ),
                SizedBox(height: 16),
                Text(
                  'Ürün açıklaması buraya gelecek...',
                  style: TextStyle(fontSize: 16),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

Hero Animasyonunu Özelleştirmek

1. Animasyon Süresini Değiştirmek

Varsayılan süre 300ms. Bunu değiştirmek için transitionDuration kullanın:

Navigator.push(
  context,
  PageRouteBuilder(
    transitionDuration: Duration(milliseconds: 600), // Daha yavaş
    pageBuilder: (context, animation, secondaryAnimation) => DetailPage(),
  ),
);

2. Özel FlightShuttleBuilder

"Uçuş" sırasında widget'ın nasıl görüneceğini kontrol edin:

Hero(
  tag: 'custom-hero',
  flightShuttleBuilder: (flightContext, animation, direction, fromContext, toContext) {
    // Animasyon sırasında farklı bir widget göster
    return Material(
      color: Colors.transparent,
      child: ScaleTransition(
        scale: animation.drive(Tween(begin: 0.0, end: 1.0)),
        child: Icon(Icons.favorite, size: 100, color: Colors.red),
      ),
    );
  },
  child: Icon(Icons.favorite_border),
)

3. CreateRectTween ile Yolu Özelleştir

Widget'ın hangi yolu izleyeceğini belirleyin:

Hero(
  tag: 'curved-hero',
  createRectTween: (begin, end) {
    return MaterialRectArcTween(begin: begin, end: end); // Kavisli yol
  },
  child: Container(width: 100, height: 100, color: Colors.blue),
)

Sayfa Geçiş Animasyonları

Hero tek başına harika, ama sayfa geçişini de özelleştirirseniz? İşte burada PageRouteBuilder devreye giriyor.

SlideTransition - Kaydırarak Geçiş

Sayfa sağdan sola, soldan sağa, yukarıdan aşağıya kayabilir:

Navigator.push(
  context,
  PageRouteBuilder(
    pageBuilder: (context, animation, secondaryAnimation) => NextPage(),
    transitionsBuilder: (context, animation, secondaryAnimation, child) {
      const begin = Offset(1.0, 0.0); // Sağdan başla
      const end = Offset.zero; // Merkezde bitir
      const curve = Curves.easeInOut;
      
      var tween = Tween(begin: begin, end: end).chain(
        CurveTween(curve: curve),
      );
      
      var offsetAnimation = animation.drive(tween);
      
      return SlideTransition(
        position: offsetAnimation,
        child: child,
      );
    },
  ),
);

Yön Örnekleri:

// Sağdan sola
const begin = Offset(1.0, 0.0);

// Soldan sağa
const begin = Offset(-1.0, 0.0);

// Aşağıdan yukarı
const begin = Offset(0.0, 1.0);

// Yukarıdan aşağı
const begin = Offset(0.0, -1.0);

// Çapraz (sağ alt köşeden)
const begin = Offset(1.0, 1.0);

FadeTransition - Solma Geçişi

iOS tarzı yumuşak geçiş:

Navigator.push(
  context,
  PageRouteBuilder(
    pageBuilder: (context, animation, secondaryAnimation) => NextPage(),
    transitionsBuilder: (context, animation, secondaryAnimation, child) {
      return FadeTransition(
        opacity: animation,
        child: child,
      );
    },
  ),
);

ScaleTransition - Ölçekleme Geçişi

Sayfa küçükten büyüğe açılır:

Navigator.push(
  context,
  PageRouteBuilder(
    pageBuilder: (context, animation, secondaryAnimation, child) => NextPage(),
    transitionsBuilder: (context, animation, secondaryAnimation, child) {
      var scaleTween = Tween<double>(begin: 0.0, end: 1.0).chain(
        CurveTween(curve: Curves.elasticOut),
      );
      
      return ScaleTransition(
        scale: animation.drive(scaleTween),
        child: child,
      );
    },
  ),
);

RotationTransition - Döndürme Geçişi

Sayfa dönerek gelir:

Navigator.push(
  context,
  PageRouteBuilder(
    pageBuilder: (context, animation, secondaryAnimation) => NextPage(),
    transitionsBuilder: (context, animation, secondaryAnimation, child) {
      return RotationTransition(
        turns: animation,
        child: child,
      );
    },
  ),
);

Birden Fazla Animasyonu Birleştirmek

Gerçek sihir, animasyonları kombine ettiğinizde ortaya çıkar:

Navigator.push(
  context,
  PageRouteBuilder(
    pageBuilder: (context, animation, secondaryAnimation) => NextPage(),
    transitionsBuilder: (context, animation, secondaryAnimation, child) {
      // Hem fade hem slide
      return FadeTransition(
        opacity: animation,
        child: SlideTransition(
          position: Tween<Offset>(
            begin: Offset(0.0, 0.3),
            end: Offset.zero,
          ).animate(animation),
          child: child,
        ),
      );
    },
  ),
);

Custom Transition: "Zoom Fade Slide"

Profesyonel uygulamalarda görülen "zoom + fade + slide" kombinasyonu:

class ZoomFadeSlideTransition extends StatelessWidget {
  final Animation<double> animation;
  final Widget child;
  
  const ZoomFadeSlideTransition({
    required this.animation,
    required this.child,
  });

  @override
  Widget build(BuildContext context) {
    return FadeTransition(
      opacity: animation,
      child: SlideTransition(
        position: Tween<Offset>(
          begin: Offset(0, 0.1),
          end: Offset.zero,
        ).animate(CurvedAnimation(
          parent: animation,
          curve: Curves.easeOut,
        )),
        child: ScaleTransition(
          scale: Tween<double>(
            begin: 0.9,
            end: 1.0,
          ).animate(CurvedAnimation(
            parent: animation,
            curve: Curves.easeOut,
          )),
          child: child,
        ),
      ),
    );
  }
}

// Kullanımı:
Navigator.push(
  context,
  PageRouteBuilder(
    pageBuilder: (context, animation, secondaryAnimation) => NextPage(),
    transitionsBuilder: (context, animation, secondaryAnimation, child) {
      return ZoomFadeSlideTransition(
        animation: animation,
        child: child,
      );
    },
  ),
);

Curve'ler ile Animasyon Karakteri

Curve, animasyonun hızlanma/yavaşlama davranışını belirler:

// Doğrusal - Mekanik hissi verir
Curves.linear

// Yavaş başla, hızlan, yavaş bitir - En doğal
Curves.easeInOut

// Yumuşak başla
Curves.easeIn

// Yumuşak bitir
Curves.easeOut

// Elastik - Oyunculuk hissi
Curves.elasticOut

// Geri sekme - Dikkat çekici
Curves.bounceOut

// Hız kazanma
Curves.decelerate

// Aşırı gitme
Curves.anticipate

Pro İpucu: Kullanıcı arayüzleri için Curves.easeInOut veya Curves.easeOut kullanın. Curves.bounceOut gibi aşırı curve'ler dikkat çekici ama yorucu olabilir.

Performans İpuçları

1. Opacity Yerine AnimatedOpacity

// ❌ Kötü - Her frame'de rebuild
Opacity(
  opacity: _controller.value,
  child: HeavyWidget(),
)

// ✅ İyi - Optimize rebuild
AnimatedOpacity(
  opacity: isVisible ? 1.0 : 0.0,
  duration: Duration(milliseconds: 300),
  child: HeavyWidget(),
)

2. Ağır Widget'ları Cache'leyin

Navigator.push(
  context,
  PageRouteBuilder(
    pageBuilder: (context, animation, secondaryAnimation) => NextPage(),
    transitionsBuilder: (context, animation, secondaryAnimation, child) {
      return FadeTransition(
        opacity: animation,
        child: child, // child zaten cache'lenmiş
      );
    },
  ),
);

3. RepaintBoundary Kullanın

Animasyon sırasında sadece gerekli kısımlar yeniden çizilsin:

RepaintBoundary(
  child: Hero(
    tag: 'image',
    child: Image.network('...'),
  ),
)

Gerçek Dünya Kullanım Senaryoları

1. Fotoğraf Galerisi

GridView.builder(
  itemCount: photos.length,
  gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: 3,
    crossAxisSpacing: 4,
    mainAxisSpacing: 4,
  ),
  itemBuilder: (context, index) {
    return GestureDetector(
      onTap: () {
        Navigator.push(
          context,
          MaterialPageRoute(
            builder: (context) => PhotoViewPage(
              photoUrl: photos[index],
              tag: 'photo-$index',
            ),
          ),
        );
      },
      child: Hero(
        tag: 'photo-$index',
        child: Image.network(
          photos[index],
          fit: BoxFit.cover,
        ),
      ),
    );
  },
)

2. Profil Fotoğrafı Genişletme

// Liste öğesi
ListTile(
  leading: GestureDetector(
    onTap: () => Navigator.push(
      context,
      MaterialPageRoute(
        builder: (context) => ProfilePage(user: user),
      ),
    ),
    child: Hero(
      tag: 'avatar-${user.id}',
      child: CircleAvatar(
        backgroundImage: NetworkImage(user.photoUrl),
      ),
    ),
  ),
  title: Text(user.name),
)

// Profil sayfası
Scaffold(
  body: Column(
    children: [
      Hero(
        tag: 'avatar-${user.id}',
        child: CircleAvatar(
          backgroundImage: NetworkImage(user.photoUrl),
          radius: 80,
        ),
      ),
      // Diğer bilgiler...
    ],
  ),
)

3. FAB'den Detay Sayfasına

FloatingActionButton(
  heroTag: 'fab', // FAB için özel tag
  onPressed: () {
    Navigator.push(
      context,
      MaterialPageRoute(builder: (context) => CreatePostPage()),
    );
  },
  child: Hero(
    tag: 'create-icon',
    child: Icon(Icons.add),
  ),
)

Yaygın Hatalar ve Çözümleri

Hata 1: Duplicate Hero Tag

Problem: Aynı tag'i birden fazla Hero için kullanmak.

// ❌ Yanlış
ListView.builder(
  itemBuilder: (context, index) {
    return Hero(
      tag: 'item', // Hepsi aynı tag!
      child: ListTile(title: Text('Item $index')),
    );
  },
)

Çözüm: Unique tag kullanın.

// ✅ Doğru
ListView.builder(
  itemBuilder: (context, index) {
    return Hero(
      tag: 'item-$index', // Her öğe unique
      child: ListTile(title: Text('Item $index')),
    );
  },
)

Hata 2: Hero ve Material Widget Uyumsuzluğu

Problem: Hero içinde Material widget kullanırken tuhaf animasyonlar.

Çözüm: Her iki sayfada da Material kullanın veya hiç kullanmayın.

// İlk sayfa
Hero(
  tag: 'card',
  child: Material(
    child: Card(child: ...),
  ),
)

// İkinci sayfa
Hero(
  tag: 'card',
  child: Material( // Aynı Material sarmalama
    child: Card(child: ...),
  ),
)

Hata 3: Animasyon Çok Hızlı veya Yavaş

Çözüm: Optimal süre 200-400ms arasıdır.

// Çok hızlı: 100ms
// Optimal: 300ms
// Çok yavaş: 800ms+

PageRouteBuilder(
  transitionDuration: Duration(milliseconds: 300), // Ideal
  ...
)

Bonus: Reusable Transition Widget'ı

Tüm projenizde kullanabileceğiniz bir yardımcı sınıf:

class PageTransitionHelper {
  static Route createRoute(Widget page, {PageTransitionType type = PageTransitionType.fade}) {
    return PageRouteBuilder(
      pageBuilder: (context, animation, secondaryAnimation) => page,
      transitionsBuilder: (context, animation, secondaryAnimation, child) {
        switch (type) {
          case PageTransitionType.fade:
            return FadeTransition(opacity: animation, child: child);
          
          case PageTransitionType.slide:
            return SlideTransition(
              position: Tween<Offset>(
                begin: Offset(1.0, 0.0),
                end: Offset.zero,
              ).animate(animation),
              child: child,
            );
          
          case PageTransitionType.scale:
            return ScaleTransition(
              scale: Tween<double>(begin: 0.0, end: 1.0).animate(
                CurvedAnimation(parent: animation, curve: Curves.easeOut),
              ),
              child: child,
            );
          
          case PageTransitionType.rotation:
            return RotationTransition(turns: animation, child: child);
          
          default:
            return child;
        }
      },
    );
  }
}

enum PageTransitionType { fade, slide, scale, rotation }

// Kullanımı:
Navigator.push(
  context,
  PageTransitionHelper.createRoute(
    NextPage(),
    type: PageTransitionType.slide,
  ),
);

Özet

  • Hero Animasyonu: Widget'lar sayfalar arası "uçar" (aynı tag gerekli)
  • SlideTransition: Kaydırma efekti (Offset ile yön kontrolü)
  • FadeTransition: Solma efekti (opacity animasyonu)
  • ScaleTransition: Ölçekleme efekti (büyüme/küçülme)
  • RotationTransition: Dönme efekti
  • PageRouteBuilder: Özel geçişler oluşturmak için temel
  • Curves: Animasyon karakterini belirler (easeInOut, elasticOut, vb.)
  • Kombinasyon: Birden fazla geçiş birleştirilebilir
  • Performans: AnimatedOpacity, RepaintBoundary, cache kullanın
  • Unique Tags: Her Hero için farklı tag kullanın

Animasyonlar, kullanıcı deneyimini sihirleyen detaylardır. Abartmadan, yerinde kullanıldığında uygulamanızı profesyonel kılar. Şimdi sıra sizde - uygulamalarınıza hayat verin!

Yorumlar