import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/gestures.dart'; class ImageCanvas extends StatefulWidget { final String filePath; const ImageCanvas({super.key, required this.filePath}); @override State createState() => _ImageCanvasState(); } class _ImageCanvasState extends State { bool _isFilled = true; final GlobalKey _imageKey = GlobalKey(); double _tx = 0, _ty = 0, _sc = 1.0; Size _vpSize = Size.zero; void _clamp() { if (_vpSize.width <= 0 || _vpSize.height <= 0) return; if (_isFilled) { // Fill mode: viewport tabanli clamp final double w = _vpSize.width * _sc; final double h = _vpSize.height * _sc; if (w >= _vpSize.width) { if (_tx > 0) _tx = 0; else if (_tx + w < _vpSize.width) _tx = _vpSize.width - w; } else { if (_tx != 0) _tx = 0; } if (h >= _vpSize.height) { if (_ty > 0) _ty = 0; else if (_ty + h < _vpSize.height) _ty = _vpSize.height - h; } else { if (_ty != 0) _ty = 0; } return; } // Fit mode: resmin gercek boyutuna gore clamp (letterbox dahil) double m = 0, n = 0; double w = _vpSize.width * _sc; double h = _vpSize.height * _sc; final box = _imageKey.currentContext?.findRenderObject() as RenderBox?; if (box != null && box.hasSize && box.size.width > 0) { final Size img = box.size; m = (_vpSize.width - img.width) / 2; n = (_vpSize.height - img.height) / 2; w = img.width * _sc; h = img.height * _sc; } if (w >= _vpSize.width) { final double lo = -m * _sc; final double hi = _vpSize.width - m * _sc - w; if (lo <= hi) { if (_tx < lo) _tx = lo; if (_tx > hi) _tx = hi; } else { if (_tx < hi) _tx = hi; if (_tx > lo) _tx = lo; } } else { final double lo = -m * _sc; final double hi = _vpSize.width - m * _sc - w; if (_tx < lo) _tx = lo; if (_tx > hi) _tx = hi; } if (h >= _vpSize.height) { final double lo = -n * _sc; final double hi = _vpSize.height - n * _sc - h; if (lo <= hi) { if (_ty < lo) _ty = lo; if (_ty > hi) _ty = hi; } else { if (_ty < hi) _ty = hi; if (_ty > lo) _ty = lo; } } else { final double lo = -n * _sc; final double hi = _vpSize.height - n * _sc - h; if (_ty < lo) _ty = lo; if (_ty > hi) _ty = hi; } } @override void didUpdateWidget(ImageCanvas old) { super.didUpdateWidget(old); if (old.filePath != widget.filePath) _resetView(); } void _resetView() { setState(() { _tx = 0; _ty = 0; _sc = 1.0; _isFilled = true; _vpSize = Size.zero; }); } void _toggleFillFit() { setState(() { _isFilled = !_isFilled; _tx = 0; _ty = 0; _sc = 1.0; _vpSize = Size.zero; }); } void _handleDoubleTap() => _toggleFillFit(); void _handleScroll(PointerScrollEvent e) { if (_vpSize.width <= 0 || _vpSize.height <= 0) return; final double vx = e.localPosition.dx; final double vy = e.localPosition.dy; final double cx = (vx - _tx) / _sc; final double cy = (vy - _ty) / _sc; const double factor = 1.1; final double rawSc = _sc * (e.scrollDelta.dy < 0 ? factor : 1.0 / factor); final double newSc = rawSc.clamp(1.0, 10.0); _tx = _tx + cx * _sc - cx * newSc; _ty = _ty + cy * _sc - cy * newSc; _sc = newSc; _clamp(); setState(() {}); } void _onPanUpdate(DragUpdateDetails d) { _tx += d.delta.dx; _ty += d.delta.dy; _clamp(); setState(() {}); } Widget _buildImage() { return Image.file( key: _imageKey, File(widget.filePath), fit: _isFilled ? BoxFit.cover : BoxFit.contain, filterQuality: FilterQuality.high, errorBuilder: (_, _, _) => _errorWidget(), ); } @override Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { _vpSize = constraints.biggest; _clamp(); return SizedBox.expand( child: Stack( children: [ ClipRect( child: GestureDetector( onDoubleTap: _handleDoubleTap, onPanUpdate: _onPanUpdate, child: Listener( onPointerSignal: (event) { if (event is PointerScrollEvent) _handleScroll(event); }, child: Transform( alignment: Alignment.topLeft, transform: Matrix4.identity() ..translate(_tx, _ty) ..scale(_sc), child: _isFilled ? SizedBox.expand(child: _buildImage()) : Center(child: _buildImage()), ), ), ), ), ], ), ); }, ); } Widget _errorWidget() { return const Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.broken_image, size: 48, color: Colors.white38), SizedBox(height: 8), Text('Resim yüklenemedi', style: TextStyle(color: Colors.white38)), ], ), ); } }