photo_zoom 1.1.0
photo_zoom: ^1.1.0 copied to clipboard
Pan, zoom, rotate and swipe a photo gallery. Double tap and pinch zoom where you touch; mouse wheel on desktop. A type-safe, maintained alternative to photo_view.
photo_zoom #
A pan and zoom image viewer, and a gallery of them, that zooms where you touched rather than towards the middle.

import 'package:photo_zoom/photo_zoom.dart';
PhotoView(imageProvider: const AssetImage('assets/photo.jpg'))
Why this instead of what you already have #
Instead of InteractiveViewer. Flutter's own viewer pans and pinches, and
for a picture you only ever look at, it is enough. It has no double tap zoom at
all (onDoubleTap does not appear in its API), no fit / fill / actual-size
cycle, no swipe-between-photos gallery, and it clamps against the widget it
wraps rather than the image inside it, so an image letterboxed in its box can
still be dragged into the empty margin. Adding those back is the work this
package already did.
Instead of photo_view. The API is the same one, so the move is mostly a
change of import, and the differences are listed
below. The one in the recording is the reason to bother: on a double tap
photo_view runs animatePosition(controller.position, Offset.zero)
(photo_view_core.dart:282), which returns the image to basePosition and
zooms towards the centre whatever you tapped. Its issues #82, #394 and
#538 ask for the tapped point to stay put. Here it does, and the same
anchoring applies to a pinch and to a mouse wheel.
The recording is example/, running on a simulator. The tap is synthesised, so
it lands on the same pixel every time; the grid is numbered so that "the tile
under the marker did not change" is something you can check rather than take on
trust. cd example && flutter create . && flutter run --dart-define=start=compare
shows the same screen, and the buttons hand the viewer back to you.
Reach for it when #
- A photo, a map, a scan or a chart has detail worth magnifying, and the reader wants to zoom into a particular part of it.
- A gallery needs each photo to keep its own zoom while the pages swipe.
- The same screen ships to phone and desktop, and the wheel and trackpad should behave like they do everywhere else.
Skip it when the image is decorative, or when a fixed Image with BoxFit
already answers the question. This is a viewer, not a canvas: no drawing, no
annotation layers, no video.
Scale limits #
PhotoView(
imageProvider: const NetworkImage('https://example.com/map.png'),
minScale: PhotoViewComputedScale.contained * 0.8,
maxScale: PhotoViewComputedScale.covered * 3,
)
Gallery #
PhotoViewGallery.builder(
itemCount: photos.length,
onPageChanged: (index) => setState(() => _current = index),
builder: (context, index) => PhotoViewGalleryPageOptions(
imageProvider: NetworkImage(photos[index].url),
heroAttributes: PhotoViewHeroAttributes(tag: photos[index].id),
),
)
Each page keeps its own zoom. A drag pans the photo while it has room to move, and turns the page once the photo is against its edge, so panning a zoomed photo does not flip the page out from under it.
Swipe to dismiss #
Pass onDismiss and a vertical drag on the unzoomed image slides it and fades
the background; let go past dismissThreshold and it fires, usually to pop the
route the photo is on.
PhotoView(
imageProvider: NetworkImage(url),
onDismiss: () => Navigator.of(context).pop(),
// How far to drag to dismiss, as a fraction of viewport height. Default 0.2.
dismissThreshold: 0.2,
)
A shorter drag springs back to rest, and a drag while zoomed still pans. Without
onDismiss the gesture is off. PhotoViewGallery takes the same two, applied to
every page; a PhotoViewGalleryPageOptions can override them per page.
Parts #
| Class | Role |
|---|---|
PhotoView |
One zoomable image, or any widget via PhotoView.customChild |
PhotoViewGallery |
A PageView of them, from a list or built on demand |
PhotoViewController |
Reads and drives the transform; a ValueNotifier |
PhotoViewScaleStateController |
Reads and drives the double tap cycle |
PhotoViewScale |
PhotoViewScale.value(2), or a PhotoViewComputedScale |
PhotoViewHeroAttributes |
The Hero configuration for a view |
PhotoViewGestureDetectorScope |
Shares drags with a gesture-sensitive parent |
Driving it from code #
PhotoViewController is a ValueNotifier. Read it with a
ValueListenableBuilder and write to it directly:
final controller = PhotoViewController();
PhotoView(imageProvider: provider, controller: controller);
controller.scale = 2; // clamped into minScale..maxScale
controller.reset(); // back to the start
ValueListenableBuilder(
valueListenable: controller,
builder: (context, value, _) => Text('${value.scale}'),
);
Whoever creates a controller disposes it. A controller you do not pass is created and disposed by the view itself.
Desktop and web #
enableScrollZoom (on by default) wires up the mouse wheel and trackpad. Events
the view cannot act on are left alone rather than swallowed: a scroll-to-zoom-in
while already at maxScale, or a trackpad pan with nowhere left to pan, falls
through to an ancestor scrollable, so a photo in a scrolling page does not trap
the wheel.
Accessibility #
The current zoom is exposed to screen readers as a percentage of initialScale,
alongside semanticLabel, with increase and decrease actions that zoom in steps.
When the platform asks for reduced motion, zoom changes jump to their target
instead of animating.
Limits #
- The view fills the box it is given and needs a bounded one. In an unbounded
parent, pass
customSize. PhotoView.customChildtransforms a widget; it does not arbitrate with gestures inside that widget. A child with its own pan or tap handlers will fight the view. UsedisableGestures: trueand drive the controller yourself.- Rotation (
enableRotation) turns the child aboutbasePosition, not about the centre of the pinch. The double tap cycle unwinds it back to zero. - Pan bounds are worked out from the child's unrotated width and height, so with
enableRotationon and the child at an angle, the edges it stops at are the ones it would have had upright. This matches photo_view. - No video, and no widget-per-frame content.
imageProviderresolves once to learn the image's size; an animated GIF plays, but its first frame sets the size. filterQualityapplies toPhotoView.newonly.PhotoView.customChilddraws whatever the child draws.- The gallery does not loop; page 0 is the first page.
tightModefrom photo_view is not carried over. ASizedBoxaround the view is the closest replacement, but it is not the same thing:tightModeshrank the background tochildSize * scaleand kept following it as the zoom changed, where aSizedBoxis whatever size you give it and stays there.
Migrating from photo_view #
Most code moves across with the import alone. What differs:
| photo_view | photo_zoom | Why |
|---|---|---|
photo_view.dart + photo_view_gallery.dart |
one photo_zoom.dart |
One entry point |
minScale: 0.5 |
minScale: PhotoViewScale.value(0.5) |
dynamic became a type: a bad value is a compile error, not a runtime assert |
minScale: PhotoViewComputedScale.contained * 0.8 |
unchanged | |
controller.outputStateStream.listen(fn) |
controller.addListener(fn), or a ValueListenableBuilder |
The controller is a ValueNotifier; no stream, and updates land on the same frame |
PhotoViewControllerBase, addIgnorableListener, setScaleInvisibly, setInvisibly |
removed | Internals that leaked into the public API |
PhotoViewControllerValue.rotationFocusPoint |
removed | It was stored and streamed but never reached the transform |
PhotoViewScaleState.isScaleStateZooming |
.isZooming |
|
tightMode: true |
removed | Wrap in a SizedBox |
PhotoViewGestureDetectorScope(axis: null) |
axis is required |
A scope without an axis did nothing |
PhotoViewGallery(..., scaleStateChangedCallback:) |
unchanged | |
double tap zooms towards basePosition |
it zooms at the tap | #82, #394, #538 |
| mouse wheel ignored | wheel zooms, trackpad pans | #481 |
strictScale freezes the whole gesture past a limit |
the scale clamps, the pan keeps working | |
| n/a | enableScrollZoom |
New |
Controllers behave the same in one respect worth repeating: whoever creates one disposes it.
Example #
example/ is a gallery: a grid of thumbnails that fly into a full screen
PhotoViewGallery with a live zoom readout.
cd example && flutter run
License #
MIT. The API and the scale and pan behaviour are derived from photo_view, also MIT, by Renan C. Araújo.