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

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

Binary file not shown.

Binary file not shown.

View File

@@ -14,12 +14,30 @@ class ImageCanvas extends StatefulWidget {
class _ImageCanvasState extends State<ImageCanvas> {
late TransformationController _controller;
double _currentScale = 1.0;
bool _isFilled = false; // false = fit, true = fill screen
Size? _imageSize;
Size? _viewportSize;
@override
void initState() {
super.initState();
_controller = TransformationController();
_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
@@ -27,22 +45,58 @@ class _ImageCanvasState extends State<ImageCanvas> {
super.didUpdateWidget(oldWidget);
if (oldWidget.filePath != widget.filePath) {
_resetView();
_loadImageSize();
}
}
void _resetView() {
_controller.value = Matrix4.identity();
_currentScale = 1.0;
_isFilled = false;
}
void _onTransformChanged() {
_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) {
final renderBox = context.findRenderObject() as RenderBox?;
if (renderBox == null) return;
_viewportSize = renderBox.size;
final localPosition = renderBox.globalToLocal(event.position);
final delta = event.scrollDelta.dy;
@@ -53,8 +107,6 @@ class _ImageCanvasState extends State<ImageCanvas> {
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 tx = matrix.getTranslation().x;
final ty = matrix.getTranslation().y;
@@ -62,7 +114,6 @@ class _ImageCanvasState extends State<ImageCanvas> {
final childX = (localPosition.dx - tx) / _currentScale;
final childY = (localPosition.dy - ty) / _currentScale;
// Build new matrix: zoom around child_point, then re-apply
final newMatrix = Matrix4.identity()
// ignore: deprecated_member_use
..translate(childX, childY)
@@ -84,42 +135,50 @@ class _ImageCanvasState extends State<ImageCanvas> {
@override
Widget build(BuildContext context) {
return Listener(
onPointerSignal: (event) {
if (event is PointerScrollEvent) {
_handleScroll(event);
}
},
child: InteractiveViewer(
transformationController: _controller,
boundaryMargin: const EdgeInsets.all(double.infinity),
minScale: 0.1,
maxScale: 10.0,
panEnabled: true,
scaleEnabled: false,
child: Center(
child: Image.file(
File(widget.filePath),
fit: BoxFit.contain,
filterQuality: FilterQuality.high,
errorBuilder: (context, error, stackTrace) {
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),
),
],
return LayoutBuilder(
builder: (context, constraints) {
_viewportSize = Size(constraints.maxWidth, constraints.maxHeight);
return Listener(
onPointerSignal: (event) {
if (event is PointerScrollEvent) {
_handleScroll(event);
}
},
child: GestureDetector(
onDoubleTap: _handleDoubleTap,
child: InteractiveViewer(
transformationController: _controller,
boundaryMargin: const EdgeInsets.all(double.infinity),
minScale: 0.1,
maxScale: 10.0,
panEnabled: true,
scaleEnabled: false,
child: Center(
child: Image.file(
File(widget.filePath),
fit: BoxFit.contain,
filterQuality: FilterQuality.high,
errorBuilder: (context, error, stackTrace) {
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),
),
],
),
);
},
),
);
},
),
),
),
),
),
);
},
);
}
}

Binary file not shown.

Binary file not shown.