Build Windows / build-windows (push) Has been cancelled
- 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)
175 lines
5.2 KiB
Dart
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)),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|