diff --git a/lib/src/on_drag_wrapper.dart b/lib/src/on_drag_wrapper.dart index fbc45c8..552ae43 100644 --- a/lib/src/on_drag_wrapper.dart +++ b/lib/src/on_drag_wrapper.dart @@ -3,12 +3,10 @@ import 'package:flutter/material.dart'; class OnDragWrapper extends StatelessWidget { final Widget child; final Function(double) dragUpdate; - final VoidCallback dragStart; final VoidCallback dragEnd; OnDragWrapper( {Key? key, - required this.dragStart, required this.dragEnd, required this.child, required this.dragUpdate}) @@ -17,9 +15,6 @@ class OnDragWrapper extends StatelessWidget { @override Widget build(BuildContext context) { return GestureDetector( - onVerticalDragStart: (_) { - this.dragStart(); - }, onVerticalDragEnd: (_) { this.dragEnd(); }, diff --git a/lib/src/scroll_controller_override.dart b/lib/src/scroll_controller_override.dart new file mode 100644 index 0000000..563e538 --- /dev/null +++ b/lib/src/scroll_controller_override.dart @@ -0,0 +1,121 @@ +import 'package:flutter/widgets.dart'; +import 'package:snapping_sheet/snapping_sheet.dart'; +import 'package:snapping_sheet/src/sheet_size_calculator.dart'; +import 'package:snapping_sheet/src/snapping_calculator.dart'; + +class ScrollControllerOverride extends StatefulWidget { + final SheetSizeCalculator sizeCalculator; + final ScrollController scrollController; + final SheetLocation sheetLocation; + final Widget child; + + final Function(double) dragUpdate; + final VoidCallback dragEnd; + final double currentPosition; + final SnappingCalculator snappingCalculator; + + ScrollControllerOverride({ + required this.sizeCalculator, + required this.scrollController, + required this.dragUpdate, + required this.dragEnd, + required this.currentPosition, + required this.snappingCalculator, + required this.child, + required this.sheetLocation, + }); + + @override + _ScrollControllerOverrideState createState() => + _ScrollControllerOverrideState(); +} + +class _ScrollControllerOverrideState extends State { + DragDirection? _currentDragDirection; + + @override + void initState() { + super.initState(); + widget.scrollController.removeListener(_onScrollUpdate); + widget.scrollController.addListener(_onScrollUpdate); + } + + @override + void dispose() { + widget.scrollController.removeListener(_onScrollUpdate); + super.dispose(); + } + + void _onScrollUpdate() { + if (!_allowScrolling) _lockScrollPosition(widget.scrollController); + } + + void _overrideScroll(double dragAmount) { + if (!_allowScrolling) widget.dragUpdate(dragAmount); + } + + bool get _allowScrolling { + if (widget.sheetLocation == SheetLocation.below) { + if (_currentDragDirection == DragDirection.up) { + if (widget.currentPosition >= _biggestSnapPos) + return true; + else + return false; + } + if (_currentDragDirection == DragDirection.down) { + if (widget.scrollController.position.pixels > 0) return true; + if (widget.currentPosition <= _smallestSnapPos) + return true; + else + return false; + } + } + + if (widget.sheetLocation == SheetLocation.above) { + if (_currentDragDirection == DragDirection.down) { + if (widget.currentPosition <= _smallestSnapPos) { + return true; + } else + return false; + } + if (_currentDragDirection == DragDirection.up) { + if (widget.scrollController.position.pixels > 0) return true; + if (widget.currentPosition >= _biggestSnapPos) + return true; + else + return false; + } + } + + return false; + } + + double get _biggestSnapPos => + widget.snappingCalculator.getBiggestPositionPixels(); + double get _smallestSnapPos => + widget.snappingCalculator.getSmallestPositionPixels(); + + void _lockScrollPosition(ScrollController controller) { + controller.position.setPixels(0); + } + + void _setDragDirection(double dragAmount) { + this._currentDragDirection = + dragAmount > 0 ? DragDirection.down : DragDirection.up; + print(this._currentDragDirection); + } + + @override + Widget build(BuildContext context) { + return Listener( + onPointerMove: (dragEvent) { + _setDragDirection(dragEvent.delta.dy); + _overrideScroll(dragEvent.delta.dy); + }, + onPointerUp: (_) { + widget.dragEnd(); + }, + child: widget.child, + ); + } +} diff --git a/lib/src/sheet_content_wrapper.dart b/lib/src/sheet_content_wrapper.dart index 06fee85..ca25662 100644 --- a/lib/src/sheet_content_wrapper.dart +++ b/lib/src/sheet_content_wrapper.dart @@ -1,38 +1,71 @@ import 'package:flutter/widgets.dart'; import 'package:snapping_sheet/src/on_drag_wrapper.dart'; +import 'package:snapping_sheet/src/scroll_controller_override.dart'; import 'package:snapping_sheet/src/sheet_size_calculator.dart'; +import 'package:snapping_sheet/src/snapping_calculator.dart'; import 'package:snapping_sheet/src/snapping_sheet_content.dart'; -class SheetContentWrapper extends StatelessWidget { +class SheetContentWrapper extends StatefulWidget { final SheetSizeCalculator sizeCalculator; final SnappingSheetContent? sheetData; final Function(double) dragUpdate; - final VoidCallback dragStart; final VoidCallback dragEnd; + final double currentPosition; + final SnappingCalculator snappingCalculator; const SheetContentWrapper( {Key? key, required this.sheetData, required this.sizeCalculator, + required this.currentPosition, + required this.snappingCalculator, required this.dragUpdate, - required this.dragStart, required this.dragEnd}) : super(key: key); - Widget _wrapSheetDataWithDraggable() { - if (!sheetData!.draggable) return sheetData!; + @override + _SheetContentWrapperState createState() => _SheetContentWrapperState(); +} + +class _SheetContentWrapperState extends State { + Widget _wrapWithDragWrapper(Widget child) { return OnDragWrapper( - dragStart: dragStart, - dragEnd: dragEnd, - child: sheetData!, - dragUpdate: dragUpdate, + dragEnd: widget.dragEnd, + child: child, + dragUpdate: widget.dragUpdate, ); } + Widget _wrapWithScrollControllerOverride(Widget child) { + return ScrollControllerOverride( + sizeCalculator: widget.sizeCalculator, + scrollController: widget.sheetData!.childScrollController!, + dragUpdate: widget.dragUpdate, + dragEnd: widget.dragEnd, + currentPosition: widget.currentPosition, + snappingCalculator: widget.snappingCalculator, + sheetLocation: widget.sheetData!.location, + child: child, + ); + } + + Widget _wrapWithNecessaryWidgets(Widget child) { + Widget wrappedChild = child; + if (widget.sheetData!.draggable) { + wrappedChild = _wrapWithDragWrapper(wrappedChild); + } + if (widget.sheetData!.childScrollController != null) { + wrappedChild = _wrapWithScrollControllerOverride(wrappedChild); + } + return wrappedChild; + } + @override Widget build(BuildContext context) { - if (sheetData == null) return SizedBox(); - return sizeCalculator.positionWidget(child: _wrapSheetDataWithDraggable()); + if (widget.sheetData == null) return SizedBox(); + return widget.sizeCalculator.positionWidget( + child: _wrapWithNecessaryWidgets(widget.sheetData!.child), + ); } } diff --git a/lib/src/snapping_calculator.dart b/lib/src/snapping_calculator.dart index 42dddfb..7b802e8 100644 --- a/lib/src/snapping_calculator.dart +++ b/lib/src/snapping_calculator.dart @@ -69,6 +69,9 @@ class SnappingCalculator { this.grabbingHeight, ); + // We have a perfect match. Often happens when overflow drag. + if (posPixels == currentPosition) return true; + bool isAbove = posPixels > currentPosition; if (isAbove && dragDirection == DragDirection.down) return false; diff --git a/lib/src/snapping_position.dart b/lib/src/snapping_position.dart index 45686d9..28dbf4b 100644 --- a/lib/src/snapping_position.dart +++ b/lib/src/snapping_position.dart @@ -52,4 +52,10 @@ class SnappingPosition { if (this._positionPixel != null) return this._positionPixel!; return this._positionFactor! * maxHeight; } + + bool operator ==(other) => + other is SnappingPosition && + other._positionFactor == this._positionFactor && + other._positionPixel == this._positionPixel; + int get hashCode => _positionFactor.hashCode ^ _positionPixel.hashCode; } diff --git a/lib/src/snapping_sheet_content.dart b/lib/src/snapping_sheet_content.dart index f8e566f..ffe6b61 100644 --- a/lib/src/snapping_sheet_content.dart +++ b/lib/src/snapping_sheet_content.dart @@ -1,36 +1,45 @@ import 'package:flutter/widgets.dart'; import 'sheet_size_behaviors.dart'; -enum SnappingSheetContentSize { - // The size of the sheet content changes to the available height - dynamicSize, - - /// The size is static and do not change when the sheet is dragged - staticSize, +enum SheetLocation { + above, + below, + unknown, } -class SnappingSheetContent extends StatelessWidget { +class SnappingSheetContent { + /// The size behavior of the sheet. Can either be [SheetSizeStatic] or + /// [SheetSizeDynamic]. final SheetSizeBehavior sizeBehavior; - final Widget child; + + /// When given a scroll controller that is attached to scrollable view, e.g + /// [ListView] or a [SingleChildScrollView], the sheet will naturally grow + /// and shrink according to the current scroll position of that view. + /// + /// OBS, the scrollable view needs to have the [reverse] parameter set to + /// false if located in the below sheet and true if located in the above + /// sheet. Otherwise, the logic wont behave as intended. + final ScrollController? childScrollController; final bool draggable; + Widget _child; + SheetLocation location = SheetLocation.unknown; SnappingSheetContent({ - Key? key, - required this.child, + required Widget child, this.draggable = false, this.sizeBehavior = const SheetSizeDynamic(), - }) : super(key: key); + this.childScrollController, + }) : this._child = child; double? _getHeight() { var sizeBehavior = this.sizeBehavior; if (sizeBehavior is SheetSizeStatic) return sizeBehavior.height; } - @override - Widget build(BuildContext context) { + Widget get child { return SizedBox( height: _getHeight(), - child: this.child, + child: this._child, ); } } diff --git a/lib/src/snapping_sheet_widget.dart b/lib/src/snapping_sheet_widget.dart index d6c4632..d38a9c0 100644 --- a/lib/src/snapping_sheet_widget.dart +++ b/lib/src/snapping_sheet_widget.dart @@ -63,6 +63,7 @@ class _SnappingSheetState extends State @override void initState() { super.initState(); + _setSheetLocationData(); _lastSnappingPosition = _initSnappingPosition; _animationController = AnimationController(vsync: this); @@ -98,6 +99,19 @@ class _SnappingSheetState extends State super.dispose(); } + @override + void didUpdateWidget(covariant SnappingSheet oldWidget) { + super.didUpdateWidget(oldWidget); + _setSheetLocationData(); + } + + void _setSheetLocationData() { + if (widget.sheetAbove != null) + widget.sheetAbove!.location = SheetLocation.above; + if (widget.sheetBelow != null) + widget.sheetBelow!.location = SheetLocation.below; + } + set _currentPosition(double newPosition) { widget.onSheetMoved?.call(_currentPosition); _currentPositionPrivate = newPosition; @@ -112,12 +126,7 @@ class _SnappingSheetState extends State double _getNewPosition(double dragAmount) { var newPosition = _currentPosition - dragAmount; if (widget.lockOverflowDrag) { - var calculator = SnappingCalculator( - allSnappingPositions: widget.snappingPositions, - lastSnappingPosition: _lastSnappingPosition, - maxHeight: _latestConstraints!.maxHeight, - grabbingHeight: widget.grabbingHeight, - currentPosition: _currentPosition); + var calculator = _getSnappingCalculator(); var maxPos = calculator.getBiggestPositionPixels(); var minPos = calculator.getSmallestPositionPixels(); if (newPosition > maxPos) return maxPos; @@ -127,26 +136,20 @@ class _SnappingSheetState extends State } void _dragSheet(double dragAmount) { + if (_animationController.isAnimating) { + _animationController.stop(); + } setState(() { _currentPosition = _getNewPosition(dragAmount); }); } void _dragEnd() { - var bestSnappingPosition = SnappingCalculator( - allSnappingPositions: widget.snappingPositions, - lastSnappingPosition: _lastSnappingPosition, - maxHeight: _latestConstraints!.maxHeight, - grabbingHeight: widget.grabbingHeight, - currentPosition: _currentPosition) - .getBestSnappingPosition(); + var bestSnappingPosition = + _getSnappingCalculator().getBestSnappingPosition(); _snapToPosition(bestSnappingPosition); } - void _dragStart() { - _animationController.stop(); - } - void _snapToPosition(SnappingPosition snappingPosition) { _animateToPosition(snappingPosition); _lastSnappingPosition = snappingPosition; @@ -171,6 +174,15 @@ class _SnappingSheetState extends State _animationController.forward(); } + SnappingCalculator _getSnappingCalculator() { + return SnappingCalculator( + allSnappingPositions: widget.snappingPositions, + lastSnappingPosition: _lastSnappingPosition, + maxHeight: _latestConstraints!.maxHeight, + grabbingHeight: widget.grabbingHeight, + currentPosition: _currentPosition); + } + @override Widget build(BuildContext context) { return LayoutBuilder( @@ -186,8 +198,9 @@ class _SnappingSheetState extends State // The above sheet content SheetContentWrapper( dragEnd: _dragEnd, - dragStart: _dragStart, dragUpdate: _dragSheet, + currentPosition: _currentPosition, + snappingCalculator: _getSnappingCalculator(), sizeCalculator: AboveSheetSizeCalculator( sheetData: widget.sheetAbove, currentPosition: _currentPosition, @@ -205,7 +218,6 @@ class _SnappingSheetState extends State height: widget.grabbingHeight, child: OnDragWrapper( dragEnd: _dragEnd, - dragStart: _dragStart, dragUpdate: _dragSheet, child: widget.grabbing, ), @@ -214,8 +226,9 @@ class _SnappingSheetState extends State // The below sheet content SheetContentWrapper( dragEnd: _dragEnd, - dragStart: _dragStart, dragUpdate: _dragSheet, + currentPosition: _currentPosition, + snappingCalculator: _getSnappingCalculator(), sizeCalculator: BelowSheetSizeCalculator( sheetData: widget.sheetBelow, currentPosition: _currentPosition, diff --git a/test/snapping_calculator_test.dart b/test/snapping_calculator_test.dart index bbb949e..187d684 100644 --- a/test/snapping_calculator_test.dart +++ b/test/snapping_calculator_test.dart @@ -204,6 +204,25 @@ void main() { }); }); + group('Edge cases.', () { + test("Testing when [currentPosition] = [snappingPosition]", () { + var localSnappingPositions = [ + SnappingPosition.pixels(positionPixels: 700), + SnappingPosition.pixels(positionPixels: 800), + SnappingPosition.pixels(positionPixels: 900), + SnappingPosition.pixels(positionPixels: 1000), + ]; + var next = SnappingCalculator( + allSnappingPositions: localSnappingPositions, + lastSnappingPosition: localSnappingPositions[2], + maxHeight: maxHeight, + grabbingHeight: grabbingHeight, + currentPosition: 1000) + .getBestSnappingPosition(); + expect(next, localSnappingPositions[3]); + }); + }); + group("Testing with only one snapping position", () { var localSnappingPositions = [ SnappingPosition.factor(positionFactor: 0.5),