import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:photo_view/photo_view.dart'; enum _RemoteImageStatus { empty, thumbnail, full } class _RemotePhotoViewState extends State { late CachedNetworkImageProvider _imageProvider; _RemoteImageStatus _status = _RemoteImageStatus.empty; bool _zoomedIn = false; static const int swipeThreshold = 100; @override Widget build(BuildContext context) { bool allowMoving = _status == _RemoteImageStatus.full; return PhotoView( imageProvider: _imageProvider, minScale: PhotoViewComputedScale.contained, maxScale: allowMoving ? 1.0 : PhotoViewComputedScale.contained, enablePanAlways: true, scaleStateChangedCallback: _scaleStateChanged, onScaleEnd: _onScaleListener, ); } void _onScaleListener( BuildContext context, ScaleEndDetails details, PhotoViewControllerValue controllerValue, ) { // Disable swipe events when zoomed in if (_zoomedIn) return; if (controllerValue.position.dy > swipeThreshold) { widget.onSwipeDown(); } else if (controllerValue.position.dy < -swipeThreshold) { widget.onSwipeUp(); } } void _scaleStateChanged(PhotoViewScaleState state) { _zoomedIn = state == PhotoViewScaleState.zoomedIn; } CachedNetworkImageProvider _authorizedImageProvider(String url) { return CachedNetworkImageProvider( url, headers: {"Authorization": widget.authToken}, cacheKey: url, ); } void _performStateTransition( _RemoteImageStatus newStatus, CachedNetworkImageProvider provider, ) { // Transition to same status is forbidden if (_status == newStatus) return; // Transition full -> thumbnail is forbidden if (_status == _RemoteImageStatus.full && newStatus == _RemoteImageStatus.thumbnail) return; if (!mounted) return; setState(() { _status = newStatus; _imageProvider = provider; }); } void _loadImages() { CachedNetworkImageProvider thumbnailProvider = _authorizedImageProvider(widget.thumbnailUrl); _imageProvider = thumbnailProvider; thumbnailProvider.resolve(const ImageConfiguration()).addListener( ImageStreamListener((ImageInfo imageInfo, _) { _performStateTransition( _RemoteImageStatus.thumbnail, thumbnailProvider, ); }), ); CachedNetworkImageProvider fullProvider = _authorizedImageProvider(widget.imageUrl); fullProvider.resolve(const ImageConfiguration()).addListener( ImageStreamListener((ImageInfo imageInfo, _) { _performStateTransition(_RemoteImageStatus.full, fullProvider); }), ); } @override void initState() { _loadImages(); super.initState(); } } class RemotePhotoView extends StatefulWidget { const RemotePhotoView({ Key? key, required this.thumbnailUrl, required this.imageUrl, required this.authToken, required this.onSwipeDown, required this.onSwipeUp, }) : super(key: key); final String thumbnailUrl; final String imageUrl; final String authToken; final void Function() onSwipeDown; final void Function() onSwipeUp; @override State createState() { return _RemotePhotoViewState(); } }