Skip to content

Commit

Permalink
Changed the way we drag with a scroll view
Browse files Browse the repository at this point in the history
By leveraging raw events from Listener instead of GestureDetector batteling in the GestureArena we are able to be less dependent on the Notification events to controll the dragging of the sheet and we can instead use vertical drag gestures directly to control the sheet.

There is also added parameters to control the default scrollphysics of underlaying scrollable views, this lets us disable scrolling in the ScrollPhysics implementation from userspace.
  • Loading branch information
Bjarte Bore committed Jan 14, 2025
1 parent 3efabee commit 8aa6112
Show file tree
Hide file tree
Showing 2 changed files with 98 additions and 88 deletions.
173 changes: 85 additions & 88 deletions modal_bottom_sheet/lib/src/bottom_sheet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import 'dart:async';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:modal_bottom_sheet/src/utils/scroll_to_top_status_bar.dart';

import 'package:modal_bottom_sheet/src/utils/bottom_sheet_suspended_curve.dart';
Expand Down Expand Up @@ -48,6 +49,8 @@ class ModalBottomSheet extends StatefulWidget {
double? closeProgressThreshold,
@Deprecated('Use preventPopThreshold instead') double? willPopThreshold,
double? preventPopThreshold,
this.scrollPhysics,
this.scrollPhysicsBuilder,
}) : preventPopThreshold =
preventPopThreshold ?? willPopThreshold ?? _willPopThreshold,
closeProgressThreshold =
Expand Down Expand Up @@ -98,6 +101,13 @@ class ModalBottomSheet extends StatefulWidget {
///
final Widget child;

/// The default scroll physics for underlaying scroll views
/// When set, we are able to change to the NeverScrollableScrollPhysics() while
/// we are dragging the bottom sheet with scrollview
final ScrollPhysics? scrollPhysics;

final ScrollPhysics Function(bool canScroll, ScrollPhysics parent)? scrollPhysicsBuilder;

/// If true, the bottom sheet can be dragged up and down and dismissed by
/// swiping downwards.
///
Expand Down Expand Up @@ -163,7 +173,12 @@ class ModalBottomSheetState extends State<ModalBottomSheet>
// Detect if user is dragging.
// Used on NotificationListener to detect if ScrollNotifications are
// before or after the user stop dragging
bool isDragging = false;
bool _isDragging = false;

// Indicates if the scrollbar is scrollable
// when we start a drag event, we are always scrollable
bool _canScroll = true;
ScrollDirection _scrollDirection = ScrollDirection.idle;

bool get hasReachedWillPopThreshold =>
widget.animationController.value < _willPopThreshold;
Expand Down Expand Up @@ -194,7 +209,7 @@ class ModalBottomSheetState extends State<ModalBottomSheet>
}

void _close() {
isDragging = false;
_isDragging = false;
widget.onClosing();
}

Expand Down Expand Up @@ -226,8 +241,16 @@ class ModalBottomSheetState extends State<ModalBottomSheet>
animationCurve = Curves.linear;
assert(widget.enableDrag, 'Dragging is disabled');

if (_dismissUnderway) return;
isDragging = true;
if (_dismissUnderway) {
return;
}

// Abort if the scrollbar is scrollable
if (_canScroll && _scrollController.hasClients && _scrollController.position.maxScrollExtent > 0) {
return;
}

_isDragging = true;

final progress = primaryDelta / (_childHeight ?? primaryDelta);

Expand Down Expand Up @@ -262,8 +285,9 @@ class ModalBottomSheetState extends State<ModalBottomSheet>
curve: _defaultCurve,
);

if (_dismissUnderway || !isDragging) return;
isDragging = false;
if (_dismissUnderway || !_isDragging) return;
_isDragging = false;
_canScroll = true;
_bounceDragController.reverse();

Future<void> tryClose() async {
Expand Down Expand Up @@ -291,89 +315,52 @@ class ModalBottomSheetState extends State<ModalBottomSheet>
}
}

