2023-02-01 18:59:34 +02:00
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 (
2023-08-10 15:38:49 +02:00
BuildContext context ,
2023-02-01 18:59:34 +02:00
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>[
/// 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 ( {
Key ? 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 ,
super ( key: key ) ;
/// Construct a gallery with dynamic items.
///
/// The builder must return a [PhotoViewGalleryPageOptions].
const PhotoViewGallery . builder ( {
Key ? 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 ) ,
super ( key: key ) ;
/// A list of options to describe the items in the gallery
final List < PhotoViewGalleryPageOptions > ? 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 < PhotoViewScaleState > ? 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 < StatefulWidget > createState ( ) {
return _PhotoViewGalleryState ( ) ;
}
}
class _PhotoViewGalleryState extends State < PhotoViewGallery > {
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 ,
2023-08-10 15:38:49 +02:00
heroAttributes: pageOption . heroAttributes ,
2023-02-01 18:59:34 +02:00
child: pageOption . child ,
)
: PhotoView (
key: ObjectKey ( index ) ,
2023-09-18 05:57:05 +02:00
index: index ,
2023-02-01 18:59:34 +02:00
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 ,
2023-08-10 15:38:49 +02:00
heroAttributes: pageOption . heroAttributes ,
2023-02-01 18:59:34 +02:00
) ;
return ClipRect (
child: photoView ,
) ;
}
2023-09-18 05:57:05 +02:00
PhotoViewGalleryPageOptions _buildPageOption (
BuildContext context ,
int index ,
) {
2023-02-01 18:59:34 +02:00
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 ;
}