İçeriğe geç / Skip to content / Zum Inhalt

Flutter: GestureDetector Kullanımı ve Dokunma Olayları

Ahmet Balaman

Son güncelleme:

5 dk okuma

FlutterGestureDetectorInkWellHitTestBehaviorGestureTouch
Flutter: GestureDetector Kullanımı ve Dokunma Olayları

GestureDetector, kendi başına tıklanabilir olmayan herhangi bir widget'a dokunma olayları eklememizi sağlar: tek ve çift tıklama, uzun basma, sürükleme, iki parmakla ölçekleme. Ekranda hiçbir şey çizmez, yalnızca çocuğunun üzerindeki parmak hareketlerini dinler. Hazır butonların yetmediği yerde, örneğin bir resmi yakınlaştırırken ya da bir kartı sürüklerken, başvurduğumuz araç budur.

Canlı Demo

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

💡 Eğer yukarıdaki örnek açılmazsa, DartPad linkine tıklayarak yeni sekmede çalıştırabilirsiniz.

Temel Kullanım

Tıklanmasını istediğimiz widget'ı child olarak verip ilgili callback'i doldurmak yeterli:

GestureDetector(
  onTap: () {
    print('Tıklandı!');
  },
  child: Container(
    padding: const EdgeInsets.all(20),
    color: Colors.blue,
    child: const Text('Tıkla bana'),
  ),
)

Desteklenen Gesture Türleri

GestureDetector olayları: onTap, onDoubleTap, onLongPress, onHorizontalDrag, onVerticalDrag, onPan ve onScale

Tıklama Olayları

GestureDetector(
  onTap: () => print('Tek tıklama'),
  onDoubleTap: () => print('Çift tıklama'),
  onLongPress: () => print('Uzun basma'),
  onTapDown: (details) => print('Parmak değdi: ${details.localPosition}'),
  onTapUp: (details) => print('Parmak kalktı'),
  onTapCancel: () => print('Tıklama iptal'),
  child: MyWidget(),
)

onTapDown ve onTapUp, dokunulan noktanın koordinatını da verir; bir çizim alanında ya da özel bir kaydırıcıda "nereye dokunuldu" bilgisini buradan alırız. onTapCancel ise parmak basıldıktan sonra hareket bir sürüklemeye dönüştüğünde tetiklenir.

Sürükleme (Drag) Olayları

Sürükleme için üç aile vardır: yalnızca yatay, yalnızca dikey ve her yöne (pan). İhtiyacımız olan tek yönü dinlemek hem daha doğru sonuç verir hem de kaydırılabilir listelerle çakışmayı azaltır.

// Yatay sürükleme
GestureDetector(
  onHorizontalDragStart: (details) => print('Yatay sürükleme başladı'),
  onHorizontalDragUpdate: (details) => print('Delta: ${details.delta.dx}'),
  onHorizontalDragEnd: (details) => print('Hız: ${details.primaryVelocity}'),
  child: MyWidget(),
)

// Dikey sürükleme
GestureDetector(
  onVerticalDragUpdate: (details) => print('Delta: ${details.delta.dy}'),
  onVerticalDragEnd: (details) => print('Hız: ${details.primaryVelocity}'),
  child: MyWidget(),
)

// Her yöne sürükleme
GestureDetector(
  onPanStart: (details) => print('Pan başladı'),
  onPanUpdate: (details) => print('Pozisyon: ${details.localPosition}'),
  onPanEnd: (details) => print('Pan bitti'),
  child: MyWidget(),
)

onHorizontalDragEnd içindeki primaryVelocity, parmağın bırakıldığı andaki hızıdır. İşareti yönü söyler: pozitifse sağa, negatifse sola doğru bir savurma yapılmıştır.

Ölçekleme (Scale/Pinch) Olayları

GestureDetector(
  onScaleStart: (details) => print('Ölçekleme başladı'),
  onScaleUpdate: (details) {
    print('Ölçek: ${details.scale}');
    print('Rotasyon: ${details.rotation}');
  },
  onScaleEnd: (details) => print('Ölçekleme bitti'),
  child: MyWidget(),
)

Burada gözden kaçan bir ayrıntı var: details.scale her hareketin başında 1.0'dan başlar, yani o harekete görelidir. Kalıcı bir yakınlaştırma için başlangıç ölçeğini saklayıp çarpmak gerekir; aşağıdaki mini senaryoda bunu göreceğiz.

GestureDetector mı, InkWell mi?

İkisi de tıklamayı yakalar, fark geri bildirimdedir. GestureDetector hiçbir görsel tepki vermez. InkWell ise Material Design'ın dalga (ripple) efektini çizer ve kullanıcıya "dokunuşun algılandı" der:

