_toggleFillFit/_resetView icinde _vpSize = Size.zero, LayoutBuilder yeniden calismadiginda zoom'u olduruyor ve pan'i sinirsiz yapiyordu. _vpSize artik yalnizca constraints.biggest'ten beslenir.
8.7 KiB
Zoom / Pan / Clamp Mekanizması — Referans Doküman
Dosya: lib/widgets/image_canvas.dart
Son güncelleme: 23 Temmuz 2026
1. Mimari Karar: Neden InteractiveViewer Yok?
Başlangıçta InteractiveViewer kullanılıyordu. Ancak InteractiveViewer'ın built-in pan sınırlaması (boundaryMargin) çocuk widget'ın tam boyutuna göre çalışır. Fit modda Center kullanılınca, Center widget'ı viewport boyutunda olduğu halde içindeki Image daha küçüktür. IV, Image'ın değil Center'ın kenarlarına göre sınırlama yapar → Image'ın etrafında boşluk kalır.
Çözüm: InteractiveViewer tamamen kaldırıldı. Yerine:
ClipRect → GestureDetector → Listener → Transform → Center/SizedBox → Image
Transform: zoom/pan uygular (_tx,_ty,_sc)ClipRect: taşan kısımları kırparGestureDetector: pan (drag) algılarListener: scroll (tekerlek) algılar
2. Durum Değişkenleri
double _tx = 0; // viewport-space X kaydırma (piksel)
double _ty = 0; // viewport-space Y kaydırma (piksel)
double _sc = 1.0; // ölçek (1.0 = orijinal boyut)
bool _isFilled; // true=fill mod, false=fit mod
Size _vpSize; // LayoutBuilder'dan alınan viewport boyutu
Dönüşüm matrisi (Transform widget'ına verilen):
M = T(_tx, _ty) · S(_sc)
Yani: child-space'deki (cx, cy) noktası → viewport-space: vx = cx * _sc + _tx
3. Widget Ağacı (build metodu, satır 111–154)
LayoutBuilder
ClipRect
GestureDetector
onDoubleTap → _toggleFillFit()
onPanUpdate → _onPanUpdate()
Listener
onPointerSignal → _handleScroll()
Transform(translate(_tx,_ty) · scale(_sc))
_isFilled ?
SizedBox.expand(Image.file(fit: BoxFit.cover)) // FILL
: Center(Image.file(fit: BoxFit.contain)) // FIT
- Viewport boyutu (
_vpSize),LayoutBuilder'ınconstraints.biggest'inden okunur (satır 115). Her build'te güncellenir.context.findRenderObject()kullanılmaz çünkü LayoutBuilder'ın verdiği constraints daha güvenilirdir. _imageKey,Image.filewidget'ına bağlıdır. Sadece errorBuilder için kullanılır (clamping'te kullanılmaz).
4. Scroll Zoom Mekanizması (satır 50–71)
_handleScroll(PointerScrollEvent e)
// 1. İmleci viewport-space'den child-space'e çevir
cx = (vx - _tx) / _sc
cy = (vy - _ty) / _sc
// 2. Yeni ölçek (her tick'te %10)
factor = 1.1
newSc = (_sc * (tekerlek_yönüne_göre factor veya 1/factor)).clamp(0.1, 10.0)
// 3. Zoom'u imleç merkezli uygula
_tx = _tx + cx * _sc - cx * newSc
_ty = _ty + cy * _sc - cy * newSc
_sc = newSc
Formülün türetilmesi: Zoom öncesi imleç altındaki child-space noktası (cx, cy), viewport'ta (vx, vy) konumundadır. Zoom sonrası aynı noktanın yine (vx, vy)'de kalması için:
vx = _tx_old + cx * _sc_old (zoom öncesi)
vx = _tx_new + cx * _sc_new (zoom sonrası, aynı nokta)
→ _tx_new = vx - cx * _sc_new
→ _tx_new = (_tx_old + cx * _sc_old) - cx * _sc_new
→ _tx_new = _tx_old + cx * _sc_old - cx * _sc_new
5. Pan Mekanizması (satır 75–80)
_onPanUpdate(DragUpdateDetails d)
_tx += d.delta.dx; // her drag update'te delta biriktir
_ty += d.delta.dy;
if (_isFilled) _clampFill(_viewport());
setState(() {});
d.delta, son update'ten beri olan değişimi verir (toplam değil). Her update'te delta eklenir, clamp yapılır, setState ile yeniden çizilir.
6. Clamp Sistemi — SADECE Fill Mod (satır 84–107)
Fit modda hiçbir sınırlama yoktur. Kullanıcı özgürce pan/zoom yapabilir.
_clampFill(Size vp)
imgL = _tx → Image'ın viewport-space sol kenarı
imgR = _tx + vpw * _sc → Image'ın viewport-space sağ kenarı
vw = imgR - imgL → Image'ın viewport-space genişliği
Mantık: Fill modda SizedBox.expand(BoxFit.cover) kullanıldığı için Image her zaman viewport'u tam doldurur. Yani Image'ın child-space genişliği = vpw. Ölçeklenmiş genişliği = vpw * _sc.
Yatay sınırlama:
if (vw > vpw + 0.5) // Image viewport'tan geniş mi?
if (imgL > 0) // sol kenar viewport içinde → sola çek
_tx -= imgL
else if (imgR < vpw) // sağ kenar viewport içinde → sağa çek
_tx += vpw - imgR
Dikey sınırlama: Aynı mantık imgT/imgB ve vph ile.
Epsilon (0.5): Floating-point hatalarını önlemek için. Tam eşitlik durumunda (vw = vpw) hiçbir işlem yapılmaz.
7. Fill / Fit Geçişi (satır 39–45)
_toggleFillFit()
_isFilled = !_isFilled;
_tx = 0; _ty = 0; _sc = 1.0; // sıfırla
Çift tıklandığında:
- Mod değiştirilir
- Transform sıfırlanır (identity)
- Viewport cache'i sıfırlanır (sonraki build'te yeniden okunur)
Fill → Fit: Image Center içinde BoxFit.contain ile ortalanır, sınırlama yok. Fit → Fill: Image SizedBox.expand içinde BoxFit.cover ile doldurur, sınırlama aktif.
8. Viewport Boyutu (satır 46, 115)
Size _viewport() => _vpSize;
_vpSize değeri LayoutBuilder'ın builder callback'inde constraints.biggest olarak alınır (satır 115). Her build'te güncellenir. Neden context.findRenderObject() değil?
LayoutBuilder'ın constraints'i, widget'ın parent'tan aldığı en güncel boyut bilgisidircontext.findRenderObject()?.sizebazen null veya eski değer döndürebilir (henüz layout olmamışsa)constraints.biggesther zaman doğru ve günceldir
9. Görüntü Boyutu Nereden Alınıyor?
Fill mod: SizedBox.expand(BoxFit.cover) → Image viewport'u tam doldurur. Görüntü boyutu = viewport boyutu. Clamp'ta vp.width ve vp.height kullanılır.
Fit mod: Center(BoxFit.contain) → Image fitted boyutta çizilir. Clamp OLMADIĞI için görüntü boyutuna ihtiyaç yoktur.
Eski versiyonlarda _imageKey.currentContext?.findRenderObject() ile render box okunuyordu. Bunun iki sorunu vardı:
frameBuilderiçinde çağrılınca render object henüz oluşmamış olabiliyor → null veya 0 boyut dönüyorSizedBox.expandiçinde render box viewport boyutunu döndürüyor (fitted boyutu değil)
10. Özet: Hangi Fonksiyon Ne Yapıyor?
| Fonksiyon | Satır | Görevi |
|---|---|---|
_handleScroll() |
50 | Mouse tekerleği ile zoom (imleç merkezli) |
_onPanUpdate() |
75 | Sürükleme ile pan |
_clampFill() |
84 | Fill modda kenar sınırlaması |
_toggleFillFit() |
39 | Fill/Fit geçişi ve sıfırlama |
_resetView() |
33 | Yeni resim açıldığında sıfırlama |
_viewport() |
46 | Viewport boyutunu döndürür |
build() |
112 | Widget ağacını kurar, _vpSize'ı günceller |
11. Widget Tree (görsel)
ImageCanvas
└─ LayoutBuilder ─(constraints.biggest→_vpSize)─
└─ ClipRect
└─ GestureDetector (doubleTap, panUpdate)
└─ Listener (onPointerSignal→scroll)
└─ Transform [T(_tx,_ty)·S(_sc)]
└─ [_isFilled ? SizedBox.expand : Center]
└─ Image.file(key:_imageKey, fit:cover/contain)
12. Geçmiş / Alınan Dersler
-
InteractiveViewer çıkarıldı — boundaryMargin çocuk widget'ın tam boyutuna göre çalışıyor, Center içindeki Image'ın asıl boyutunu bilmiyor.
-
Center yerine SizedBox.expand denenmesi —
_baseContentRectile fitted boyut hesaplandı ama_intrinsicSizeasenkron yükleniyor, ilk frame'lerde yanlış boyut kullanılıyordu. -
Render box'tan boyut okuma —
frameBuilderiçinde render object henüz oluşmamış olabiliyor (build → layout sırası). Retry mekanizması eklendi ama güvenilir olmadı. -
Fit modda sınırlamanın tamamen kaldırılması — En temiz çözüm. Center widget'ı zaten layout seviyesinde ortalar. Zoom sonrası kullanıcı istediği gibi pan yapabilir.
-
translateByDouble / scaleByDouble hatası —
wparametresi1olmalıydı,0kullanılınca transform bozuldu. Eskitranslate/scaleAPI'sine geri dönüldü. -
_vpSize = Size.zero sıfırlaması (2026-08-03 düzeltildi) —
_resetView()ve_toggleFillFit()içinde_vpSize = Size.zerovardı. Çift tıklama (fill/fit toggle)setStateyerine yalnızca_markRenderDirty()çağırdığı için LayoutBuilder yeniden çalışmıyor,_vpSizesıfır kalıyordu →_handleScrollve_clamperken return edip zoom ölüyor, pan sınırsız oluyordu. Çözüm: metot içi sıfırlama kaldırıldı;_vpSizeyalnızca LayoutBuilder'ınconstraints.biggest'inden beslenir (alan tanımındaki ilk değer korunur). Linux'ta da mevcuttu, Windows testinde fark edildi.