Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add option to allow swipe-down to dismiss #21

Merged
merged 3 commits into from
Aug 4, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"recommendations": [
"dart-code.dart-code",
"dart-code.flutter"
]
}

7 changes: 7 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"[dart]": {
"editor.defaultFormatter": "Dart-Code.dart-code",
"editor.formatOnSave": false
},
"dart.lineLength": 120
}
2 changes: 2 additions & 0 deletions example/ios/Runner/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,7 @@
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
</dict>
</plist>
11 changes: 4 additions & 7 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -58,17 +58,14 @@ class _MyHomePageState extends State<MyHomePage> {
ElevatedButton(
child: const Text("Show Single Image"),
onPressed: () {
showImageViewer(
context,
Image.network("https://picsum.photos/id/1001/5616/3744")
.image);
showImageViewer(context, Image.network("https://picsum.photos/id/1001/5616/3744").image,
swipeDismissible: true);
}),
ElevatedButton(
child: const Text("Show Multiple Images (Simple)"),
onPressed: () {
MultiImageProvider multiImageProvider =
MultiImageProvider(_imageProviders);
showImageViewerPager(context, multiImageProvider);
MultiImageProvider multiImageProvider = MultiImageProvider(_imageProviders);
showImageViewerPager(context, multiImageProvider, swipeDismissible: true);
}),
ElevatedButton(
child: const Text("Show Multiple Images (Custom)"),
Expand Down
8 changes: 8 additions & 0 deletions example/windows/flutter/generated_plugins.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
list(APPEND FLUTTER_PLUGIN_LIST
)

list(APPEND FLUTTER_FFI_PLUGIN_LIST
)

set(PLUGIN_BUNDLED_LIBRARIES)

foreach(plugin ${FLUTTER_PLUGIN_LIST})
Expand All @@ -13,3 +16,8 @@ foreach(plugin ${FLUTTER_PLUGIN_LIST})
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
endforeach(plugin)

foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin})
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
endforeach(ffi_plugin)
93 changes: 28 additions & 65 deletions lib/easy_image_viewer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

import 'src/easy_image_provider.dart';
import 'src/easy_image_view_pager.dart';
import 'src/easy_image_viewer_dismissible_dialog.dart';
import 'src/single_image_provider.dart';

export 'src/easy_image_provider.dart' show EasyImageProvider;
Expand All @@ -25,26 +25,27 @@ const _defaultCloseButtonTooltip = 'Close';
/// Setting [immersive] to false will prevent the top and bottom bars from being hidden.
/// The optional [onViewerDismissed] callback function is called when the dialog is closed.
/// The optional [useSafeArea] boolean defaults to false and is passed to [showDialog].
/// The optional [swipeDismissible] boolean defaults to false allows swipe-down-to-dismiss.
/// 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.
/// The [closeButtonColor] defaults to white, but can be set to any other color.
Future<Dialog?> showImageViewer(
BuildContext context, ImageProvider imageProvider,
Future<Dialog?> showImageViewer(BuildContext context, ImageProvider imageProvider,
{bool immersive = true,
void Function()? onViewerDismissed,
bool useSafeArea = false,
bool swipeDismissible = false,
Color backgroundColor = _defaultBackgroundColor,
String closeButtonTooltip = _defaultCloseButtonTooltip,
Color closeButtonColor = _defaultCloseButtonColor}) {
return showImageViewerPager(context, SingleImageProvider(imageProvider),
immersive: immersive,
onViewerDismissed:
onViewerDismissed != null ? (_) => onViewerDismissed() : null,
useSafeArea: useSafeArea,
backgroundColor: backgroundColor,
closeButtonTooltip: closeButtonTooltip,
closeButtonColor: closeButtonColor);
immersive: immersive,
onViewerDismissed: onViewerDismissed != null ? (_) => onViewerDismissed() : null,
useSafeArea: useSafeArea,
swipeDismissible: swipeDismissible,
backgroundColor: backgroundColor,
closeButtonTooltip: closeButtonTooltip,
closeButtonColor: closeButtonColor);
}