InkWell(
  onTap: () => print('Tıklandı'),
  onLongPress: () => print('Uzun basıldı'),
  borderRadius: BorderRadius.circular(8),
  child: const Padding(
    padding: EdgeInsets.all(16),
    child: Text('Ripple Efektli Satır'),
  ),
)
GestureDetector InkWell
Görsel geri bildirim Yok Ripple ve vurgu
Sürükleme, ölçekleme Var Yok
Material atası gerekir mi? Hayır Evet
Tipik kullanım Resim, çizim alanı, özel etkileşim Liste satırı, kart, özel buton

Kural olarak: buton gibi davranan her şey için InkWell (ya da doğrudan hazır bir buton), tıklamadan fazlası gereken yerlerde GestureDetector. InkWell'in en yaygın kullanım yeri kartlardır; ayrıntısını Card yazısında anlattım.

HitTestBehavior

behavior parametresi, dokunmanın hangi alanda algılanacağını belirler. Varsayılan değer (çocuk varken) deferToChild'dır: yalnızca çocuğun gerçekten bir şey çizdiği noktalar dokunmayı yakalar.

GestureDetector(
  behavior: HitTestBehavior.opaque, // Boş alanlarda da algıla
  onTap: () => print('Tıklandı'),
  child: const SizedBox(
    width: 200,
    height: 200,
    child: Center(child: Text('Tıkla')),
  ),
)
  • deferToChild: Yalnızca çocuğun dolu kısımları dokunmayı alır. Rengi olmayan bir alanın boşluklarına dokunmak işe yaramaz.
  • opaque: Widget'ın tüm dikdörtgen alanı dokunmayı alır ve arkasındaki widget'lara geçirmez.
  • translucent: Tüm alan dokunmayı alır, ama arkadaki widget'lar da aynı dokunuşu görebilir.

Ne Zaman Kullanılır, Ne Zaman Kullanılmaz?

GestureDetector'ı hazır bir widget'ın karşılamadığı etkileşimlerde kullanırız: çift tıklamayla beğenme, bir görselin üzerinde dokunulan noktayı bulma, özel bir sürükleme davranışı, pinch ile ölçekleme.

Şu işler için ise Flutter'da daha uygun hazır çözümler var:

  • Buton davranışı: ElevatedButton, IconButton, InkWell. Bunlar geri bildirim, odak ve erişilebilirlik bilgilerini kendileri sağlar. Bir Container'ı GestureDetector ile butona çevirdiğimizde bunların hepsini kaybederiz.
  • Kaydırarak silme: Dismissible. Animasyonu ve eşik hesabını hazır verir.
  • Yakınlaştırma ve gezdirme: InteractiveViewer. Harita ya da büyük görsel gibi içerikler için sınır kontrolüyle birlikte gelir.
  • Sürükle-bırak: Draggable ve DragTarget.
  • İçerik kaydırma: ListView gibi kaydırılabilir widget'lar. Kaydırılabilir bir listenin içinde aynı yönde sürükleme dinlemek iki tarafı birbiriyle yarıştırır.

Sık Yapılan Hatalar

1. Boş alanın dokunmayı almaması

Belirti: Bir Row ya da rengi olmayan bir Container'ı GestureDetector ile sardık; yazının üstüne basınca çalışıyor, yazının yanındaki boşluğa basınca çalışmıyor. Sebep varsayılan deferToChild davranışı. Çözüm tek satır:

GestureDetector(
  behavior: HitTestBehavior.opaque,
  onTap: _openDetail,
  child: Row(
    children: const [Text('Başlık'), Spacer(), Icon(Icons.chevron_right)],
  ),
)

2. InkWell'de Material atasının olmaması ya da ripple'ın görünmemesi

İlk belirti kırmızı ekranda "No Material widget found" hatasıdır: InkWell dalgayı en yakın Material widget'ının üzerine çizer, bulamazsa hata verir. Scaffold, Card ve Dialog bu atayı zaten sağlar; bunların dışındaysak InkWell'i Material ile sarmak gerekir.

İkinci belirti daha sinsidir: hata yok ama ripple görünmüyor. Sebep genellikle InkWell'in çocuğunda renkli bir Container olmasıdır; dalga Material'a çizilir, opak renk de onu örter. Rengi çocuğa değil Material'a vermek sorunu çözer:

Material(
  color: Colors.orange,
  borderRadius: BorderRadius.circular(8),
  child: InkWell(
    borderRadius: BorderRadius.circular(8),
    onTap: () {},
    child: const Padding(
      padding: EdgeInsets.all(16),
      child: Text('Ripple görünür'),
    ),
  ),
)

3. onPan ve onScale'i birlikte kullanmak

