diff --git a/lib/ui/viewer/file/zoomable_image.dart b/lib/ui/viewer/file/zoomable_image.dart index 6e53dabe5..79e17c471 100644 --- a/lib/ui/viewer/file/zoomable_image.dart +++ b/lib/ui/viewer/file/zoomable_image.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; @@ -13,6 +14,7 @@ import 'package:photos/events/local_photos_updated_event.dart'; import 'package:photos/models/file.dart'; import 'package:photos/ui/common/loading_widget.dart'; import 'package:photos/utils/file_util.dart'; +import 'package:photos/utils/image_util.dart'; import 'package:photos/utils/thumbnail_util.dart'; class ZoomableImage extends StatefulWidget { @@ -35,7 +37,7 @@ class ZoomableImage extends StatefulWidget { class _ZoomableImageState extends State with SingleTickerProviderStateMixin { - final Logger _logger = Logger("ZoomableImage"); + late Logger _logger; late File _photo; ImageProvider? _imageProvider; bool _loadedSmallThumbnail = false; @@ -45,10 +47,13 @@ class _ZoomableImageState extends State bool _loadedFinalImage = false; ValueChanged? _scaleStateChangedCallback; bool _isZooming = false; + PhotoViewController _photoViewController = PhotoViewController(); + int? _thumbnailWidth; @override void initState() { _photo = widget.photo; + _logger = Logger("ZoomableImage_" + _photo.displayName); debugPrint('initState for ${_photo.toString()}'); _scaleStateChangedCallback = (value) { if (widget.shouldDisableScroll != null) { @@ -61,6 +66,12 @@ class _ZoomableImageState extends State super.initState(); } + @override + void dispose() { + _photoViewController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { if (_photo.isRemoteFile) { @@ -75,6 +86,7 @@ class _ZoomableImageState extends State axis: Axis.vertical, child: PhotoView( imageProvider: _imageProvider, + controller: _photoViewController, scaleStateChangedCallback: _scaleStateChangedCallback, minScale: PhotoViewComputedScale.contained, gaplessPlayback: true, @@ -106,6 +118,7 @@ class _ZoomableImageState extends State if (cachedThumbnail != null) { _imageProvider = Image.memory(cachedThumbnail).image; _loadedSmallThumbnail = true; + _captureThumbnailDimensions(_imageProvider!); } else { getThumbnailFromServer(_photo).then((file) { final imageProvider = Image.memory(file).image; @@ -115,6 +128,7 @@ class _ZoomableImageState extends State setState(() { _imageProvider = imageProvider; _loadedSmallThumbnail = true; + _captureThumbnailDimensions(_imageProvider!); }); } }).catchError((e) { @@ -125,7 +139,8 @@ class _ZoomableImageState extends State }); } } - if (!_loadedFinalImage) { + if (!_loadedFinalImage && !_loadingFinalImage) { + _loadingFinalImage = true; getFileFromServer(_photo).then((file) { _onFinalImageLoaded( Image.file( @@ -209,16 +224,42 @@ class _ZoomableImageState extends State void _onFinalImageLoaded(ImageProvider imageProvider) { if (mounted) { - precacheImage(imageProvider, context).then((value) { + precacheImage(imageProvider, context).then((value) async { if (mounted) { + await _updatePhotoViewController(imageProvider); setState(() { _imageProvider = imageProvider; _loadedFinalImage = true; + _logger.info("Final image loaded"); }); } }); } } + Future _captureThumbnailDimensions(ImageProvider imageProvider) async { + final imageInfo = await getImageInfo(imageProvider); + _thumbnailWidth = imageInfo.image.width; + } + + Future _updatePhotoViewController(ImageProvider imageProvider) async { + if (_thumbnailWidth == null || _photoViewController.scale == null) { + return; + } + final imageInfo = await getImageInfo(imageProvider); + final scale = _photoViewController.scale! / + (imageInfo.image.width / _thumbnailWidth!); + final currentPosition = _photoViewController.value.position; + final positionScaleFactor = 1 / scale; + final newPosition = currentPosition.scale( + positionScaleFactor, + positionScaleFactor, + ); + _photoViewController = PhotoViewController( + initialPosition: newPosition, + initialScale: scale, + ); + } + bool _isGIF() => _photo.displayName.toLowerCase().endsWith(".gif"); } diff --git a/lib/utils/image_util.dart b/lib/utils/image_util.dart new file mode 100644 index 000000000..a5bcb03a7 --- /dev/null +++ b/lib/utils/image_util.dart @@ -0,0 +1,16 @@ +import 'dart:async'; + +import 'package:flutter/widgets.dart'; + +Future getImageInfo(ImageProvider imageProvider) { + final completer = Completer(); + final imageStream = imageProvider.resolve(const ImageConfiguration()); + final listener = ImageStreamListener( + ((imageInfo, _) { + completer.complete(imageInfo); + }), + ); + imageStream.addListener(listener); + completer.future.whenComplete(() => imageStream.removeListener(listener)); + return completer.future; +}