/// Shows the images provided by the [imageProvider] in a full-screen PageView [Dialog].
Expand All @@ -54,16 +55,17 @@ Future<Dialog?> showImageViewer(
/// The optional [onViewerDismissed] callback function is called with the index of
/// the image that is displayed when the dialog is closed.
/// The optional [useSafeArea] boolean defaults to false and is passed to [showDialog].
/// The optional [swipeDismissible] boolean defaults to false allows swipe-down-to-dismiss.
/// 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.
/// The [closeButtonColor] defaults to white, but can be set to any other color.
Future<Dialog?> showImageViewerPager(
BuildContext context, EasyImageProvider imageProvider,
Future<Dialog?> showImageViewerPager(BuildContext context, EasyImageProvider imageProvider,
{bool immersive = true,
void Function(int)? onPageChanged,
void Function(int)? onViewerDismissed,
bool useSafeArea = false,
bool swipeDismissible = false,
Color backgroundColor = _defaultBackgroundColor,
String closeButtonTooltip = _defaultCloseButtonTooltip,
Color closeButtonColor = _defaultCloseButtonColor}) {
Expand All @@ -72,57 +74,18 @@ Future<Dialog?> showImageViewerPager(
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
}

void Function()? internalPageChangeListener;
final pageController =
PageController(initialPage: imageProvider.initialIndex);

if (onPageChanged != null) {
internalPageChangeListener = () {
onPageChanged(pageController.page?.round() ?? 0);
};
pageController.addListener(internalPageChangeListener);
}

return showDialog<Dialog>(
context: context,
useSafeArea: useSafeArea,
builder: (context) {
return Dialog(
backgroundColor: backgroundColor,
insetPadding: const EdgeInsets.all(0),
child: Stack(
clipBehavior: Clip.none,
alignment: Alignment.center,
children: <Widget>[
EasyImageViewPager(
easyImageProvider: imageProvider,
pageController: pageController),
Positioned(
top: 5,
right: 5,
child: IconButton(
icon: const Icon(Icons.close),
color: closeButtonColor,
tooltip: closeButtonTooltip,
onPressed: () {
Navigator.of(context).pop();

if (onViewerDismissed != null) {
onViewerDismissed(
pageController.page?.round() ?? 0);
}

if (immersive) {
SystemChrome.setEnabledSystemUIMode(
SystemUiMode.edgeToEdge);
}
if (internalPageChangeListener != null) {
pageController
.removeListener(internalPageChangeListener);
}
pageController.dispose();
},
))
]));
});
}
context: context,
useSafeArea: useSafeArea,
builder: (context) {
return EasyImageViewerDismissibleDialog(imageProvider,
immersive: immersive,
onPageChanged: onPageChanged,
onViewerDismissed: onViewerDismissed,
useSafeArea: useSafeArea,
swipeDismissible: swipeDismissible,
backgroundColor: backgroundColor,
closeButtonColor: closeButtonColor,
closeButtonTooltip: closeButtonTooltip);
});
}
21 changes: 12 additions & 9 deletions lib/src/easy_image_view_pager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,40 +17,43 @@ class EasyImageViewPager extends StatefulWidget {
final EasyImageProvider easyImageProvider;
final PageController pageController;

/// Callback for when the scale has changed, only invoked at the end of
/// an interaction.
final void Function(double)? onScaleChanged;

/// Create new instance, using the [easyImageProvider] to populate the [PageView],
/// and the [pageController] to control the initial image index to display.
const EasyImageViewPager(
{Key? key, required this.easyImageProvider, required this.pageController})
{Key? key, required this.easyImageProvider, required this.pageController, this.onScaleChanged})
: super(key: key);

@override
_EasyImageViewPagerState createState() =>
// ignore: no_logic_in_create_state
_EasyImageViewPagerState(pageController: pageController);
_EasyImageViewPagerState createState() => _EasyImageViewPagerState();
}

