Files
imajviewer/lib/widgets/image_canvas.dart
T
Alhan 4dae4234ba
Build Windows / build-windows (push) Has been cancelled
feat: letterbox-aware clamp, full-frame image, fit/fill zoom fix
- image_canvas.dart:
  - Fill mod: viewport tabanli clamp (_tx=0'da kilit)
  - Fit mod: RenderBox ile resmin gercek render boyutuna gore clamp
  - Letterbox offset (offX/offY) hesaba katilarak resmin kenarlarina kadar pan
  - Zoom-to-cursor dogru calisiyor (alignment: topLeft ile)
  - Min scale 1.0 (her iki modda da)
  - Overflow durumunda lo/hi reversed clamp (goruntu disari kacmaz)
- viewer_screen.dart: image alani top:24 -> top:0 (tam cerceve)
- install.sh: release -> debug bundle path (gelistirme)
2026-07-24 06:22:59 +03:00

175 lines
5.2 KiB
Dart

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<ImageCanvas> createState() => _ImageCanvasState();
}
class _ImageCanvasState extends State<ImageCanvas> {
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)),
],
),
);
}
}