1
0
mirror of https://github.com/immich-app/immich.git synced 2024-12-24 10:37:28 +02:00
immich/mobile/lib/shared/ui/photo_view/photo_view_gallery.dart
martyfuhry dac4020f27
fix(mobile): Fixes hero animation on main timeline (#1924)
* fixed hero animation for local assets

* fixes backwards hero animation out of gallery image
2023-03-03 16:49:40 -06:00

458 lines
15 KiB
Dart

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>[
/// 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,
child: pageOption.child,
)
: PhotoView(
key: ObjectKey(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,
);
if (pageOption.heroAttributes != null) {
return Hero(
tag: pageOption.heroAttributes!.tag,
createRectTween: pageOption.heroAttributes!.createRectTween,
flightShuttleBuilder: pageOption.heroAttributes!.flightShuttleBuilder,
placeholderBuilder: pageOption.heroAttributes!.placeholderBuilder,
transitionOnUserGestures: pageOption.heroAttributes!.transitionOnUserGestures,
child: ClipRect(
child: photoView,
),
);
}
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;
}