class _EasyImageViewPagerState extends State<EasyImageViewPager> {
final PageController pageController;
bool _pagingEnabled = true;

_EasyImageViewPagerState({required this.pageController}) : super();

@override
Widget build(BuildContext context) {
return PageView.builder(
physics: _pagingEnabled
? const PageScrollPhysics()
: const NeverScrollableScrollPhysics(),
key: const Key('easy_image_view_page_view'),
key: GlobalObjectKey(widget.easyImageProvider),
itemCount: widget.easyImageProvider.imageCount,
controller: pageController,
controller: widget.pageController,
scrollBehavior: MouseEnabledScrollBehavior(),
itemBuilder: (context, index) {
final image = widget.easyImageProvider.imageBuilder(context, index);
return EasyImageView(
key: Key('easy_image_view_$index'),
imageProvider: image,
onScaleChanged: (scale) {
if (widget.onScaleChanged != null) {
widget.onScaleChanged!(scale);
}

setState(() {
_pagingEnabled = scale <= 1.0;
});
Expand Down
126 changes: 126 additions & 0 deletions lib/src/easy_image_viewer_dismissible_dialog.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

import 'easy_image_provider.dart';
import 'easy_image_view_pager.dart';

/// An internal widget that is used to hold a state to activate/deactivate the ability to
/// swipe-to-dismiss. This needs to be tied to the zoom scale of the current image, since
/// the user needs to be able to pan around on a zoomed-in image without triggering the
/// swipe-to-dismiss gesture.
class EasyImageViewerDismissibleDialog extends StatefulWidget {
final EasyImageProvider imageProvider;
final bool immersive;
final void Function(int)? onPageChanged;
final void Function(int)? onViewerDismissed;
final bool useSafeArea;
final bool swipeDismissible;
final Color backgroundColor;
final String closeButtonTooltip;
final Color closeButtonColor;

/// Refer to [showImageViewerPager] for the arguments
const EasyImageViewerDismissibleDialog(this.imageProvider, {
Key? key,
this.immersive = true,
this.onPageChanged,
this.onViewerDismissed,
this.useSafeArea = false,
this.swipeDismissible = false,
required this.backgroundColor,
required this.closeButtonTooltip,
required this.closeButtonColor}) : super(key: key);

@override
State<EasyImageViewerDismissibleDialog> createState() => _EasyImageViewerDismissibleDialogState();
}

class _EasyImageViewerDismissibleDialogState extends State<EasyImageViewerDismissibleDialog> {

/// This is used to either activate or deactivate the ability to swipe-to-dismissed, based on
/// whether the current image is zoomed in (scale > 0) or not.
DismissDirection _dismissDirection = DismissDirection.down;
void Function()? _internalPageChangeListener;
late final PageController _pageController;

@override
void initState() {
super.initState();
_pageController = PageController(initialPage: widget.imageProvider.initialIndex);
if (widget.onPageChanged != null) {
_internalPageChangeListener = () {
widget.onPageChanged!(_pageController.page?.round() ?? 0);
};
_pageController.addListener(_internalPageChangeListener!);
}
}

@override
void dispose() {
if (_internalPageChangeListener != null) {
_pageController.removeListener(_internalPageChangeListener!);
}
_pageController.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
final dialog = Dialog(
backgroundColor: widget.backgroundColor,
insetPadding: const EdgeInsets.all(0),
child: Stack(clipBehavior: Clip.none, alignment: Alignment.center, children: <Widget>[
EasyImageViewPager(easyImageProvider: widget.imageProvider, pageController: _pageController, onScaleChanged: (scale) {
setState(() {
_dismissDirection = scale <= 1.0 ? DismissDirection.down : DismissDirection.none;
});
}),
Positioned(
top: 5,
right: 5,
child: IconButton(
icon: const Icon(Icons.close),
color: widget.closeButtonColor,
tooltip: widget.closeButtonTooltip,
onPressed: () {
Navigator.of(context).pop();

if (widget.onViewerDismissed != null) {
widget.onViewerDismissed!(_pageController.page?.round() ?? 0);
}

if (widget.immersive) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
}
},
)
)
]
));

if (widget.swipeDismissible) {
return Dismissible(
direction: _dismissDirection,
resizeDuration: null,
confirmDismiss: (dir) async {
return true;
},
onDismissed: (_) {
Navigator.of(context).pop();

if (widget.onViewerDismissed != null) {
widget.onViewerDismissed!(_pageController.page?.round() ?? 0);
}

if (widget.immersive) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
}
},
key: const Key('dismissible_easy_image_viewer_dialog'),
child: dialog
);
} else {
return dialog;
}
}
}
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: easy_image_viewer
description: An easy image viewer with pinch & zoom, multi image, and built-in full-screen dialog support.
version: 1.0.4
version: 1.0.5
homepage: https://github.com/thesmythgroup/easy_image_viewer

environment:
Expand Down
Loading