viewfinder 0.3.2
viewfinder: ^0.3.2 copied to clipboard
Modern Flutter photo viewer / gallery. Pinch / double-tap / mouse-wheel zoom with rubber-band edges and fling, drag-to-dismiss, thumbnails, page indicator, and keyboard shortcuts.
0.3.2 #
Bug fixes #
- Added
mountedguard to the thumbnail bar's post-frame scroll callback. Previously, if the widget was disposed between scheduling and execution of the callback, the scroll could attempt to run on a defunct state.
0.3.1 #
API additions #
ViewfinderThumbnails.errorBuilderlets callers customize the placeholder rendered in the thumbnail strip when anImageProviderfails to decode. When omitted, the library default (ColoredBox(color: Colors.white12)) is used; when provided, the caller's builder runs in its place. Applied to the default tile only — when [itemBuilder] is provided the builder owns the entire visual treatment, including error handling.
Internal #
- Removed an internal
_precachedindex set that was dedupingprecacheImagecalls. Successful providers are already deduped by Flutter'sImageCache; failed providers do get re-attempted on subsequent adjacent moves, but the cost is bounded byprecacheAdjacent. The set never shrank, so it grew monotonically with every visited index. ItsonErrorcallback was also silently swallowing precache failures; precache errors now route through the normalFlutterError.reportErrorpath. The actual display path still surfaces decode errors to the user-suppliederrorBuilder. - Dropped a redundant
LayoutBuilderfrom the image-and-thumb composite. The parentZoomableViewportalready wraps its child in a tight-sizedSizedBox, soImage.fitlays out against the viewport without an extra layout pass.
0.3.0 #
Breaking changes #
ViewfinderImageController.canSwipeHorizontally/canSwipeVerticallygetters removed in favor of a singlecanSwipe(Axis axis, {SwipeEdgeMode mode = SwipeEdgeMode.screen})method. The default behavior is unchanged —canSwipe(Axis.horizontal)returns the same value as the oldcanSwipeHorizontallydid — but the call site changes. Migration:c.canSwipeHorizontally→c.canSwipe(Axis.horizontal),c.canSwipeVertically→c.canSwipe(Axis.vertical).
API additions #
SwipeEdgeModeselects the frame of reference used bycanSwipeunder rotation.SwipeEdgeMode.screen(the default, used by the bundled gallery) checks the rotated content's AABB against the viewport — symmetric with the internal boundary clamp and matching a screen-axis pager's intent.SwipeEdgeMode.contentinverse-projects the viewport into photo space and checks whether it has reached the photo's logical extents[0, viewport.width]/[0, viewport.height]; the result tracks "the user has reached the photo's logical edge in the photo's own frame" at every rotation (no special-casing of cardinal angles). Use it for consumers whose handoff target follows the photo's frame, e.g. a custom pager that swipes along the photo's axes through rotation.
0.2.1 #
API additions #
thumbCrossFadeCurveis now configurable onViewfinderImageandViewfinderItem(defaultCurves.easeOut). Previously the curve was hardcoded whilethumbCrossFadeDurationwas the only knob, so callers wanting non-default easing on the thumb-to-main fade had no way in.Viewfinder.images(...)andViewfinder.single(...)now forward the per-page image-display knobs —thumbCrossFadeDuration,thumbCrossFadeCurve,gaplessPlayback— to everyViewfinderItemthey construct. Previously these were reachable only by writing a customitemBuilder; per-page transform overrides (initialScale/minScale/maxScale) still are.
0.2.0 #
Breaking changes #
- Removed
ViewfinderInitialScale.value(scale). It was behaviorally identical toViewfinderInitialScale.contain(scale)— three names for two behaviors. Usecontain(scale)instead. ViewfinderImage.childandViewfinderItem.childnow require acontentKey. The gallery uses it to detect content swaps and reset the in-page transform on a re-order or swap-in (the image-backed variant gets this for free from theImageProvider's==; for.childthe rendered widget identity is unreliable, so the caller hands in a stable handle). For a single static.child, any constant works (e.g.contentKey: 'main').
Bug fixes #
ViewfinderImageController.canSwipeHorizontally/canSwipeVerticallywere computed from raw matrix translation components and so misreported the edge state whenrotateEnabled: true. They now project the photo's logical left/right (and top/bottom) edges through the current transform and check whether either has been pulled into the viewport — matching the user's intent ("the photo's left side is exposed → swipe to the previous page") rather than the AABB extents (which, under rotation, are the photo's outermost corners, not its visible sides).Viewfinderno longer caches builtViewfinderItems by index. The cache pinned the gallery to the first builder's output, so a dynamic gallery (re-order, swap-in) with the sameitemCountwould keep showing stale items. Pages are now rebuilt lazily through the underlyingPageView.builder/ListView.builder, matching standard Flutter scrollables.ViewfinderImagenow resets its in-page pan/zoom transform when the underlying content identity changes — theImageProvider's==for the image-backed variant, the new requiredcontentKeyfor.child. Previously, slot reuse during a re-order or swap-in left the previous photo's transform applied to the new content. Pure rebuilds with the same content value still preserve the user's transform.
API additions #
Viewfinder.images(...)now forwardsreverse,allowEdgeHandoff, andrubberBandPan(previously only available on the mainViewfinder.newconstructor).
Validation #
- Combining
pagerAxis: Axis.verticalwith a non-nulldismissis now rejected by a debug assert at construction. Both consume vertical drags; pick one. - Attaching the same
ViewfinderImageControllerto more than oneViewfinderImageat once is now rejected by a debug assert. Each controller can drive only one viewer; sharing it would silently overwrite the previous binding and produce incorrect reads throughscaleState/canSwipe*. Release builds still overwrite silently —assertis debug-only — so callers should not rely on it as a hard guard. Internally,ViewfinderImagenow detaches indeactivateand re-attaches inactivateso tree rearrangements andGlobalKeymoves don't trip the assert.
Documentation #
Viewfinder.rotateEnableddoc said "Boundary clamping is disabled while rotated"; in fact the clamp always runs against the rotated content's AABB. Doc fixed to match the implementation.ViewfinderPageIndicatorAdaptivedoc clarifies that customizing innerdots.alignment/padding/label.alignment/paddingis rejected by a debug assert and silently ignored in release.
0.1.0 #
- Initial release.
Viewfindergallery (PageView-backed, horizontal or verticalpagerAxis) andViewfinderImage/.childsingle viewer.ViewfinderInitialScale.contain([factor])/.cover([factor])accept an optional multiplier —contain(0.8)shows the photo at 80% of fit,cover(1.2)zooms to 120% of fill..value(scale)remains as an absolute-multiplier shortcut.- Pinch, pan, double-tap ladder, double-tap-drag continuous zoom, opt-in two-finger rotation; post-release fling on both pan and scale via
FrictionSimulation(X / Y / scale axes), anchored to the focal point captured at release. - Rubber-band elastic edges — pulling a zoomed image past its boundary shows live elastic over-pan with diminishing returns, then animates back on release.
- Custom
ZoomableViewportgesture layer that yields edge pans to the parent scrollable viacanPan(Axis, int). Yields single-pointer drags along the pager axis when not zoomed, so mouse-drag and trackpad-drag swipe pages on web/desktop. Viewfinder.swipeDragDevices— explicit pointer-kind set for the underlyingPageView. Defaults to all kinds (kViewfinderDefaultSwipeDragDevices) — overrides Flutter's defaultScrollBehaviorwhich excludes mouse on web/desktop.ViewfinderItem.thumbImage— low-res preview that cross-fades into the main image on first frame.ViewfinderThumbnails(4 positions,.custom()), sealedViewfinderPageIndicatorwithDots/Label/Adaptivevariants (Adaptivefalls back from dots to a"1 / N"label pastmaxDots;Labelaccepts a customlabelBuilder).ViewfinderDismissdrag-to-dismiss withslideType: wholePage | onlyImageand anonProgresscallback that reports normalized drag progress through the spring-back.ViewfinderChromeController— tap-to-toggle, auto-hide after idle, auto-hide while zoomed.- Keyboard (arrows, PageUp/Down, two-stage Escape), Android back-button two-stage,
PopScope-based Hero coherence on pop. - Mouse wheel + trackpad pinch zoom.
precacheAdjacentwarmsimageCachefor pages ±N around the current one, using the user'sImageProviderdirectly so cache keys match whatImage()will resolve.kViewfinderDefaultFlingDrag = 0.0000135, overridable viainteractionEndFrictionCoefficient.- Semantics labels per image and at gallery level.
Viewfinder.reverse— forwarded toPageView.reversefor right-to-left galleries.Viewfinder.allowEdgeHandoff(defaulttrue) — whenfalse, a zoomed image consumes all pan and never yields to the parentPageView; the user must reset zoom before swiping.Viewfinder.rubberBandPan/ViewfinderImage.rubberBandPan(defaulttrue) — opt out of elastic over-pan for hard edge clamping.ViewfinderImage.onScaleStart/onScaleEnd— gesture lifecycle callbacks, useful for haptics and analytics.ViewfinderImage.gaplessPlayback/ViewfinderImageItem.gaplessPlayback(defaulttrue) — forwarded to the underlyingImage.gaplessPlayback.ViewfinderImageController.currentTransformgetter andjumpToTransform/animateToTransformsetters for imperative position / rotation control.
