diff --git a/lib/src/theme/wolt_modal_sheet_default_theme_data.dart b/lib/src/theme/wolt_modal_sheet_default_theme_data.dart index 56b63963..6ee5dc71 100644 --- a/lib/src/theme/wolt_modal_sheet_default_theme_data.dart +++ b/lib/src/theme/wolt_modal_sheet_default_theme_data.dart @@ -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; diff --git a/lib/src/theme/wolt_modal_sheet_theme_data.dart b/lib/src/theme/wolt_modal_sheet_theme_data.dart index ece5f15e..c657aab9 100644 --- a/lib/src/theme/wolt_modal_sheet_theme_data.dart +++ b/lib/src/theme/wolt_modal_sheet_theme_data.dart @@ -130,7 +130,31 @@ class WoltModalSheetThemeData extends ThemeExtension { /// 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. diff --git a/lib/src/widgets/wolt_modal_sheet_content_gesture_detector.dart b/lib/src/widgets/wolt_modal_sheet_drag_to_dismiss_detector.dart similarity index 77% rename from lib/src/widgets/wolt_modal_sheet_content_gesture_detector.dart rename to lib/src/widgets/wolt_modal_sheet_drag_to_dismiss_detector.dart index 21386d4f..9fc106de 100644 --- a/lib/src/widgets/wolt_modal_sheet_content_gesture_detector.dart +++ b/lib/src/widgets/wolt_modal_sheet_drag_to_dismiss_detector.dart @@ -1,12 +1,11 @@ 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, @@ -14,7 +13,6 @@ class WoltModalSheetContentGestureDetector extends StatelessWidget { final WoltModalType modalType; final Widget child; - final bool enableDrag; final WoltModalSheetRoute route; final VoidCallback? onModalDismissedWithDrag; final GlobalKey modalContentKey; @@ -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; @@ -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, + ), ); } diff --git a/lib/src/wolt_modal_sheet.dart b/lib/src/wolt_modal_sheet.dart index 02a089d0..064b6bd5 100644 --- a/lib/src/wolt_modal_sheet.dart +++ b/lib/src/wolt_modal_sheet.dart @@ -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'; /// Signature for a function that builds a list of [SliverWoltModalSheetPage] based on the given [BuildContext]. @@ -85,7 +85,6 @@ class WoltModalSheet 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 animationCurve = decelerateEasing; @override State createState() => WoltModalSheetState(); @@ -356,39 +355,42 @@ class WoltModalSheetState extends State { 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,