↑↓ seç · Enter aç · Esc kapat

Mutlu Tekin
Mutlu Tekin
← Yazılar

Bölüm 8 — Konu 38: `CustomPainter` / `Canvas`

4 dk okuma #flutter
Dizi · 38/61 Flutter Türkçe Tutorial
  1. Bölüm 1 — Konu 1: Flutter Nedir, Mimarisi (Widget → Element → RenderObject, Skia/Impeller)
  2. Bölüm 1 — Konu 2: Ortam Kurulumu (Flutter SDK, Android Studio/VS Code, Emulator/Simulator, DevTools'a İlk Bakış)
  3. Bölüm 1 — Konu 3: İlk Proje Yapısı (`pubspec.yaml`, `lib/`, Klasör Mimarisi, `flutter create` Anatomisi)
  4. Bölüm 1 — Konu 4: Hot Reload / Hot Restart Ne Yapıyor, Neden Önemli
  5. Bölüm 2 — Konu 5: "Her Şey Widget'tır" Felsefesi, Widget Ağacı
  6. Bölüm 2 — Konu 6: StatelessWidget vs StatefulWidget + `setState()` Derinlemesine (Rebuild Mekanizması)
  7. Bölüm 2 — Konu 7: Temel Layout — Container, Row, Column, Stack, Padding, Align, Center
  8. Bölüm 2 — Konu 8: Constraint Sistemi — "Constraints Go Down, Sizes Go Up, Parent Sets Position"
  9. Bölüm 2 — Konu 9: `Expanded`, `Flexible`, `Spacer`, Intrinsic Widget'lar
  10. Bölüm 3 — Konu 10: MaterialApp/CupertinoApp, Scaffold, AppBar
  11. Bölüm 3 — Konu 11: `ListView`, `GridView`, `SingleChildScrollView` (+ Builder Pattern)
  12. Bölüm 3 — Konu 12: Text, TextStyle, Icon, Image, Asset Yönetimi
  13. Bölüm 3 — Konu 13: Custom Widget Yazma Prensipleri (Composition Over Inheritance)
  14. Bölüm 3 — Konu 14: Navigator 1.0 — Push/Pop, Named Routes
  15. Bölüm 4 — Konu 15: Form, TextField/TextFormField, GlobalKey<FormState>, Validasyon
  16. Bölüm 4 — Konu 16: Theme Sistemi (ThemeData, ColorScheme)
  17. Bölüm 4 — Konu 17: Responsive & Adaptive Tasarım (MediaQuery, LayoutBuilder, OrientationBuilder, Breakpoint Stratejileri)
  18. Bölüm 5 — Konu 18: Future/Async-Await Flutter Bağlamında, FutureBuilder
  19. Bölüm 5 — Konu 19: `Stream`, `StreamBuilder`
  20. Bölüm 5 — Konu 20: HTTP İstekleri (`http` / `dio` Paketleri)
  21. Bölüm 5 — Konu 21: JSON Serialization (Manuel → `json_serializable`/`freezed`)
  22. Bölüm 5 — Konu 22: Local Storage — `shared_preferences` → `Hive` → `sqflite`/Drift
  23. Bölüm 6 — Konu 23: InheritedWidget ve InheritedModel — "Neden setState Yetmiyor" Sorusunun Cevabı
  24. Bölüm 6 — Konu 24: Provider Paketi
  25. Bölüm 6 — Konu 25: Riverpod (Modern Yaklaşım, Provider'ın Halefi)
  26. Bölüm 6 — Konu 26: BLoC/Cubit Pattern (`flutter_bloc`)
  27. Bölüm 6 — Konu 27: GetX (Tartışmalı Ama Yaygın)
  28. Bölüm 6 — Konu 28: Karşılaştırma — Hangi Projede Hangisi?
  29. Bölüm 7 — Konu 29: Navigator 2.0 (Router, RouteInformationParser, RouterDelegate)
  30. Bölüm 7 — Konu 30: `go_router` Paketi (Pratik ve Modern Çözüm)
  31. Bölüm 7 — Konu 31: Deep Linking
  32. Bölüm 7 — Konu 32: Repository Pattern, Katmanlı Mimari (Data/Domain/Presentation)
  33. Bölüm 7 — Konu 33: Dependency Injection (`get_it`, `injectable`)
  34. Bölüm 7 — Konu 34: Clean Architecture Uyarlaması, SOLID Prensipleri
  35. Bölüm 8 — Konu 35: Implicit Animasyonlar
  36. Bölüm 8 — Konu 36: Explicit Animasyonlar (`AnimationController`, `Tween`, `Curve`)
  37. Bölüm 8 — Konu 37: Hero Animasyonları
  38. Bölüm 8 — Konu 38: `CustomPainter` / `Canvas`
  39. Bölüm 8 — Konu 39: Rive / Lottie Entegrasyonu
  40. Bölüm 9 — Konu 40: Platform Channels
  41. Bölüm 9 — Konu 41: Permission Yönetimi (`permission_handler`)
  42. Bölüm 9 — Konu 42: Kamera, Konum, Sensörler
  43. Bölüm 9 — Konu 43: Push Notification (Firebase Cloud Messaging)
  44. Bölüm 9 — Konu 44: Android/iOS Build Sistemleri
  45. Bölüm 10 — Konu 45: Test Yazımı (Unit, Widget, Integration, Golden)
  46. Bölüm 10 — Konu 46: CI/CD
  47. Bölüm 10 — Konu 47: Rebuild Optimizasyonu (`const`, `key` Kullanımı)
  48. Bölüm 10 — Konu 48: DevTools Profiling
  49. Bölüm 10 — Konu 49: Lazy Loading, Pagination, Büyük Liste Performansı
  50. Bölüm 11 — Konu 50: Firebase Ekosistemi (Auth, Firestore, Storage, Functions)
  51. Bölüm 11 — Konu 51: Supabase Alternatifi
  52. Bölüm 11 — Konu 52: GraphQL (Opsiyonel)
  53. Bölüm 11 — Konu 53: Store Yayınlama Süreci (İmzalama, Listing, Versiyonlama)
  54. Bölüm 11 — Konu 54: App Size, Obfuscation, Flavor Yönetimi
  55. Bölüm 12 — Konu 55: Flutter Web / Desktop
  56. Bölüm 12 — Konu 56: Custom `RenderObject` Yazımı
  57. Bölüm 12 — Konu 57: Engine Mimarisi Derinlemesine (Impeller vs Skia)
  58. Bölüm 12 — Konu 58: Plugin Geliştirme, pub.dev'e Paket Yayınlama
  59. Bölüm 12 — Konu 59: Monorepo Mimarisi (Melos)
  60. Bölüm 12 — Konu 60: Erişilebilirlik (Accessibility) Derinlemesine
  61. Bölüm 12 — Konu 61: Yerelleştirme (Localization / i18n)
Dizinin sayfası →
İçindekiler 9 başlık
  1. Neden CustomPainter? Hazır Widget'lar Yetmiyor mu?
  2. Widget → Element → RenderObject Hatırlatması
  3. Temel Yapı
  4. Dinamik Bir Örnek — Dairesel İlerleme Çubuğu
  5. AnimatedBuilder ile Birleştirme — Animasyonlu Bir Çizim
  6. CustomPaint'in child Parametresi
  7. Metin Çizmek — TextPainter
  8. 🎯 Bu Dersten Çıkarılması Gerekenler
  9. 📝 Ödevler

Şimdiye kadar hep hazır widget'ları (Container, Icon, Image) birleştirerek arayüz kurduk. Bu ders, Flutter'ın kendi çizim motoruna doğrudan erişmeni sağlıyor — bir dairesel ilerleme çubuğu, özel bir grafik, ya da hazır widget'larla yapamayacağın herhangi bir şekli çizebilmeni.

Neden CustomPainter? Hazır Widget'lar Yetmiyor mu?

Container, BoxDecoration, ClipPath gibi widget'larla çoğu basit şekli (dikdörtgen, daire, kenarlık) çizebilirsin. Ama şu durumlarda CustomPainter'a ihtiyaç duyarsın:

  • Veriye dayalı özel grafikler (bir çizgi grafiği, bir pasta grafiği, bir dalga formu).
  • Piksel seviyesinde kontrol gerektiren, hazır widget'larla ifade edilemeyen şekiller.
  • Performans kritik çizimler — yüzlerce küçük şekli tek tek widget olarak oluşturmak yerine, hepsini tek bir Canvas'a çizmek çok daha hızlıdır.

Widget → Element → RenderObject Hatırlatması

Bölüm 1 Konu 1'de öğrendiğimiz üç katmanlı mimariyi (Widget → Element → RenderObject) hatırlarsan — her widget, sonunda bir RenderObject'e dönüşür ve gerçek çizim, RenderObject.paint() metodunda, bir Canvas üzerinde gerçekleşir. CustomPainter, bu düşük seviye çizim katmanına, kendi RenderObject'ini yazmak zorunda kalmadan doğrudan erişmeni sağlayan bir kolaylık katmanıdır (Bölüm 12'de, RenderObject'i tamamen elle yazmayı göreceğiz — CustomPainter, o işin %90'ını basitleştirilmiş bir arayüzle sunar).

Temel Yapı

dart
class DaireCizici extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final boya = Paint()
      ..color = Colors.indigo
      ..style = PaintingStyle.fill;

    final merkez = Offset(size.width / 2, size.height / 2);
    final yaricap = size.width / 2;

    canvas.drawCircle(merkez, yaricap, boya);
  }

  @override
  bool shouldRepaint(covariant DaireCizici oldDelegate) {
    return false; // statik bir çizim, hiç yeniden çizilmesine gerek yok
  }
}
dart
CustomPaint(
  size: const Size(200, 200),
  painter: DaireCizici(),
)

