flutter_hero_photo_viewer 0.1.0
flutter_hero_photo_viewer: ^0.1.0 copied to clipboard
Instagram-style photo viewer with a jank-free Hero transition from a cover thumbnail grid: isomorphic Hero endpoints, deferred gesture layer, swipe-to-dismiss, pinch / double-tap zoom, paging.
flutter_hero_photo_viewer #
English | 简体中文
Instagram-style photo viewer for Flutter: seamless, jank-free Hero transition from square cover thumbnail grids to a full-screen viewer with zero aspect-ratio distortion, zero flicker, swipe-to-dismiss, pinch/double-tap zoom, and paging.
All common Hero transition pitfalls are eliminated at the architectural level.
Features #
- Isomorphic Hero Endpoints — Both ends use
BoxFit.coverwithin matched geometric bounds (screenWidth × aspectRatio). The default Flutter RectTween interpolates cleanly without distortion. - Deferred Layering — Only the lightweight thumbnail is mounted during flight. High-resolution decoding and gesture handling layers attach only after the route animation completes.
- Offstage Frame Guard — Guarded against Flutter's offstage layout frame where
ModalRoute.animationfalsely reports completed. Never displays destination layers prematurely. - Zero-Flicker Pop — Gesture and overlay layers dismount immediately on route reverse, preventing lingering full-size images during the flight back to the grid.
- Physics Swipe-to-Dismiss — Smooth gesture handling (translation + scale + backdrop fade) without third-party page wrappers that cause flashes or coordinate jumps.
- Multi-Touch Zoom & Paging — Pinch-to-zoom, double-tap zoom, and horizontal paging across multiple photos.
- Decoupled Overlay System — Custom UI overlay (
overlayBuilder) for title, pagination, actions, or close buttons, fading smoothly with dismiss gestures. - Zero Native Dependencies — Pure Dart/Flutter, fully compatible with iOS, Android, Web, macOS, Windows, and Linux.
Installation #
Add flutter_hero_photo_viewer to your pubspec.yaml:
dependencies:
flutter_hero_photo_viewer: ^0.1.0
Or run:
flutter pub add flutter_hero_photo_viewer
Quick Start #
1. Grid Thumbnail #
Import the package:
import 'package:flutter_hero_photo_viewer/flutter_hero_photo_viewer.dart';
Wrap your grid item with HeroPhotoThumbnail:
HeroPhotoThumbnail(
image: thumbProvider,
heroTag: 'photo_$id',
fit: BoxFit.cover,
)
Tip: You can also use your own widget, provided it wraps
Hero(tag, flightShuttleBuilder: heroPhotoFlightShuttle)with aBoxFit.coverimage and caches its aspect ratio viaHeroPhotoSizes.remember.
2. Open Viewer #
HeroPhotoViewer.open(
context,
photos: [
HeroPhoto(
image: fullImageProvider,
thumbnail: thumbProvider,
heroTag: 'photo_$id',
),
],
initialIndex: index,
);
Custom Overlay UI #
flutter_hero_photo_viewer handles flight, gestures, zoom, and paging. Any floating controls (close button, page indicator, share button, title, caption) are built using overlayBuilder:
// 1. Extend HeroPhoto to carry custom business metadata
class WorkPhoto extends HeroPhoto {
const WorkPhoto({
required super.image,
super.thumbnail,
super.heroTag,
required this.title,
});
final String title;
}
// 2. Build custom overlay
HeroPhotoViewer.open(
context,
photos: works,
initialIndex: i,
overlayBuilder: (context, info) => HeroPhotoOverlayFade(
progress: info.dismissProgress, // Fades out with swipe-to-dismiss
child: Stack(
children: [
HeroPhotoCloseButton(onTap: info.close),
HeroPhotoPageIndicator(index: info.index, total: info.total),
Positioned(
left: 0,
right: 0,
bottom: 80,
child: Text(
(info.photo as WorkPhoto).title,
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.white, fontSize: 16),
),
),
Positioned(
right: 16,
bottom: 40,
child: IconButton(
icon: const Icon(Icons.ios_share, color: Colors.white),
onPressed: () => share(info.photo),
),
),
],
),
),
);
Pre-built Overlay Components #
HeroPhotoDefaultOverlay— Standard overlay with top close button and center page indicator.HeroPhotoOverlayFade— Wraps your overlay and fades it out based oninfo.dismissProgress.HeroPhotoCloseButton— Standard top-left circular close button.HeroPhotoPageIndicator— Pill-style page index indicator (1 / 5).
Core Architecture: The Isomorphic Hero Principle #
Every transition bug in photo viewers stems from mismatched geometry or premature decoding. This package enforces 7 golden rules:
- Both endpoints are
BoxFit.coverwith the sameImageProvider.
The full-screen container is not a full-screen box; it is a centered box ofscreenWidth × imageAspectRatio. When a cover image sits inside an aspect-fit box, it visually behaves likecontain. The defaultRectTweenhandles the interpolation cleanly without stretching. - Hero wraps only static images; flight uses only the thumbnail.
Full-size images and gesture layers only mount after the route transition finishes (AnimationStatus.completed). Decoding high-res images on the push frame causes dropped frames. - Route first frame is offstage;
ModalRoute.animationreports falsecompleted.
During initial offstage layout, Flutter's route animation reportscompletedwhile the actual transition is still at 0.0. The package guards against this so the destination image never flickers prematurely. - Interactive layer dismounts instantly on reverse.
When the user pops or begins reverse navigation, the gesture layer is immediately unmounted so it does not linger or obscure the returning shuttle. - Custom physics swipe-to-dismiss.
Dismissal handles translation, scaling, and backdrop opacity natively without third-party sliding page packages that cause coordinate jumping. - Pre-cached aspect ratios for zero-wait tap.
HeroPhotoThumbnailrecords image aspect ratios inHeroPhotoSizesas soon as the grid renders. Clicking opens immediately without an asynchronous dimension query. enableLoadState: falseon local image sources.
Prevents infinite progress spinners on instant memory/file caches, ensuring widget tests and UI remain deterministic.
API Reference #
HeroPhotoViewer.open #
| Parameter | Type | Default | Description |
|---|---|---|---|
context |
BuildContext |
required | The navigation context. |
photos |
List<HeroPhoto> |
required | List of photos to display. |
initialIndex |
int |
0 |
Initial photo index to display. |
backgroundColor |
Color |
Colors.black |
Background color of viewer. |
enableSwipeToDismiss |
bool |
true |
Enable vertical swipe to dismiss. |
overlayBuilder |
Widget Function(BuildContext, HeroPhotoViewerInfo)? |
null |
Custom top-level overlay builder. |
callbacks |
HeroPhotoViewerCallbacks? |
null |
Callbacks for onOpen, onPageChanged, onDismiss. |
HeroPhoto #
| Property | Type | Description |
|---|---|---|
image |
ImageProvider |
Full-resolution image provider. |
thumbnail |
ImageProvider? |
Thumbnail provider matching grid cache (==). |
heroTag |
Object? |
Hero tag matching grid thumbnail. null for fade-in only. |
size |
Size? |
Image size/aspect ratio (defaults to HeroPhotoSizes.of(thumbnail)). |
HeroPhotoViewerInfo #
| Property | Type | Description |
|---|---|---|
index |
int |
Current active page index (0-based). |
total |
int |
Total number of photos. |
photo |
HeroPhoto |
Current active photo object. |
dismissProgress |
double |
Swipe-to-dismiss progress (0.0 normal, 1.0 dismissed). |
settled |
bool |
True once Hero flight is completed. |
close |
VoidCallback |
Programmatically triggers close animation. |
pageController |
PageController |
Controller for the horizontal photo pager. |
Testing #
The package includes comprehensive widget tests in test/hero_photo_viewer_test.dart asserting:
- During flight, only the thumbnail layer is present at the destination.
- High-res image and gesture layers only mount when
completed. - First reverse/pop frame immediately dismounts the gesture layer.
- Works gracefully without Hero tags (pure fade transition).
Run tests:
flutter test
License #
MIT License © 2026 Hugo