Skip to content

Commit

Permalink
Infinite scroll for multiple images (#59)
Browse files Browse the repository at this point in the history
* Add infinite scroll option for multiple images

* Updated example app
  • Loading branch information
furkankurt authored Apr 24, 2024
1 parent e563f91 commit 6fd1196
Show file tree
Hide file tree
Showing 6 changed files with 193 additions and 6 deletions.
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,27 @@ The default error widget shows a red `Icons.broken_image` and '🖼️💥🚫'

![Default Error Widget](https://github.com/thesmythgroup/easy_image_viewer/blob/main/demo_images/default-error-widget.png?raw=true "Default Error Widget")


## Using Infinite Scroll for Multiple Images

You can use `infinitelyScrollable: true` to create a "looping" effect with the images. This means that when you reach the end of the image list, you will be taken back to the beginning. This can be useful if you have a small number of images and want to allow the user to scroll through them continuously.

```dart
MultiImageProvider multiImageProvider = MultiImageProvider([
const NetworkImage("https://picsum.photos/id/1001/4912/3264"),
const NetworkImage("https://picsum.photos/id/1003/1181/1772"),
const NetworkImage("https://picsum.photos/id/1004/4912/3264"),
const NetworkImage("https://picsum.photos/id/1005/4912/3264")
]);
showImageViewerPager(context, multiImageProvider, onPageChanged: (page) {
print("page changed to $page");
}, onViewerDismissed: (page) {
print("dismissed while on page $page");
}, infinitelyScrollable: true);
```


## How to release a new version on pub.dev
1. Update the version number in `pubspec.yaml`.
2. Add an entry for the new version in `CHANGELOG.md`.
Expand Down
9 changes: 9 additions & 0 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,15 @@ class _MyHomePageState extends State<MyHomePage> {
showImageViewerPager(context, multiImageProvider,
swipeDismissible: true, doubleTapZoomable: true);
}),
ElevatedButton(
child: const Text("Show Multiple Images (Simple) with Infinite Scroll"),
onPressed: () {
MultiImageProvider multiImageProvider =
MultiImageProvider(_imageProviders);
showImageViewerPager(context, multiImageProvider,
swipeDismissible: true, doubleTapZoomable: true,
infinitelyScrollable: true);
}),
ElevatedButton(
child: const Text("Show Multiple Images (Custom)"),
onPressed: () {
Expand Down
3 changes: 3 additions & 0 deletions lib/easy_image_viewer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ Future<Dialog?> showImageViewer(
/// The optional [useSafeArea] boolean defaults to false and is passed to [showDialog].
/// The optional [swipeDismissible] boolean defaults to false and allows swipe-down-to-dismiss.
/// The optional [doubleTapZoomable] boolean defaults to false and allows double tap to zoom.
/// The optional [infinitelyScrollable] boolean defaults to false and allows infinite scrolling.
/// The [backgroundColor] defaults to black, but can be set to any other color.
/// The [closeButtonTooltip] text is displayed when the user long-presses on the
/// close button and is used for accessibility.
Expand All @@ -74,6 +75,7 @@ Future<Dialog?> showImageViewerPager(
bool useSafeArea = false,
bool swipeDismissible = false,
bool doubleTapZoomable = false,
bool infinitelyScrollable = false,
Color backgroundColor = _defaultBackgroundColor,
String closeButtonTooltip = _defaultCloseButtonTooltip,
Color closeButtonColor = _defaultCloseButtonColor}) {
Expand All @@ -93,6 +95,7 @@ Future<Dialog?> showImageViewerPager(
useSafeArea: useSafeArea,
swipeDismissible: swipeDismissible,
doubleTapZoomable: doubleTapZoomable,
infinitelyScrollable: infinitelyScrollable,
backgroundColor: backgroundColor,
closeButtonColor: closeButtonColor,
closeButtonTooltip: closeButtonTooltip);
Expand Down
23 changes: 19 additions & 4 deletions lib/src/easy_image_view_pager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class EasyImageViewPager extends StatefulWidget {
final EasyImageProvider easyImageProvider;
final PageController pageController;
final bool doubleTapZoomable;
final bool infinitelyScrollable;

/// Callback for when the scale has changed, only invoked at the end of
/// an interaction.
Expand All @@ -30,7 +31,8 @@ class EasyImageViewPager extends StatefulWidget {
required this.easyImageProvider,
required this.pageController,
this.doubleTapZoomable = false,
this.onScaleChanged})
this.onScaleChanged,
this.infinitelyScrollable = false,})
: super(key: key);

@override
Expand All @@ -47,13 +49,16 @@ class _EasyImageViewPagerState extends State<EasyImageViewPager> {
? const PageScrollPhysics()
: const NeverScrollableScrollPhysics(),
key: GlobalObjectKey(widget.easyImageProvider),
itemCount: widget.easyImageProvider.imageCount,
itemCount: widget.infinitelyScrollable
? null
: widget.easyImageProvider.imageCount,
controller: widget.pageController,
scrollBehavior: MouseEnabledScrollBehavior(),
itemBuilder: (context, index) {
final pageIndex = _getPageIndex(index);
return EasyImageView.imageWidget(
widget.easyImageProvider.imageWidgetBuilder(context, index),
key: Key('easy_image_view_$index'),
widget.easyImageProvider.imageWidgetBuilder(context, pageIndex),
key: Key('easy_image_view_$pageIndex'),
doubleTapZoomable: widget.doubleTapZoomable,
onScaleChanged: (scale) {
if (widget.onScaleChanged != null) {
Expand All @@ -68,4 +73,14 @@ class _EasyImageViewPagerState extends State<EasyImageViewPager> {
},
);
}

// If the infinitelyScrollable true, the page number is calculated modulo the
// total number of images, effectively creating a looping carousel effect.
// Otherwise, the index is returned as is.
int _getPageIndex(int index) {
if (widget.infinitelyScrollable) {
return index % widget.easyImageProvider.imageCount;
}
return index;
}
}
18 changes: 16 additions & 2 deletions lib/src/easy_image_viewer_dismissible_dialog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class EasyImageViewerDismissibleDialog extends StatefulWidget {
final Color backgroundColor;
final String closeButtonTooltip;
final Color closeButtonColor;
final bool infinitelyScrollable;

/// Refer to [showImageViewerPager] for the arguments
const EasyImageViewerDismissibleDialog(this.imageProvider,
Expand All @@ -29,6 +30,7 @@ class EasyImageViewerDismissibleDialog extends StatefulWidget {
this.useSafeArea = false,
this.swipeDismissible = false,
this.doubleTapZoomable = false,
this.infinitelyScrollable = false,
required this.backgroundColor,
required this.closeButtonTooltip,
required this.closeButtonColor})
Expand Down Expand Up @@ -60,7 +62,7 @@ class _EasyImageViewerDismissibleDialogState
PageController(initialPage: widget.imageProvider.initialIndex);
if (widget.onPageChanged != null) {
_internalPageChangeListener = () {
widget.onPageChanged!(_pageController.page?.round() ?? 0);
widget.onPageChanged!(_getCurrentPage());
};
_pageController.addListener(_internalPageChangeListener!);
}
Expand Down Expand Up @@ -100,6 +102,7 @@ class _EasyImageViewerDismissibleDialogState
easyImageProvider: widget.imageProvider,
pageController: _pageController,
doubleTapZoomable: widget.doubleTapZoomable,
infinitelyScrollable: widget.infinitelyScrollable,
onScaleChanged: (scale) {
setState(() {
_dismissDirection = scale <= 1.0
Expand Down Expand Up @@ -145,7 +148,7 @@ class _EasyImageViewerDismissibleDialogState
// through the "x" close button, or through swipe-to-dismiss.
void _handleDismissal() {
if (widget.onViewerDismissed != null) {
widget.onViewerDismissed!(_pageController.page?.round() ?? 0);
widget.onViewerDismissed!(_getCurrentPage());
}

if (widget.immersive) {
Expand All @@ -155,4 +158,15 @@ class _EasyImageViewerDismissibleDialogState
_pageController.removeListener(_internalPageChangeListener!);
}
}

// Returns the current page number.
// If the infinitelyScrollable true, the page number is calculated modulo the
// total number of images, effectively creating a looping carousel effect.
int _getCurrentPage() {
var currentPage = _pageController.page?.round() ?? 0;
if (widget.infinitelyScrollable) {
currentPage = currentPage % widget.imageProvider.imageCount;
}
return currentPage;
}
}
125 changes: 125 additions & 0 deletions test/easy_image_viewer_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,80 @@ void main() {
expect(pageOnDismissal, 2);
});


testWidgets('should have a PageView of infinite images and invoke callbacks',
(WidgetTester tester) async {
List<ImageProvider> imageProviders = List.empty(growable: true);
final context = await createTestBuildContext(tester);
bool dismissed = false;
int currentPage = -1;
int pageOnDismissal = -1;

await tester.runAsync(() async {
const colors = [
Colors.amber,
Colors.red,
Colors.green,
Colors.blue,
Colors.teal
];
imageProviders =
await Future.wait(colors.map((color) => createColorImageProvider(color)));
});

final multiImageProvider = MultiImageProvider(imageProviders);

final dialogFuture = showImageViewerPager(context, multiImageProvider,
onPageChanged: (page) {
currentPage = page;
}, onViewerDismissed: (page) {
dismissed = true;
pageOnDismissal = page;
}, infinitelyScrollable: true);
await tester.pumpAndSettle();

// Create the Finders.
final pageViewFinder = find.byKey(GlobalObjectKey(multiImageProvider));
final closeButtonFinder = find.byIcon(Icons.close);

// Check existence
expect(pageViewFinder, findsOneWidget);
expect(closeButtonFinder, findsOneWidget);

// Swipe to second image
await tester.drag(pageViewFinder, const Offset(-501.0, 0.0));
await tester.pumpAndSettle();
expect(currentPage, 1);

// Swipe to third image
await tester.drag(pageViewFinder, const Offset(-501.0, 0.0));
await tester.pumpAndSettle();
expect(currentPage, 2);

// Swipe to fourth image
await tester.drag(pageViewFinder, const Offset(-501.0, 0.0));
await tester.pumpAndSettle();
expect(currentPage, 3);

// Swipe to fifth image
await tester.drag(pageViewFinder, const Offset(-501.0, 0.0));
await tester.pumpAndSettle();
expect(currentPage, 4);

// Swipe to first image
await tester.drag(pageViewFinder, const Offset(-501.0, 0.0));
await tester.pumpAndSettle();
expect(currentPage, 0);

// Dismiss the dialog
await tester.tap(closeButtonFinder);

await dialogFuture;

expect(dismissed, true);
expect(pageOnDismissal, 0);
});

testWidgets('should invoke callbacks when dismissed with a swipe',
(WidgetTester tester) async {
List<ImageProvider> imageProviders = List.empty(growable: true);
Expand Down Expand Up @@ -219,6 +293,57 @@ void main() {
expect(pageOnDismissal, 2);
});

testWidgets('should respect the initialIndex when infinitelyScrollable is true',
(WidgetTester tester) async {
List<ImageProvider> imageProviders = List.empty(growable: true);
final context = await createTestBuildContext(tester);
bool dismissed = false;
int pageOnDismissal = -1;

await tester.runAsync(() async {
const colors = [
Colors.amber,
Colors.red,
Colors.green,
Colors.blue,
Colors.teal
];
imageProviders =
await Future.wait(colors.map((color) => createColorImageProvider(color)));
});

final multiImageProvider =
MultiImageProvider(imageProviders, initialIndex: 2);

final dialogFuture = showImageViewerPager(context, multiImageProvider,
onViewerDismissed: (page) {
dismissed = true;
pageOnDismissal = page;
},infinitelyScrollable: true);
await tester.pumpAndSettle();

// Create the Finders.
final closeButtonFinder = find.byIcon(Icons.close);

// Check default closeButtonColor
IconButton closeButton =
tester.firstWidget(find.widgetWithIcon(IconButton, Icons.close));
expect(closeButton.color, Colors.white);

// Check default dialog backgroundColor
Dialog dialog = tester
.firstWidget(find.byWidgetPredicate((widget) => widget is Dialog));
expect(dialog.backgroundColor, Colors.black);

// Dismiss the dialog
await tester.tap(closeButtonFinder);

await dialogFuture;

expect(dismissed, true);
expect(pageOnDismissal, 2);
});

testWidgets('should respect the backgroundColor and closeButtonColor',
(WidgetTester tester) async {
List<ImageProvider> imageProviders = List.empty(growable: true);
Expand Down

0 comments on commit 6fd1196

Please sign in to comment.