Parça parça inceleyelim:

extends CustomPainter — Dart Bölüm 7'de öğrendiğimiz extends; CustomPainter, Flutter'ın soyut bir sınıfıdır (Bölüm 7'de öğrendiğimiz gibi, paint/shouldRepaint zorunlu override edilecek metodlardır).

paint(Canvas canvas, Size size) — bu metod, her çizim gerektiğinde çağrılır. canvas, üzerine çizim yapacağın tuvaldir; size, CustomPaint widget'ının sana ayırdığı alanın boyutudur (Bölüm 2 Konu 8'de öğrendiğimiz constraint sistemini hatırla — CustomPaint, kendisine verilen constraint'lere göre bir boyut alır, paint() bu boyutu size parametresiyle sana bildirir).

Paint nesnesi — Dart Bölüm 6'da öğrendiğimiz class + cascade operatörü (..) kombinasyonu; Paint, nasıl çizeceğini (renk, dolgu mu çizgi mi, çizgi kalınlığı) tanımlar — kalemi/fırçayı seçmek gibi düşün.

canvas.drawCircle(merkez, yaricap, boya) — asıl çizim komutu. Canvas sınıfı, drawCircle, drawRect, drawLine, drawPath, drawArc gibi düzinelerce çizim metodu sunar.

shouldRepaint(covariant DaireCizici oldDelegate) — Bölüm 6 Konu 23'te InheritedWidget'ın updateShouldNotify'ını öğrenmiştik; bu, kavramsal olarak birebir aynı fikir. false döndürürsen, Flutter bu çizimi bir kere yapar ve bir daha (veri değişmediği sürece) tekrar çizmez — performans için kritik. covariant anahtar kelimesi, Dart Bölüm 7'de değinmediğimiz ama burada karşına çıkan bir detay: üst sınıftaki (CustomPainter) parametre tipi CustomPainter'dır, ama biz burada daha spesifik bir tip (DaireCizici) kullanmak istiyoruz — covariant, Dart'a "bu daraltmayı bilerek yapıyorum, tip güvenliğini burada gevşet" demenin yoludur.

Dinamik Bir Örnek — Dairesel İlerleme Çubuğu

dart
class IlerlemeCizici extends CustomPainter {
  final double ilerleme; // 0.0 - 1.0 arası (Konu 36'daki Animation.value gibi düşünebilirsin)

  IlerlemeCizici({required this.ilerleme});

  @override
  void paint(Canvas canvas, Size size) {
    final merkez = Offset(size.width / 2, size.height / 2);
    final yaricap = size.width / 2;

    // Arka plan halkası (gri, tam daire)
    final arkaPlanBoyasi = Paint()
      ..color = Colors.grey[300]!
      ..style = PaintingStyle.stroke
      ..strokeWidth = 12;
    canvas.drawCircle(merkez, yaricap, arkaPlanBoyasi);

    // İlerleme yayı (mavi, sadece 'ilerleme' kadar)
    final ilerlemeBoyasi = Paint()
      ..color = Colors.indigo
      ..style = PaintingStyle.stroke
      ..strokeWidth = 12
      ..strokeCap = StrokeCap.round;

    final rect = Rect.fromCircle(center: merkez, radius: yaricap);
    const baslangicAcisi = -3.14159 / 2; // saat 12 yönünden başla
    final taramaAcisi = 2 * 3.14159 * ilerleme; // Dart Bölüm 3 — matematik operatörleri

    canvas.drawArc(rect, baslangicAcisi, taramaAcisi, false, ilerlemeBoyasi);
  }

  @override
  bool shouldRepaint(covariant IlerlemeCizici oldDelegate) {
    return oldDelegate.ilerleme != ilerleme; // sadece değer değiştiğinde yeniden çiz
  }
}

Dikkat et: ilerleme, Dart Bölüm 6'da öğrendiğimiz constructor parametresi olarak veriliyor — bu, CustomPainter'ı her ilerleme değeri için yeniden oluşturarak kullanacağın anlamına gelir (tıpkı bir widget'ı yeniden build() etmek gibi). shouldRepaint, eski ve yeni ilerleme'yi karşılaştırarak, gereksiz yeniden çizimleri önlüyor.

AnimatedBuilder ile Birleştirme — Animasyonlu Bir Çizim

Konu 36'da öğrendiğimiz AnimatedBuilder'ı, CustomPainter ile birleştirmek çok yaygın bir kalıptır:

dart
class DaireselIlerlemeCubugu extends StatefulWidget {
  const DaireselIlerlemeCubugu({super.key});

  @override
  State<DaireselIlerlemeCubugu> createState() => _DaireselIlerlemeCubuguState();
}

class _DaireselIlerlemeCubuguState extends State<DaireselIlerlemeCubugu>
    with SingleTickerProviderStateMixin {
  late final AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(seconds: 2),
      vsync: this,
    )..forward();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _controller,
      builder: (context, child) {
        return CustomPaint(
          size: const Size(150, 150),
          painter: IlerlemeCizici(ilerleme: _controller.value),
        );
      },
    );
  }
}

