feat: çift tıklama fill/fit toggle + ok tuşları ile gezinme
Build Windows / build-windows (push) Has been cancelled

This commit is contained in:
Alhan committed 2026-07-22 05:09:26 +03:00
1 parent f267442fdb
commit 31b1032661
5 files changed
+96 -37

No files matched your search

Binary file not shown.
Binary file not shown.
+96 -37
View File
@@ -14,12 +14,30 @@ class ImageCanvas extends StatefulWidget {
class _ImageCanvasState extends State<ImageCanvas> { class _ImageCanvasState extends State<ImageCanvas> {
late TransformationController _controller; late TransformationController _controller;
double _currentScale = 1.0; double _currentScale = 1.0;
bool _isFilled = false; // false = fit, true = fill screen
Size? _imageSize;
Size? _viewportSize;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_controller = TransformationController(); _controller = TransformationController();
_controller.addListener(_onTransformChanged); _controller.addListener(_onTransformChanged);
_loadImageSize();
}
void _loadImageSize() {
final image = Image.file(File(widget.filePath));
image.image.resolve(const ImageConfiguration()).addListener(
ImageStreamListener((info, _) {
setState(() {
_imageSize = Size(
info.image.width.toDouble(),
info.image.height.toDouble(),
);
});
}),
);
} }
@override @override
@@ -27,22 +45,58 @@ class _ImageCanvasState extends State<ImageCanvas> {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
if (oldWidget.filePath != widget.filePath) { if (oldWidget.filePath != widget.filePath) {
_resetView(); _resetView();
_loadImageSize();
} }
} }
void _resetView() { void _resetView() {
_controller.value = Matrix4.identity(); _controller.value = Matrix4.identity();
_currentScale = 1.0; _currentScale = 1.0;
_isFilled = false;
} }
void _onTransformChanged() { void _onTransformChanged() {
_currentScale = _controller.value.getMaxScaleOnAxis(); _currentScale = _controller.value.getMaxScaleOnAxis();
} }
void _toggleFillFit() {
if (_imageSize == null || _viewportSize == null) return;
if (_isFilled) {
// Go to fit mode — reset transform
_controller.value = Matrix4.identity();
_currentScale = 1.0;
_isFilled = false;
} else {
// Go to fill mode — scale to cover viewport
final scaleX = _viewportSize!.width / _imageSize!.width;
final scaleY = _viewportSize!.height / _imageSize!.height;
final fillScale = scaleX > scaleY ? scaleX : scaleY;
// Center the image
final offsetX = (_viewportSize!.width - _imageSize!.width * fillScale) / 2;
final offsetY = (_viewportSize!.height - _imageSize!.height * fillScale) / 2;
_controller.value = Matrix4.identity()
// ignore: deprecated_member_use
..translate(offsetX, offsetY)
// ignore: deprecated_member_use
..scale(fillScale);
_currentScale = fillScale;
_isFilled = true;
}
}
void _handleDoubleTap() {
_toggleFillFit();
}
void _handleScroll(PointerScrollEvent event) { void _handleScroll(PointerScrollEvent event) {
final renderBox = context.findRenderObject() as RenderBox?; final renderBox = context.findRenderObject() as RenderBox?;
if (renderBox == null) return; if (renderBox == null) return;
_viewportSize = renderBox.size;
final localPosition = renderBox.globalToLocal(event.position); final localPosition = renderBox.globalToLocal(event.position);
final delta = event.scrollDelta.dy; final delta = event.scrollDelta.dy;
@@ -53,8 +107,6 @@ class _ImageCanvasState extends State<ImageCanvas> {
final actualScale = newScale / _currentScale; final actualScale = newScale / _currentScale;
// Decompose current matrix: widget_point = scale * child_point + translation
// So: child_point = (widget_point - translation) / scale
final matrix = _controller.value; final matrix = _controller.value;
final tx = matrix.getTranslation().x; final tx = matrix.getTranslation().x;
final ty = matrix.getTranslation().y; final ty = matrix.getTranslation().y;
@@ -62,7 +114,6 @@ class _ImageCanvasState extends State<ImageCanvas> {
final childX = (localPosition.dx - tx) / _currentScale; final childX = (localPosition.dx - tx) / _currentScale;
final childY = (localPosition.dy - ty) / _currentScale; final childY = (localPosition.dy - ty) / _currentScale;
// Build new matrix: zoom around child_point, then re-apply
final newMatrix = Matrix4.identity() final newMatrix = Matrix4.identity()
// ignore: deprecated_member_use // ignore: deprecated_member_use
..translate(childX, childY) ..translate(childX, childY)
@@ -84,42 +135,50 @@ class _ImageCanvasState extends State<ImageCanvas> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Listener( return LayoutBuilder(
onPointerSignal: (event) { builder: (context, constraints) {
if (event is PointerScrollEvent) { _viewportSize = Size(constraints.maxWidth, constraints.maxHeight);
_handleScroll(event); return Listener(
} onPointerSignal: (event) {
}, if (event is PointerScrollEvent) {
child: InteractiveViewer( _handleScroll(event);
transformationController: _controller, }
boundaryMargin: const EdgeInsets.all(double.infinity), },
minScale: 0.1, child: GestureDetector(
maxScale: 10.0, onDoubleTap: _handleDoubleTap,
panEnabled: true, child: InteractiveViewer(
scaleEnabled: false, transformationController: _controller,
child: Center( boundaryMargin: const EdgeInsets.all(double.infinity),
child: Image.file( minScale: 0.1,
File(widget.filePath), maxScale: 10.0,
fit: BoxFit.contain, panEnabled: true,
filterQuality: FilterQuality.high, scaleEnabled: false,
errorBuilder: (context, error, stackTrace) { child: Center(
return const Center( child: Image.file(
child: Column( File(widget.filePath),
mainAxisSize: MainAxisSize.min, fit: BoxFit.contain,
children: [ filterQuality: FilterQuality.high,
Icon(Icons.broken_image, size: 48, color: Colors.white38), errorBuilder: (context, error, stackTrace) {
SizedBox(height: 8), return const Center(
Text( child: Column(
'Resim yüklenemedi', mainAxisSize: MainAxisSize.min,
style: TextStyle(color: Colors.white38), children: [
), Icon(Icons.broken_image, size: 48, color: Colors.white38),
], SizedBox(height: 8),
Text(
'Resim yüklenemedi',
style: TextStyle(color: Colors.white38),
),
],
),
);
},
), ),
); ),
}, ),
), ),
), );
), },
); );
} }
} }
Binary file not shown.
Binary file not shown.