library photo_view_gallery; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:immich_mobile/shared/ui/photo_view/photo_view.dart' show LoadingBuilder, PhotoView, PhotoViewImageTapDownCallback, PhotoViewImageTapUpCallback, PhotoViewImageDragStartCallback, PhotoViewImageDragEndCallback, PhotoViewImageDragUpdateCallback, PhotoViewImageScaleEndCallback, ScaleStateCycle; import 'package:immich_mobile/shared/ui/photo_view/src/controller/photo_view_controller.dart'; import 'package:immich_mobile/shared/ui/photo_view/src/controller/photo_view_scalestate_controller.dart'; import 'package:immich_mobile/shared/ui/photo_view/src/core/photo_view_gesture_detector.dart'; import 'package:immich_mobile/shared/ui/photo_view/src/photo_view_scale_state.dart'; import 'package:immich_mobile/shared/ui/photo_view/src/utils/photo_view_hero_attributes.dart'; /// A type definition for a [Function] that receives a index after a page change in [PhotoViewGallery] typedef PhotoViewGalleryPageChangedCallback = void Function(int index); /// A type definition for a [Function] that defines a page in [PhotoViewGallery.build] typedef PhotoViewGalleryBuilder = PhotoViewGalleryPageOptions Function( BuildContext context, int index, ); /// A [StatefulWidget] that shows multiple [PhotoView] widgets in a [PageView] /// /// Some of [PhotoView] constructor options are passed direct to [PhotoViewGallery] constructor. Those options will affect the gallery in a whole. /// /// Some of the options may be defined to each image individually, such as `initialScale` or `PhotoViewHeroAttributes`. Those must be passed via each [PhotoViewGalleryPageOptions]. /// /// Example of usage as a list of options: /// ``` /// PhotoViewGallery( /// pageOptions: [ /// PhotoViewGalleryPageOptions( /// imageProvider: AssetImage("assets/gallery1.jpg"), /// heroAttributes: const PhotoViewHeroAttributes(tag: "tag1"), /// ), /// PhotoViewGalleryPageOptions( /// imageProvider: AssetImage("assets/gallery2.jpg"), /// heroAttributes: const PhotoViewHeroAttributes(tag: "tag2"), /// maxScale: PhotoViewComputedScale.contained * 0.3 /// ), /// PhotoViewGalleryPageOptions( /// imageProvider: AssetImage("assets/gallery3.jpg"), /// minScale: PhotoViewComputedScale.contained * 0.8, /// maxScale: PhotoViewComputedScale.covered * 1.1, /// heroAttributes: const HeroAttributes(tag: "tag3"), /// ), /// ], /// loadingBuilder: (context, progress) => Center( /// child: Container( /// width: 20.0, /// height: 20.0, /// child: CircularProgressIndicator( /// value: _progress == null /// ? null /// : _progress.cumulativeBytesLoaded / /// _progress.expectedTotalBytes, /// ), /// ), /// ), /// backgroundDecoration: widget.backgroundDecoration, /// pageController: widget.pageController, /// onPageChanged: onPageChanged, /// ) /// ``` /// /// Example of usage with builder pattern: /// ``` /// PhotoViewGallery.builder( /// scrollPhysics: const BouncingScrollPhysics(), /// builder: (BuildContext context, int index) { /// return PhotoViewGalleryPageOptions( /// imageProvider: AssetImage(widget.galleryItems[index].image), /// initialScale: PhotoViewComputedScale.contained * 0.8, /// minScale: PhotoViewComputedScale.contained * 0.8, /// maxScale: PhotoViewComputedScale.covered * 1.1, /// heroAttributes: HeroAttributes(tag: galleryItems[index].id), /// ); /// }, /// itemCount: galleryItems.length, /// loadingBuilder: (context, progress) => Center( /// child: Container( /// width: 20.0, /// height: 20.0, /// child: CircularProgressIndicator( /// value: _progress == null /// ? null /// : _progress.cumulativeBytesLoaded / /// _progress.expectedTotalBytes, /// ), /// ), /// ), /// backgroundDecoration: widget.backgroundDecoration, /// pageController: widget.pageController, /// onPageChanged: onPageChanged, /// ) /// ``` class PhotoViewGallery extends StatefulWidget { /// Construct a gallery with static items through a list of [PhotoViewGalleryPageOptions]. const PhotoViewGallery({ super.key, required this.pageOptions, this.loadingBuilder, this.backgroundDecoration, this.wantKeepAlive = false, this.gaplessPlayback = false, this.reverse = false, this.pageController, this.onPageChanged, this.scaleStateChangedCallback, this.enableRotation = false, this.scrollPhysics, this.scrollDirection = Axis.horizontal, this.customSize, this.allowImplicitScrolling = false, }) : itemCount = null, builder = null; /// Construct a gallery with dynamic items. /// /// The builder must return a [PhotoViewGalleryPageOptions]. const PhotoViewGallery.builder({ super.key, required this.itemCount, required this.builder, this.loadingBuilder, this.backgroundDecoration, this.wantKeepAlive = false, this.gaplessPlayback = false, this.reverse = false, this.pageController, this.onPageChanged, this.scaleStateChangedCallback, this.enableRotation = false, this.scrollPhysics, this.scrollDirection = Axis.horizontal, this.customSize, this.allowImplicitScrolling = false, }) : pageOptions = null, assert(itemCount != null), assert(builder != null); /// A list of options to describe the items in the gallery final List? pageOptions; /// The count of items in the gallery, only used when constructed via [PhotoViewGallery.builder] final int? itemCount; /// Called to build items for the gallery when using [PhotoViewGallery.builder] final PhotoViewGalleryBuilder? builder; /// [ScrollPhysics] for the internal [PageView] final ScrollPhysics? scrollPhysics; /// Mirror to [PhotoView.loadingBuilder] final LoadingBuilder? loadingBuilder; /// Mirror to [PhotoView.backgroundDecoration] final BoxDecoration? backgroundDecoration; /// Mirror to [PhotoView.wantKeepAlive] final bool wantKeepAlive; /// Mirror to [PhotoView.gaplessPlayback] final bool gaplessPlayback; /// Mirror to [PageView.reverse] final bool reverse; /// An object that controls the [PageView] inside [PhotoViewGallery] final PageController? pageController; /// An callback to be called on a page change final PhotoViewGalleryPageChangedCallback? onPageChanged; /// Mirror to [PhotoView.scaleStateChangedCallback] final ValueChanged? scaleStateChangedCallback; /// Mirror to [PhotoView.enableRotation] final bool enableRotation; /// Mirror to [PhotoView.customSize] final Size? customSize; /// The axis along which the [PageView] scrolls. Mirror to [PageView.scrollDirection] final Axis scrollDirection; /// When user attempts to move it to the next element, focus will traverse to the next page in the page view. final bool allowImplicitScrolling; bool get _isBuilder => builder != null; @override State createState() { return _PhotoViewGalleryState(); } } class _PhotoViewGalleryState extends State { late final PageController _controller = widget.pageController ?? PageController(); void scaleStateChangedCallback(PhotoViewScaleState scaleState) { if (widget.scaleStateChangedCallback != null) { widget.scaleStateChangedCallback!(scaleState); } } int get actualPage { return _controller.hasClients ? _controller.page!.floor() : 0; } int get itemCount { if (widget._isBuilder) { return widget.itemCount!; } return widget.pageOptions!.length; } @override Widget build(BuildContext context) { // Enable corner hit test return PhotoViewGestureDetectorScope( axis: widget.scrollDirection, child: PageView.builder( reverse: widget.reverse, controller: _controller, onPageChanged: widget.onPageChanged, itemCount: itemCount, itemBuilder: _buildItem, scrollDirection: widget.scrollDirection, physics: widget.scrollPhysics, allowImplicitScrolling: widget.allowImplicitScrolling, ), ); } Widget _buildItem(BuildContext context, int index) { final pageOption = _buildPageOption(context, index); final isCustomChild = pageOption.child != null; final PhotoView photoView = isCustomChild ? PhotoView.customChild( key: ObjectKey(index), childSize: pageOption.childSize, backgroundDecoration: widget.backgroundDecoration, wantKeepAlive: widget.wantKeepAlive, controller: pageOption.controller, scaleStateController: pageOption.scaleStateController, customSize: widget.customSize, scaleStateChangedCallback: scaleStateChangedCallback, enableRotation: widget.enableRotation, initialScale: pageOption.initialScale, minScale: pageOption.minScale, maxScale: pageOption.maxScale, scaleStateCycle: pageOption.scaleStateCycle, onTapUp: pageOption.onTapUp, onTapDown: pageOption.onTapDown, onDragStart: pageOption.onDragStart, onDragEnd: pageOption.onDragEnd, onDragUpdate: pageOption.onDragUpdate, onScaleEnd: pageOption.onScaleEnd, gestureDetectorBehavior: pageOption.gestureDetectorBehavior, tightMode: pageOption.tightMode, filterQuality: pageOption.filterQuality, basePosition: pageOption.basePosition, disableGestures: pageOption.disableGestures, heroAttributes: pageOption.heroAttributes, child: pageOption.child, ) : PhotoView( key: ObjectKey(index), index: index, imageProvider: pageOption.imageProvider, loadingBuilder: widget.loadingBuilder, backgroundDecoration: widget.backgroundDecoration, wantKeepAlive: widget.wantKeepAlive, controller: pageOption.controller, scaleStateController: pageOption.scaleStateController, customSize: widget.customSize, gaplessPlayback: widget.gaplessPlayback, scaleStateChangedCallback: scaleStateChangedCallback, enableRotation: widget.enableRotation, initialScale: pageOption.initialScale, minScale: pageOption.minScale, maxScale: pageOption.maxScale, scaleStateCycle: pageOption.scaleStateCycle, onTapUp: pageOption.onTapUp, onTapDown: pageOption.onTapDown, onDragStart: pageOption.onDragStart, onDragEnd: pageOption.onDragEnd, onDragUpdate: pageOption.onDragUpdate, onScaleEnd: pageOption.onScaleEnd, gestureDetectorBehavior: pageOption.gestureDetectorBehavior, tightMode: pageOption.tightMode, filterQuality: pageOption.filterQuality, basePosition: pageOption.basePosition, disableGestures: pageOption.disableGestures, errorBuilder: pageOption.errorBuilder, heroAttributes: pageOption.heroAttributes, ); return ClipRect( child: photoView, ); } PhotoViewGalleryPageOptions _buildPageOption( BuildContext context, int index, ) { if (widget._isBuilder) { return widget.builder!(context, index); } return widget.pageOptions![index]; } } /// A helper class that wraps individual options of a page in [PhotoViewGallery] /// /// The [maxScale], [minScale] and [initialScale] options may be [double] or a [PhotoViewComputedScale] constant /// class PhotoViewGalleryPageOptions { PhotoViewGalleryPageOptions({ Key? key, required this.imageProvider, this.heroAttributes, this.minScale, this.maxScale, this.initialScale, this.controller, this.scaleStateController, this.basePosition, this.scaleStateCycle, this.onTapUp, this.onTapDown, this.onDragStart, this.onDragEnd, this.onDragUpdate, this.onScaleEnd, this.gestureDetectorBehavior, this.tightMode, this.filterQuality, this.disableGestures, this.errorBuilder, }) : child = null, childSize = null, assert(imageProvider != null); PhotoViewGalleryPageOptions.customChild({ required this.child, this.childSize, this.heroAttributes, this.minScale, this.maxScale, this.initialScale, this.controller, this.scaleStateController, this.basePosition, this.scaleStateCycle, this.onTapUp, this.onTapDown, this.onDragStart, this.onDragEnd, this.onDragUpdate, this.onScaleEnd, this.gestureDetectorBehavior, this.tightMode, this.filterQuality, this.disableGestures, }) : errorBuilder = null, imageProvider = null; /// Mirror to [PhotoView.imageProvider] final ImageProvider? imageProvider; /// Mirror to [PhotoView.heroAttributes] final PhotoViewHeroAttributes? heroAttributes; /// Mirror to [PhotoView.minScale] final dynamic minScale; /// Mirror to [PhotoView.maxScale] final dynamic maxScale; /// Mirror to [PhotoView.initialScale] final dynamic initialScale; /// Mirror to [PhotoView.controller] final PhotoViewController? controller; /// Mirror to [PhotoView.scaleStateController] final PhotoViewScaleStateController? scaleStateController; /// Mirror to [PhotoView.basePosition] final Alignment? basePosition; /// Mirror to [PhotoView.child] final Widget? child; /// Mirror to [PhotoView.childSize] final Size? childSize; /// Mirror to [PhotoView.scaleStateCycle] final ScaleStateCycle? scaleStateCycle; /// Mirror to [PhotoView.onTapUp] final PhotoViewImageTapUpCallback? onTapUp; /// Mirror to [PhotoView.onDragUp] final PhotoViewImageDragStartCallback? onDragStart; /// Mirror to [PhotoView.onDragDown] final PhotoViewImageDragEndCallback? onDragEnd; /// Mirror to [PhotoView.onDraUpdate] final PhotoViewImageDragUpdateCallback? onDragUpdate; /// Mirror to [PhotoView.onTapDown] final PhotoViewImageTapDownCallback? onTapDown; /// Mirror to [PhotoView.onScaleEnd] final PhotoViewImageScaleEndCallback? onScaleEnd; /// Mirror to [PhotoView.gestureDetectorBehavior] final HitTestBehavior? gestureDetectorBehavior; /// Mirror to [PhotoView.tightMode] final bool? tightMode; /// Mirror to [PhotoView.disableGestures] final bool? disableGestures; /// Quality levels for image filters. final FilterQuality? filterQuality; /// Mirror to [PhotoView.errorBuilder] final ImageErrorWidgetBuilder? errorBuilder; }