Burada, _controller.value (Konu 36'da öğrendiğimiz 0.0–1.0 arası ilerleme), her karede IlerlemeCizici'ye yeni bir ilerleme değeri olarak geçiliyor — bu da shouldRepaint'in true dönmesine ve ekranın her karede yeniden çizilmesine yol açıyor. Bu, tam olarak CustomPainter + AnimationController'ın birlikte çalışma deseni.

CustomPaint'in child Parametresi

dart
CustomPaint(
  size: const Size(200, 200),
  painter: DaireCizici(),
  child: const Center(child: Text('50%')), // çizimin ÜZERİNE bir widget koy
)

CustomPaint, sadece çizim yapmakla kalmaz — child: parametresiyle, çizimin üzerine normal widget'lar (metin, ikon) da koyabilirsin. Bu, örneğin bir dairesel ilerleme çubuğunun ortasına yüzde metni yazmak için çok kullanışlıdır.

Metin Çizmek — TextPainter

Canvas'a doğrudan Text widget'ı koyamazsın (widget değil, düşük seviye bir çizim yüzeyi) — metin çizmek için TextPainter kullanılır:

dart
void _metinCiz(Canvas canvas, Size size) {
  final textPainter = TextPainter(
    text: const TextSpan(
      text: 'Merhaba',
      style: TextStyle(color: Colors.black, fontSize: 16),
    ),
    textDirection: TextDirection.ltr,
  );
  textPainter.layout(); // önce boyutunu hesapla
  textPainter.paint(canvas, Offset(size.width / 2 - textPainter.width / 2, 10));
}

layout() çağrısı, Bölüm 2 Konu 8'de öğrendiğimiz "constraints go down, sizes go up" ilkesinin düşük seviyede bir yansımasıdır — metnin ne kadar yer kaplayacağını önce hesaplaman (layout()), sonra çizmen (paint()) gerekir.


🎯 Bu Dersten Çıkarılması Gerekenler

  • CustomPainter, RenderObject'in paint() katmanına doğrudan erişim sağlar — hazır widget'larla ifade edilemeyen özel çizimler için kullanılır.
  • paint(Canvas canvas, Size size), asıl çizimin yapıldığı metoddur; Paint nesnesi rengi/stili, Canvas metodları (drawCircle, drawArc, drawPath vb.) şekli belirler.
  • shouldRepaint, InheritedWidget'ın updateShouldNotify'ına benzer — gereksiz yeniden çizimleri önler.
  • CustomPainter + AnimationController + AnimatedBuilder, animasyonlu özel çizimlerin standart birleşimidir.
  • CustomPaint'in child: parametresi, çizimin üzerine normal widget koymanı sağlar.
  • Metin çizmek için Text widget'ı değil, TextPainter (layout() + paint()) kullanılır.

📝 Ödevler

  • [ ] CustomPainter ile basit bir üçgen çiz (Path + drawPath kullanarak — moveTo/lineTo/close metodlarını araştır).
  • [ ] Dairesel ilerleme çubuğu örneğini, AnimationController ile 0'dan 1'e animasyonlu şekilde dolduracak hale getir.
  • [ ] CustomPaint'in child: parametresiyle, ilerleme çubuğunun ortasına o anki yüzdeyi gösteren bir Text koy.
  • [ ] TextPainter kullanarak, bir Canvas üzerine ortalanmış bir başlık metni çiz.
  • [ ] Kendi cümlelerinle, "shouldRepaint'i her zaman true döndürseydik ne olurdu" sorusunu, performans açısından açıkla.

Sıradaki konu: Bölüm 8 — Konu 39: Rive / Lottie Entegrasyonu (Bölüm 8'in Son Konusu)