Flutter Photo View
photo_view_plus provides a gesture-aware zoomable widget for images and custom
content. This fork is updated for modern Flutter, adds a stronger typed API,
and exposes new configuration points for overlays, gallery behavior, and
interaction policies.
Requirements
- Flutter
>=3.14.5 - Dart
>=3.1.0
dependencies:
photo_view_plus: ^1.1.0
What's New
- Flutter 3.14.5+ and Dart 3.1+ baseline
- typed scale API with
PhotoViewScale.fixed(...) - new
PhotoViewOptionsfor consolidated widget configuration - new
PhotoViewGalleryOptionsfor gallery preload and retention - richer customization with
overlayBuilder,backgroundBuilder,loadingStateBuilder, anderrorStateBuilder childWrapperfor wrapping the final rendered widget in single view or gallery- new gesture callbacks:
onLongPress,onScaleStart, andonScaleUpdate disableDoubleTapto keep pan and pinch active while disabling the internal double tap zoom cyclePhotoViewComputedScale.containedNoScaleUpfor contain-fit without enlarging smaller assets- injectable
PhotoViewInteractionPolicyfor clamp, gesture-end, and dynamic filter quality rules - gallery page option caching and configurable image preloading
- desktop/web pointer support with wheel pan,
Ctrl+scrollzoom, trackpad pan/zoom, and contextual cursors - internal architecture split into
ui/,domain/,data/,core/, andshared/
Basic Usage
import 'package:photo_view_plus/photo_view_plus.dart';
PhotoView(
imageProvider: const AssetImage('assets/large-image.jpg'),
initialScale: PhotoViewScale.contained,
minScale: PhotoViewScale.contained * 0.8,
maxScale: PhotoViewScale.covered * 1.8,
);
Use PhotoView.customChild to zoom any widget:
PhotoView.customChild(
child: const FlutterLogo(size: 200),
childSize: const Size(200, 200),
initialScale: const PhotoViewScale.fixed(1),
);
Configuration with PhotoViewOptions
Prefer options for new integrations. Legacy constructor parameters still
work and override values from options when both are provided.
PhotoView(
imageProvider: const AssetImage('assets/large-image.jpg'),
options: PhotoViewOptions(
filterQuality: FilterQuality.high,
strictScale: true,
overlayBuilder: (context, details) => Align(
alignment: Alignment.bottomRight,
child: Text(details.scaleState.name),
),
),
);
PhotoViewOptions supports:
backgroundDecorationwantKeepAlivecustomSizegestureDetectorBehaviortightModefilterQualitydisableGesturesdisableDoubleTapenablePanAlwaysstrictScaleinteractionPolicyoverlayBuilderbackgroundBuilderloadingStateBuildererrorStateBuilderchildWrapper
New Interaction Hooks
PhotoView(
imageProvider: provider,
options: const PhotoViewOptions(disableDoubleTap: true),
onLongPress: (context, value) {
debugPrint('long press at scale ${value.scale}');
},
onScaleStart: (context, details, value) {},
onScaleUpdate: (context, details, value) {},
);
For desktop and web, mouse wheel pan, Ctrl + scroll zoom-to-cursor, and
trackpad pan/zoom now work out of the box.
Gallery
import 'package:photo_view_plus/photo_view_plus.dart';
import 'package:photo_view_plus/photo_view_plus_gallery.dart';
PhotoViewGallery.builder(
itemCount: galleryItems.length,
options: const PhotoViewGalleryOptions(
preloadPagesCount: 2,
pageRetentionPolicy: PhotoViewGalleryPageRetentionPolicy.keepAlive,
),
builder: (context, index) {
final item = galleryItems[index];
return PhotoViewGalleryPageOptions(
imageProvider: AssetImage(item.image),
initialScale: PhotoViewScale.contained,
heroAttributes: PhotoViewHeroAttributes(tag: item.id),
options: PhotoViewOptions(
overlayBuilder: (_, details) => Align(
alignment: Alignment.topRight,
child: Text(details.scaleState.name),
),
),
);
},
);
PhotoViewGalleryOptions adds:
preloadPagesCountpageRetentionPolicyscrollPhysicsscrollDirectionallowImplicitScrollingpageSnapping- shared
optionsfor all pages - shared
childWrapperfor all pages
PhotoViewGalleryPageOptions now also accepts:
pageKeyoptionschildWrapperdisableDoubleTapstrictScaleonLongPressonScaleStartonScaleUpdate
Wrapping The Final Child
Use childWrapper when you need interactive UI around the final rendered
result instead of a passive overlay:
PhotoViewGallery.builder(
itemCount: items.length,
options: PhotoViewGalleryOptions(
childWrapper: (context, index, child) {
return Stack(
fit: StackFit.expand,
children: [
child,
Positioned(
top: 16,
right: 16,
child: IconButton(
onPressed: () {},
icon: const Icon(Icons.more_vert, color: Colors.white),
),
),
],
);
},
),
builder: (context, index) => PhotoViewGalleryPageOptions(
imageProvider: AssetImage(items[index]),
),
);
Interaction Policies
PhotoViewInteractionPolicy lets you customize interaction rules without
forking the widget.
const policy = PhotoViewInteractionPolicy(
filterQuality: defaultFilterQualityProvider,
clampPosition: defaultClampPositionPolicy,
onGestureEnd: defaultGestureEndPolicy,
);
You can replace:
- position clamp behavior
- post-gesture return/fling behavior
- filter quality while gestures are active
Migration Guide
1. Update SDK constraints
Use Flutter >=3.14.5 and Dart >=3.1.0.
2. Replace dynamic scale values
Old:
PhotoView(
minScale: 0.8,
maxScale: 3.0,
initialScale: 1.0,
);
New:
PhotoView(
minScale: const PhotoViewScale.fixed(0.8),
maxScale: const PhotoViewScale.fixed(3.0),
initialScale: const PhotoViewScale.fixed(1.0),
);
Viewport-relative scales still work:
PhotoView(
minScale: PhotoViewComputedScale.contained * 0.8,
maxScale: PhotoViewComputedScale.covered * 1.8,
initialScale: PhotoViewScale.contained,
);
There is also a no-upscale fit mode:
PhotoView(
imageProvider: provider,
initialScale: PhotoViewComputedScale.containedNoScaleUp,
);
3. Move optional flags into options
Old:
PhotoView(
imageProvider: provider,
filterQuality: FilterQuality.high,
strictScale: true,
enablePanAlways: false,
);
New:
PhotoView(
imageProvider: provider,
options: const PhotoViewOptions(
filterQuality: FilterQuality.high,
strictScale: true,
enablePanAlways: false,
),
);
4. Migrate gallery setup
Old gallery-wide state retention relied on wantKeepAlive.
New code can use:
const PhotoViewGalleryOptions(
preloadPagesCount: 2,
pageRetentionPolicy: PhotoViewGalleryPageRetentionPolicy.keepAlive,
)
5. Adopt rich loading and error builders
Old:
loadingBuilder: (context, event) => const CircularProgressIndicator(),
errorBuilder: (context, error, stackTrace) => const Icon(Icons.error),
New:
options: PhotoViewOptions(
loadingStateBuilder: (context, details) {
return CircularProgressIndicator(
value: details.progress == null
? null
: details.progress!.cumulativeBytesLoaded /
details.progress!.expectedTotalBytes!,
);
},
errorStateBuilder: (context, details) => Text('${details.error}'),
),
Breaking Changes Summary
- Flutter and Dart minimum versions increased
- scale inputs are now typed as
PhotoViewScale - new options objects are the preferred configuration path
- gallery adds preload/retention concepts beyond
wantKeepAlive
Controllers
PhotoViewController exposes viewport state updates.
PhotoViewScaleStateController exposes scale-state transitions.
Both follow the standard Flutter controller lifecycle: create externally when needed, listen to their streams, and dispose them when no longer used.
Internal Architecture
The package internals are organized as:
lib/src/ui/: widgets, view models, coordinatorslib/src/domain/: immutable models and interaction ruleslib/src/data/: image stream resolutionlib/src/core/: low-level rendering and layout primitiveslib/src/shared/: small reusable foundation utilities
This matters mainly for contributors. Public integrations should keep using the
exports from lib/photo_view_plus.dart and lib/photo_view_plus_gallery.dart.
Validation
The current package state is validated with:
flutter analyzeflutter test
The automated suite currently covers:
- controller and scale-state controller behavior
- typed scale resolution, including
PhotoViewComputedScale.containedNoScaleUp PhotoViewOptionshooks such asoverlayBuilderandchildWrapper- gesture callbacks:
onLongPress,onScaleStart, andonScaleUpdate disableDoubleTapbehavior in single view and gallery page overridesenablePanAlwaysdrag behavior- gallery preload, page-option caching, and
childWrapper - desktop/web pointer interactions:
PointerScrollEventpan,Ctrl + scrollzoom, and trackpadPointerPanZoomUpdateEvent - error-state rendering with
heroAttributesstill keeping aHeroin the tree
Example App
Run the example locally:
flutter run -d <device> example/lib/main.dart
The gallery example demonstrates preload, retention, and overlay support.