// As we cannot access the dragGesture detector of the scroll view
// we can not know the DragDownDetails and therefore the end velocity.
// VelocityTracker it is used to calculate the end velocity of the scroll
// when user is trying to close the modal by dragging
VelocityTracker? _velocityTracker;
DateTime? _startTime;
Curve get _defaultCurve => widget.animationCurve ?? _decelerateEasing;

void _handleScrollUpdate(ScrollNotification notification) {
assert(notification.context != null);
//Check if scrollController is used
if (!_scrollController.hasClients) return;
late final VerticalDragGestureRecognizer _verticalDragRecognizer;

ScrollPosition scrollPosition;
void _handleRawDragUpdate(DragUpdateDetails details) {
_handleDragUpdate(details.delta.dy);
}

if (_scrollController.positions.length > 1) {
scrollPosition = _scrollController.positions.firstWhere(
(p) => p.isScrollingNotifier.value,
orElse: () => _scrollController.positions.first);
} else {
scrollPosition = _scrollController.position;
}
void _handleRawDragEnd(DragEndDetails details) {
_handleDragEnd(details.primaryVelocity ?? 0);
}

if (scrollPosition.axis == Axis.horizontal) return;

final isScrollReversed = scrollPosition.axisDirection == AxisDirection.down;
final offset = isScrollReversed
? scrollPosition.pixels
: scrollPosition.maxScrollExtent - scrollPosition.pixels;

if (offset <= 0) {
// Clamping Scroll Physics end with a ScrollEndNotification with a DragEndDetail class
// while Bouncing Scroll Physics or other physics that Overflow don't return a drag end info

// We use the velocity from DragEndDetail in case it is available
if (notification is ScrollEndNotification) {
final dragDetails = notification.dragDetails;
if (dragDetails != null) {
_handleDragEnd(dragDetails.primaryVelocity ?? 0);
_velocityTracker = null;
_startTime = null;
return;
}
}
bool _handleScrollNotification(ScrollNotification notification) {

// Otherwise the calculate the velocity with a VelocityTracker
if (_velocityTracker == null) {
final pointerKind = defaultPointerDeviceKind(context);
_velocityTracker = VelocityTracker.withKind(pointerKind);
_startTime = DateTime.now();
}
if (notification is UserScrollNotification) {
_scrollDirection = notification.direction;
}

DragUpdateDetails? dragDetails;
if (notification is ScrollUpdateNotification) {
dragDetails = notification.dragDetails;
}
if (notification is OverscrollNotification) {
dragDetails = notification.dragDetails;
}
assert(_velocityTracker != null);
assert(_startTime != null);
final startTime = _startTime!;
final velocityTracker = _velocityTracker!;
if (dragDetails != null) {
final duration = startTime.difference(DateTime.now());
velocityTracker.addPosition(duration, Offset(0, offset));
_handleDragUpdate(dragDetails.delta.dy);
} else if (isDragging) {
final velocity = velocityTracker.getVelocity().pixelsPerSecond.dy;
_velocityTracker = null;
_startTime = null;
_handleDragEnd(velocity);
if (notification is OverscrollNotification || notification is ScrollUpdateNotification) {
final downwards = _scrollDirection == ScrollDirection.forward;
final atEdge = notification.metrics.atEdge;

// disable scrolling when we are at the edge
if (downwards && atEdge && _canScroll) {
setState(() {
_canScroll = false;
});
} else if (!downwards && atEdge && !_canScroll) {
setState(() {
_canScroll = true;
});
}
}
return false;
}

Curve get _defaultCurve => widget.animationCurve ?? _decelerateEasing;

@override
void initState() {
animationCurve = _defaultCurve;
_bounceDragController =
AnimationController(vsync: this, duration: Duration(milliseconds: 300));

_verticalDragRecognizer = _AllowMultipleVerticalDragGestureRecognizer();
_verticalDragRecognizer.onUpdate = _handleRawDragUpdate;
_verticalDragRecognizer.onEnd = _handleRawDragEnd;

// Todo: Check if we can remove scroll Controller
super.initState();
}
Expand Down Expand Up @@ -410,18 +397,12 @@ class ModalBottomSheetState extends State<ModalBottomSheet>
animation: bounceAnimation,
builder: (context, _) => CustomSingleChildLayout(
delegate: _CustomBottomSheetLayout(bounceAnimation.value),
child: GestureDetector(
onVerticalDragUpdate: (details) {
_handleDragUpdate(details.delta.dy);
},
onVerticalDragEnd: (details) {
_handleDragEnd(details.primaryVelocity ?? 0);
child: Listener(
onPointerDown: (event) {
_verticalDragRecognizer.addPointer(event);
},
child: NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification notification) {
_handleScrollUpdate(notification);
return false;
},
onNotification: _handleScrollNotification,
child: KeyedSubtree(
key: _contentKey,
child: child!,
Expand All @@ -432,12 +413,19 @@ class ModalBottomSheetState extends State<ModalBottomSheet>
),
);
return ClipRect(
child: CustomSingleChildLayout(
delegate: _ModalBottomSheetLayout(
animationValue,
widget.expanded,
child: ScrollConfiguration(
behavior: ScrollConfiguration.of(context).copyWith(
physics: widget.scrollPhysicsBuilder
?.call(_canScroll, ScrollConfiguration.of(context).getScrollPhysics(context))
?? widget.scrollPhysics,
),
child: CustomSingleChildLayout(
delegate: _ModalBottomSheetLayout(
animationValue,
widget.expanded,
),
child: draggableChild,
),
child: draggableChild,
),
);
},
Expand Down Expand Up @@ -534,3 +522,12 @@ PointerDeviceKind defaultPointerDeviceKind(BuildContext context) {
return PointerDeviceKind.unknown;
}
}


class _AllowMultipleVerticalDragGestureRecognizer extends VerticalDragGestureRecognizer{

@override
void rejectGesture(int pointer) {
acceptGesture(pointer);
}
}
13 changes: 13 additions & 0 deletions modal_bottom_sheet/lib/src/bottom_sheet_route.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ class _ModalBottomSheet<T> extends StatefulWidget {
this.expanded = false,
this.enableDrag = true,
this.animationCurve,
this.scrollPhysics,
this.scrollPhysicsBuilder,
});

final double? closeProgressThreshold;
Expand All @@ -25,6 +27,8 @@ class _ModalBottomSheet<T> extends StatefulWidget {
final bool enableDrag;
final AnimationController? secondAnimationController;
final Curve? animationCurve;
final ScrollPhysics? scrollPhysics;
final ScrollPhysics Function(bool canScroll, ScrollPhysics parent)? scrollPhysicsBuilder;

@override
_ModalBottomSheetState<T> createState() => _ModalBottomSheetState<T>();
Expand Down Expand Up @@ -123,6 +127,8 @@ class _ModalBottomSheetState<T> extends State<_ModalBottomSheet<T>> {
bounce: widget.bounce,
scrollController: scrollController,
animationCurve: widget.animationCurve,
scrollPhysics: widget.scrollPhysics,
scrollPhysicsBuilder: widget.scrollPhysicsBuilder,
),
);
},
Expand All @@ -148,6 +154,8 @@ class ModalSheetRoute<T> extends PageRoute<T> {
this.bounce = false,
this.animationCurve,
Duration? duration,
this.scrollPhysics,
this.scrollPhysicsBuilder,
super.settings,
}) : duration = duration ?? _bottomSheetDuration;

Expand All @@ -166,6 +174,9 @@ class ModalSheetRoute<T> extends PageRoute<T> {
final AnimationController? secondAnimationController;
final Curve? animationCurve;

final ScrollPhysics? scrollPhysics;
final ScrollPhysics Function(bool canScroll, ScrollPhysics parent)? scrollPhysicsBuilder;

@override
Duration get transitionDuration => duration;

Expand Down Expand Up @@ -215,6 +226,8 @@ class ModalSheetRoute<T> extends PageRoute<T> {
bounce: bounce,
enableDrag: enableDrag,
animationCurve: animationCurve,
scrollPhysics: scrollPhysics,
scrollPhysicsBuilder: scrollPhysicsBuilder,
),
);
return bottomSheet;
Expand Down

0 comments on commit 8aa6112

Please sign in to comment.