Belirti: Debug modda "Incorrect GestureDetector arguments" hatası; açıklamasında scale'in pan'i zaten kapsadığı yazar. Hem gezdirme hem yakınlaştırma gerekiyorsa yalnızca onScale* callback'leri kullanılır; parmağın ne kadar kaydığı details.focalPointDelta ile okunur. Aynı şekilde yatay, dikey ve pan ailelerinin üçünü birden tanımlamak da aynı hatayı üretir.

4. onDoubleTap eklenince tek tıklamanın gecikmesi

onTap ve onDoubleTap birlikte tanımlandığında Flutter, ilk dokunuştan sonra ikincisinin gelip gelmeyeceğini görmek için kısa bir süre bekler. Bu yüzden tek tıklama hissedilir biçimde geç tepki verir. Çift tıklama gerçekten gerekli değilse eklememek, gerekliyse tek tıklamaya kritik bir işlev yüklememek en temiz çözümdür.

Mini Senaryo: Yakınlaştırılabilir Fotoğraf

Bir fotoğraf kartı düşünelim: çift tıklamayla beğeniliyor, iki parmakla yakınlaştırılıyor, uzun basınca eski boyutuna dönüyor.

class ZoomablePhoto extends StatefulWidget {
  const ZoomablePhoto({super.key, required this.image});

  final ImageProvider image;

  @override
  State<ZoomablePhoto> createState() => _ZoomablePhotoState();
}

class _ZoomablePhotoState extends State<ZoomablePhoto> {
  double _scale = 1.0;
  double _startScale = 1.0;
  bool _liked = false;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onDoubleTap: () => setState(() => _liked = !_liked),
      onLongPress: () => setState(() => _scale = 1.0),
      onScaleStart: (_) => _startScale = _scale,
      onScaleUpdate: (details) {
        setState(() {
          // details.scale bu harekete görelidir, o yüzden başlangıçla çarpıyoruz
          _scale = (_startScale * details.scale).clamp(1.0, 4.0);
        });
      },
      child: Stack(
        alignment: Alignment.topRight,
        children: [
          ClipRect(
            child: Transform.scale(
              scale: _scale,
              child: Image(image: widget.image, fit: BoxFit.cover),
            ),
          ),
          Padding(
            padding: const EdgeInsets.all(8),
            child: Icon(
              _liked ? Icons.favorite : Icons.favorite_border,
              color: Colors.white,
            ),
          ),
        ],
      ),
    );
  }
}

onScaleStart içinde o anki ölçeği saklıyoruz; saklamasaydık her yeni pinch hareketinde fotoğraf 1.0'a geri zıplardı. clamp ile ölçeği sınırlıyoruz, ClipRect de büyüyen görselin kartın dışına taşmasını engelliyor. Burada onTap tanımlamadığımız için çift tıklama gecikmesi sorun olmuyor. Yakınlaştırmanın bir anda değil yumuşak bir geçişle sıfırlanmasını isterseniz AnimatedContainer ve implicit animasyonlar yazısındaki yaklaşım buraya doğrudan uygulanabilir. Tam teşekküllü bir görsel gezdirme gerekiyorsa da kendi matematiğimizi yazmak yerine InteractiveViewer'a geçmek doğru karar olur.

Olay Özeti

Olay Açıklama
onTap Tek tıklama
onDoubleTap Çift tıklama
onLongPress Uzun basma
onHorizontalDrag* Yatay sürükleme
onVerticalDrag* Dikey sürükleme
onPan* Her yöne sürükleme
onScale* Pinch ile yakınlaştırma ve döndürme

Sık Sorulan Sorular

GestureDetector ile InkWell arasındaki fark nedir?

GestureDetector yalnızca hareketi algılar, görsel bir tepki vermez ve sürükleme ile ölçekleme gibi olayları da destekler. InkWell tıklama ve uzun basmayı Material ripple efektiyle birlikte sunar, ancak bir Material atasına ihtiyaç duyar.

GestureDetector neden boş alana tıklayınca çalışmıyor?

Varsayılan HitTestBehavior.deferToChild davranışında yalnızca çocuğun bir şey çizdiği noktalar dokunmayı alır. behavior parametresini HitTestBehavior.opaque yapmak widget'ın tüm alanını tıklanabilir hâle getirir.

onPan ve onScale birlikte kullanılabilir mi?

Hayır, Flutter bunu debug modda hata olarak bildirir çünkü scale hareketi pan'i zaten kapsar. Yalnızca onScale callback'lerini kullanıp kaydırma miktarını focalPointDelta üzerinden okumak gerekir.

Pinch ile yakınlaştırma için GestureDetector mı, InteractiveViewer mı?

Yakınlaştırma ve gezdirme standart bir ihtiyaçsa InteractiveViewer daha az kodla, sınır kontrolleriyle birlikte çalışır. Ölçeğe başka bir davranış bağlamak ya da hareketi tamamen özelleştirmek gerekiyorsa GestureDetector tercih edilir.

Yorumlar