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

Enable drag to dismiss modal if the scroll position is at top edge #283

Merged
merged 4 commits into from
Aug 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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
28 changes: 28 additions & 0 deletions lib/src/theme/wolt_modal_sheet_default_theme_data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,34 @@ class WoltModalSheetDefaultThemeData extends WoltModalSheetThemeData {
@override
bool get useSafeArea => true;

/// [mainContentScrollPhysics] sets the scrolling behavior for the main content area of the
/// WoltModalSheet, defaulting to [ClampingScrollPhysics]. This physics type is chosen for
/// several key reasons:
///
/// 1. **Prevent Overscroll:** ClampingScrollPhysics stops the scrollable content from moving
/// beyond the viewport's bounds. This clear boundary is crucial for drag-to-dismiss feature,
/// ensuring that any drag beyond the scroll limit is recognized as an intent to dismiss the
/// modal.
///
/// 2. **Clear Interaction Boundaries:** By preventing the content from bouncing or scrolling
/// past the edge, users receive clear feedback that reaching the end of the scrollable area can
/// transition to other interactions, like closing the modal. This helps avoid confusion
/// between scrolling and modal dismissal gestures.
///
/// 3. **Simplify Gesture Detection:** Using ClampingScrollPhysics simplifies the detection of
/// user gestures, differentiating more reliably between scrolling and actions intended to
/// dismiss the modal. This reduces the complexity and potential errors in handling these
/// interactions.
///
/// Choosing alternative scroll physics like [BouncingScrollPhysics] or [ElasticScrollPhysics]
/// could disrupt the drag-to-dismiss feature. These physics allow content to move beyond
/// scroll limits, which can interfere with gesture recognition, making it unclear whether a
/// gesture is intended for scrolling or dismissing the modal. As a result, drag-to-dismiss
/// would only be functional with a custom drag handle, limiting interaction flexibility on
/// main content area.
@override
ScrollPhysics? get mainContentScrollPhysics => const ClampingScrollPhysics();

@override
WoltModalTypeBuilder get modalTypeBuilder => (context) {
final width = MediaQuery.sizeOf(context).width;
Expand Down
26 changes: 25 additions & 1 deletion lib/src/theme/wolt_modal_sheet_theme_data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,31 @@ class WoltModalSheetThemeData extends ThemeExtension<WoltModalSheetThemeData> {
/// If null, [WoltModalSheet] uses [Clip.antiAliasWithSaveLayer].
final Clip? clipBehavior;

/// The default value for [WoltModalSheet] scrollPhysics in the main content.
/// [mainContentScrollPhysics] sets the scrolling behavior for the main content area of the
/// WoltModalSheet, defaulting to [ClampingScrollPhysics]. This physics type is chosen for
/// several key reasons:
///
/// 1. **Prevent Overscroll:** ClampingScrollPhysics stops the scrollable content from moving
/// beyond the viewport's bounds. This clear boundary is crucial for drag-to-dismiss feature,
/// ensuring that any drag beyond the scroll limit is recognized as an intent to dismiss the
/// modal.
///
/// 2. **Clear Interaction Boundaries:** By preventing the content from bouncing or scrolling
/// past the edge, users receive clear feedback that reaching the end of the scrollable area can
/// transition to other interactions, like closing the modal. This helps avoid confusion
/// between scrolling and modal dismissal gestures.
///
/// 3. **Simplify Gesture Detection:** Using ClampingScrollPhysics simplifies the detection of
/// user gestures, differentiating more reliably between scrolling and actions intended to
/// dismiss the modal. This reduces the complexity and potential errors in handling these
/// interactions.
///
/// Choosing alternative scroll physics like [BouncingScrollPhysics] or [ElasticScrollPhysics]
/// could disrupt the drag-to-dismiss feature. These physics allow content to move beyond
/// scroll limits, which can interfere with gesture recognition, making it unclear whether a
/// gesture is intended for scrolling or dismissing the modal. As a result, drag-to-dismiss
/// would only be functional with a custom drag handle, limiting interaction flexibility on
/// main content area.
final ScrollPhysics? mainContentScrollPhysics;

/// Motion animation styles for both pagination and scrolling animations.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
import 'package:flutter/material.dart';
import 'package:wolt_modal_sheet/wolt_modal_sheet.dart';

class WoltModalSheetContentGestureDetector extends StatelessWidget {
const WoltModalSheetContentGestureDetector({
class WoltModalSheetDragToDismissDetector extends StatelessWidget {
const WoltModalSheetDragToDismissDetector({
super.key,
required this.child,
required this.modalType,
required this.enableDrag,
required this.onModalDismissedWithDrag,
required this.modalContentKey,
required this.route,
});

final WoltModalType modalType;
final Widget child;
final bool enableDrag;
final WoltModalSheetRoute route;
final VoidCallback? onModalDismissedWithDrag;
final GlobalKey modalContentKey;
Expand All @@ -26,11 +24,6 @@ class WoltModalSheetContentGestureDetector extends StatelessWidget {

double get _minFlingVelocity => modalType.minFlingVelocity;

bool get canDragToDismiss =>
enableDrag &&
_dismissDirection != null &&
_dismissDirection != WoltModalDismissDirection.none;

bool get _isDismissUnderway =>
_animationController.status == AnimationStatus.reverse;

Expand All @@ -45,24 +38,45 @@ class WoltModalSheetContentGestureDetector extends StatelessWidget {

@override
Widget build(BuildContext context) {
final isVertical = _dismissDirection?.isVertical ?? false;
final isHorizontal = _dismissDirection?.isHorizontal ?? false;

return GestureDetector(
excludeFromSemantics: true,
onVerticalDragUpdate: (details) => canDragToDismiss && isVertical
? _handleVerticalDragUpdate(details)
: null,
onVerticalDragEnd: (details) => canDragToDismiss && isVertical
? _handleVerticalDragEnd(context, details)
: null,
onHorizontalDragUpdate: (details) => canDragToDismiss && isHorizontal
? _handleHorizontalDragUpdate(context, details)
: null,
onHorizontalDragEnd: (details) => canDragToDismiss && isHorizontal
? _handleHorizontalDragEnd(context, details)
: null,
child: child,
final isVerticalDismissAllowed = _dismissDirection?.isVertical ?? false;
final isHorizontalDismissAllowed = _dismissDirection?.isHorizontal ?? false;

return NotificationListener(
onNotification: (notification) {
if (notification is OverscrollNotification &&
notification.dragDetails != null) {
if (isVerticalDismissAllowed) {
_handleVerticalDragUpdate(notification.dragDetails!);
} else if (isHorizontalDismissAllowed) {
_handleHorizontalDragUpdate(context, notification.dragDetails!);
}
}
if (notification is ScrollEndNotification &&
notification.dragDetails != null) {
if (isVerticalDismissAllowed) {
_handleVerticalDragEnd(context, notification.dragDetails!);
} else if (isHorizontalDismissAllowed) {
_handleHorizontalDragEnd(context, notification.dragDetails!);
}
}
return true;
},
child: GestureDetector(
excludeFromSemantics: true,
onVerticalDragUpdate: (details) => isVerticalDismissAllowed
? _handleVerticalDragUpdate(details)
: null,
onVerticalDragEnd: (details) => isVerticalDismissAllowed
? _handleVerticalDragEnd(context, details)
: null,
onHorizontalDragUpdate: (details) => isHorizontalDismissAllowed
? _handleHorizontalDragUpdate(context, details)
: null,
onHorizontalDragEnd: (details) => isHorizontalDismissAllowed
? _handleHorizontalDragEnd(context, details)
: null,
child: child,
),
);
}

Expand Down
62 changes: 32 additions & 30 deletions lib/src/wolt_modal_sheet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import 'package:wolt_modal_sheet/src/content/wolt_modal_sheet_animated_switcher.
import 'package:wolt_modal_sheet/src/theme/wolt_modal_sheet_default_theme_data.dart';
import 'package:wolt_modal_sheet/src/utils/wolt_modal_type_utils.dart';
import 'package:wolt_modal_sheet/src/widgets/wolt_animated_modal_barrier.dart';
import 'package:wolt_modal_sheet/src/widgets/wolt_modal_sheet_content_gesture_detector.dart';
import 'package:wolt_modal_sheet/src/widgets/wolt_modal_sheet_drag_to_dismiss_detector.dart';
import 'package:wolt_modal_sheet/wolt_modal_sheet.dart';

const int defaultWoltModalTransitionAnimationDuration = 350;
Expand Down Expand Up @@ -87,7 +87,6 @@ class WoltModalSheet<T> extends StatefulWidget {
/// A boolean that determines whether the modal should avoid system UI intrusions such as the
/// notch and system gesture areas.
final bool? useSafeArea;
static const ParametricCurve<double> animationCurve = decelerateEasing;

@override
State<WoltModalSheet> createState() => WoltModalSheetState();
Expand Down Expand Up @@ -358,39 +357,42 @@ class WoltModalSheetState extends State<WoltModalSheet> {
key: _childKey,
child: Semantics(
label: modalType.routeLabel(context),
child: WoltModalSheetContentGestureDetector(
route: widget.route,
enableDrag: enableDrag,
modalContentKey: _childKey,
onModalDismissedWithDrag: widget.onModalDismissedWithDrag,
modalType: modalType,
child: Material(
color: pageBackgroundColor,
elevation: modalElevation,
surfaceTintColor: surfaceTintColor,
shadowColor: shadowColor,
shape: modalType.shapeBorder,
clipBehavior: clipBehavior,
child: LayoutBuilder(
builder: (_, constraints) {
return modalType.decoratePageContent(
context,
WoltModalSheetAnimatedSwitcher(
woltModalType: modalType,
pageIndex: currentPageIndex,
pages: pages,
sheetWidth: constraints.maxWidth,
showDragHandle: showDragHandle,
),
useSafeArea,
);
},
),
child: Material(
color: pageBackgroundColor,
elevation: modalElevation,
surfaceTintColor: surfaceTintColor,
shadowColor: shadowColor,
shape: modalType.shapeBorder,
clipBehavior: clipBehavior,
child: LayoutBuilder(
builder: (_, constraints) {
return modalType.decoratePageContent(
context,
WoltModalSheetAnimatedSwitcher(
woltModalType: modalType,
pageIndex: currentPageIndex,
pages: pages,
sheetWidth: constraints.maxWidth,
showDragHandle: showDragHandle,
),
useSafeArea,
);
},
),
),
),
);

if (enableDrag) {
pageContent = WoltModalSheetDragToDismissDetector(
route: widget.route,
modalContentKey: _childKey,
onModalDismissedWithDrag: widget.onModalDismissedWithDrag,
modalType: modalType,
child: pageContent,
);
}

final multiChildLayout = CustomMultiChildLayout(
delegate: _WoltModalMultiChildLayoutDelegate(
contentLayoutId: contentLayoutId,
Expand Down