From e1c05bc0558afb2631af1c3e33e43414a0a71b19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= Date: Wed, 22 Jan 2025 20:28:52 +0100 Subject: [PATCH 1/6] reordered scrolling --- lib/utils/reorderable_scrollable.dart | 95 +++++++++++++++++++ lib/widgets/reorderable_builder.dart | 62 ++++-------- .../reorderable_scrolling_listener.dart | 56 ++--------- 3 files changed, 123 insertions(+), 90 deletions(-) create mode 100644 lib/utils/reorderable_scrollable.dart diff --git a/lib/utils/reorderable_scrollable.dart b/lib/utils/reorderable_scrollable.dart new file mode 100644 index 0000000..bc6e8be --- /dev/null +++ b/lib/utils/reorderable_scrollable.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; + +/// +class ReorderableScrollable { + static late BuildContext _context; + static late ScrollController? _scrollController; + + const ReorderableScrollable._(); + + static ReorderableScrollable of( + BuildContext context, { + required ScrollController? scrollController, + }) { + _context = context; + _scrollController = scrollController; + + return const ReorderableScrollable._(); + } + + /// Returning the current scroll position. + /// + /// There are two possibilities to get the scroll position. + /// + /// First one is, the returned child of [widget.builder] is a scrollable widget. + /// In this case, it is important that the [widget.scrollController] is added + /// to the scrollable widget to get the current scroll position. + /// + /// Another possibility is that one of the parents is scrollable. + /// In that case, the position of the scroll is accessible inside [context]. + /// + /// Otherwise, 0.0 will be returned. + Offset getScrollOffset({required bool reverse}) { + var scrollPosition = Scrollable.maybeOf(_context)?.position; + final scrollController = _scrollController; + + // For example, in cases where there are nested scrollable widgets + // like GridViews inside a parent scrollable widget, + // the widget assigned to the controller will be used for scroll calculations + if (scrollController != null && scrollController.hasClients) { + scrollPosition = scrollController.position; + } + + if (scrollPosition != null) { + final pixels = scrollPosition.pixels; + final isScrollingVertical = scrollPosition.axis == Axis.vertical; + final offset = Offset( + isScrollingVertical ? 0.0 : pixels, + isScrollingVertical ? pixels : 0.0, + ); + return reverse ? -offset : offset; + } + + return Offset.zero; + } + + /// No [ScrollController] means that this widget is already in a scrollable widget. + /// + /// [widget.scrollController] should be assigned if the scrollable widget + /// is rendered inside this widget e.g. in a [GridView]. + bool get isScrollOutside { + return _scrollController == null; + } + + Axis? get scrollDirection { + if (isScrollOutside) { + return Scrollable.of(_context).position.axis; + } else { + return _scrollController?.position.axis; + } + } + + double? get pixels { + if (isScrollOutside) { + return Scrollable.of(_context).position.pixels; + } else { + return _scrollController?.offset; + } + } + + double? get maxScrollExtent { + if (isScrollOutside) { + return Scrollable.of(_context).position.maxScrollExtent; + } else { + return _scrollController?.position.maxScrollExtent; + } + } + + void jumpTo({required double value}) { + if (isScrollOutside == true) { + Scrollable.of(_context).position.moveTo(value); + } else { + _scrollController?.jumpTo(value); + } + } +} diff --git a/lib/widgets/reorderable_builder.dart b/lib/widgets/reorderable_builder.dart index 160e81b..323befb 100644 --- a/lib/widgets/reorderable_builder.dart +++ b/lib/widgets/reorderable_builder.dart @@ -5,6 +5,7 @@ import 'package:flutter_reorderable_grid_view/controller/reorderable_drag_and_dr import 'package:flutter_reorderable_grid_view/controller/reorderable_item_builder_controller.dart'; import 'package:flutter_reorderable_grid_view/entities/released_reorderable_entity.dart'; import 'package:flutter_reorderable_grid_view/entities/reorderable_entity.dart'; +import 'package:flutter_reorderable_grid_view/utils/reorderable_scrollable.dart'; import 'package:flutter_reorderable_grid_view/widgets/reorderable_builder_item.dart'; import 'package:flutter_reorderable_grid_view/widgets/reorderable_scrolling_listener.dart'; @@ -293,7 +294,7 @@ class _ReorderableBuilderState extends State> if (orientationBefore != orientationAfter || screenSizeBefore != screenSizeAfter) { _reorderableController.handleDeviceOrientationChanged(); - _reorderableController.scrollOffset == _getScrollOffset(); + _reorderableController.scrollOffset == _scrollOffset; setState(() {}); } }); @@ -393,7 +394,7 @@ class _ReorderableBuilderState extends State> onCreated: _handleCreatedChild, releasedReorderableEntity: _reorderableController.releasedReorderableEntity, - scrollOffset: _getScrollOffset(), + scrollOffset: _scrollOffset, releasedChildDuration: widget.releasedChildDuration, enableDraggable: widget.enableDraggable && isDraggable, currentDraggedEntity: currentDraggedEntity, @@ -416,7 +417,7 @@ class _ReorderableBuilderState extends State> void _handleDragStarted(ReorderableEntity reorderableEntity) { _reorderableController.handleDragStarted( reorderableEntity: reorderableEntity, - currentScrollOffset: _getScrollOffset(), + currentScrollOffset: _scrollOffset, lockedIndices: widget.lockedIndices, isScrollableOutside: _isScrollOutside, ); @@ -432,7 +433,7 @@ class _ReorderableBuilderState extends State> if (hasUpdated) { // this fixes the issue when the user scrolls while dragging to get the updated scroll value - _reorderableController.scrollOffset = _getScrollOffset(); + _reorderableController.scrollOffset = _scrollOffset; // notifying about the new position of the dragged child final orderId = _reorderableController.draggedEntity!.updatedOrderId; @@ -458,7 +459,7 @@ class _ReorderableBuilderState extends State> // scrollable part is outside this widget if (_isScrollOutside) { - offset -= _getScrollOffset(); + offset -= _scrollOffset; } // call to ensure animation to dropped item @@ -548,7 +549,7 @@ class _ReorderableBuilderState extends State> offset = parentRenderObject.globalToLocal( renderBox.localToGlobal(Offset.zero), ); - offset += _getScrollOffset(); + offset += _scrollOffset; } } @@ -567,45 +568,18 @@ class _ReorderableBuilderState extends State> } } - /// Returning the current scroll position. - /// - /// There are two possibilities to get the scroll position. - /// - /// First one is, the returned child of [widget.builder] is a scrollable widget. - /// In this case, it is important that the [widget.scrollController] is added - /// to the scrollable widget to get the current scroll position. - /// - /// Another possibility is that one of the parents is scrollable. - /// In that case, the position of the scroll is accessible inside [context]. - /// - /// Otherwise, 0.0 will be returned. - Offset _getScrollOffset() { - var scrollPosition = Scrollable.maybeOf(context)?.position; - final scrollController = widget.scrollController; - - // For example, in cases where there are nested scrollable widgets - // like GridViews inside a parent scrollable widget, - // the widget assigned to the controller will be used for scroll calculations - if (scrollController != null && scrollController.hasClients) { - scrollPosition = scrollController.position; - } - - if (scrollPosition != null) { - final pixels = scrollPosition.pixels; - final isScrollingVertical = scrollPosition.axis == Axis.vertical; - final offset = Offset( - isScrollingVertical ? 0.0 : pixels, - isScrollingVertical ? pixels : 0.0, - ); - return widget.reverse ? -offset : offset; - } + Offset get _scrollOffset { + return _reorderableScrollable.getScrollOffset( + reverse: widget.reverse, + ); + } - return Offset.zero; + bool get _isScrollOutside { + return _reorderableScrollable.isScrollOutside; } - /// No [ScrollController] means that this widget is already in a scrollable widget. - /// - /// [widget.scrollController] should be assigned if the scrollable widget - /// is rendered inside this widget e.g. in a [GridView]. - bool get _isScrollOutside => widget.scrollController == null; + ReorderableScrollable get _reorderableScrollable => ReorderableScrollable.of( + context, + scrollController: widget.scrollController, + ); } diff --git a/lib/widgets/reorderable_scrolling_listener.dart b/lib/widgets/reorderable_scrolling_listener.dart index 7394df1..b079e11 100644 --- a/lib/widgets/reorderable_scrolling_listener.dart +++ b/lib/widgets/reorderable_scrolling_listener.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:flutter/cupertino.dart'; +import 'package:flutter_reorderable_grid_view/utils/reorderable_scrollable.dart'; import 'package:flutter_reorderable_grid_view/widgets/reorderable_builder.dart'; /// Uses [Listener] to indicate position updates while dragging a child and enables an autoscroll functionality. @@ -61,15 +62,6 @@ class _ReorderableScrollingListenerState /// [Offset] of the child that was found in [widget.reorderableChildKey]. Offset? _childOffset; - /// Indicator to know if the widget is scrollable and where it is. - /// - /// If this is null, then it means that any widget isn't scrollable. - /// If this is true, then it means a widget outside of [ReorderableBuilder] - /// is scrollable. - /// If this is false, then it means a widget inside [ReorderableBuilder] - /// is scrollable. - bool? _isScrollableOutside; - @override void didUpdateWidget(covariant ReorderableScrollingListener oldWidget) { super.didUpdateWidget(oldWidget); @@ -103,7 +95,8 @@ class _ReorderableScrollingListenerState /// no timer is ongoing when the [widget.isDragging] stopped, it checks also himself /// if it has to be canceled. void _handleDragUpdate(PointerMoveEvent details) { - final scrollDirection = _scrollDirection; + final scrollDirection = _reorderableScrollable.scrollDirection; + if (widget.enableScrollingWhileDragging && widget.reorderableChildKey != null && scrollDirection != null) { @@ -179,15 +172,15 @@ class _ReorderableScrollingListenerState scrollToTop = !scrollToTop; } - final scrollOffset = _scrollOffset; - final maxScrollExtent = _maxScrollExtent; + final scrollOffset = _reorderableScrollable.pixels; + final maxScrollExtent = _reorderableScrollable.maxScrollExtent; if (scrollOffset != null && maxScrollExtent != null) { final value = scrollToTop ? scrollOffset - 10 : scrollOffset + 10; // only scroll in the viewport of scrollable widget if (value > 0 && value < maxScrollExtent + 10) { - _jumpTo(value: value); + _reorderableScrollable.jumpTo(value: value); } } } @@ -209,45 +202,16 @@ class _ReorderableScrollingListenerState final dimension = Scrollable.of(context).position.viewportDimension; _childSize = Size(dimension, dimension); _childOffset = renderBox.localToGlobal(Offset.zero); - _isScrollableOutside = true; } else { _childSize = renderBox.size; _childOffset = Offset.zero; - _isScrollableOutside = false; } } }); } - Axis? get _scrollDirection { - if (_isScrollableOutside == true) { - return Scrollable.of(context).position.axis; - } else { - return widget.scrollController?.position.axis; - } - } - - double? get _scrollOffset { - if (_isScrollableOutside == true) { - return Scrollable.of(context).position.pixels; - } else { - return widget.scrollController?.offset; - } - } - - double? get _maxScrollExtent { - if (_isScrollableOutside == true) { - return Scrollable.of(context).position.maxScrollExtent; - } else { - return widget.scrollController?.position.maxScrollExtent; - } - } - - void _jumpTo({required double value}) { - if (_isScrollableOutside == true) { - Scrollable.of(context).position.moveTo(value); - } else { - widget.scrollController?.jumpTo(value); - } - } + ReorderableScrollable get _reorderableScrollable => ReorderableScrollable.of( + context, + scrollController: widget.scrollController, + ); } From 3b450855415acc41301d33568626ee51fa739cbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= Date: Wed, 22 Jan 2025 20:58:15 +0100 Subject: [PATCH 2/6] lock --- example/ios/Podfile.lock | 2 +- example/pubspec.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 9422c38..41fe01a 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -19,4 +19,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796 -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 diff --git a/example/pubspec.lock b/example/pubspec.lock index d298642..d8aeab8 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -89,7 +89,7 @@ packages: path: ".." relative: true source: path - version: "5.4.0" + version: "5.4.1" flutter_test: dependency: "direct dev" description: flutter From 76f4677bc66994280f33db9c1bbad096fcdb9a41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= Date: Thu, 23 Jan 2025 19:14:56 +0100 Subject: [PATCH 3/6] updated changelog, readme, automaticScrollExtent to 150 --- CHANGELOG.md | 6 ++++++ README.md | 8 ++++++-- example/lib/other_examples/outer_scrollable_example.dart | 7 ++++--- lib/widgets/reorderable_builder.dart | 2 +- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ca804a..60bcc54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 5.5.0 +🏄‍♂️ **Improvements** +- updated the default value of `automaticScrollExtent` to `150.0` (was `80.0` before) +- updated `README` to explain when to use a `ScrollController` (related to the issue ([#152](https://github.com/karvulf/flutter-reorderable-grid-view/issues/152))) +- some code refactoring + ## 5.4.1 🐛 **Bug Fixes** - when dragging an item to a `DragTarget` widget, the `onDragEnd` callback was not being called, causing the reorder process to remain incomplete diff --git a/README.md b/README.md index 78e228d..6423c1b 100644 --- a/README.md +++ b/README.md @@ -102,10 +102,14 @@ To use this widget, wrap `ReorderableBuilder` around your `GridView`. For more d **Important**: Ensure each child within the `GridView` has a unique key. +### ScrollController + When using a scrollable `GridView`, `ReorderableBuilder` requires a `ScrollController`. This means you must assign the `ScrollController` to both the scrollable widget and `ReorderableBuilder`. -This applies whether the scrollable widget is a parent of your `GridView` (e.g. `SingleChildScrollView`) -or the `GridView` itself. + +If the parent is a scrollable widget, you should not assign a `ScrollController`. +In this case, the widget automatically looks up the `ScrollController`. +Assigning one manually can cause issues with drag-and-drop functionality. ### Drag and Drop The drag-and-drop functionality is enabled by default. diff --git a/example/lib/other_examples/outer_scrollable_example.dart b/example/lib/other_examples/outer_scrollable_example.dart index 2904ff1..5fb4a0e 100644 --- a/example/lib/other_examples/outer_scrollable_example.dart +++ b/example/lib/other_examples/outer_scrollable_example.dart @@ -14,6 +14,7 @@ class MyApp extends StatefulWidget { class _MyAppState extends State { final _gridViewKey = GlobalKey(); + final bool reverse = false; List children = List.generate(200, (index) => index); @@ -21,7 +22,7 @@ class _MyAppState extends State { Widget build(BuildContext context) { return Scaffold( body: SingleChildScrollView( - reverse: true, + reverse: reverse, child: Column( children: [ const SizedBox( @@ -35,11 +36,11 @@ class _MyAppState extends State { children = reorderedListFunction(children); }); }, - reverse: true, + reverse: reverse, childBuilder: (itemBuilder) { return GridView.builder( key: _gridViewKey, - reverse: true, + reverse: reverse, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: children.length, diff --git a/lib/widgets/reorderable_builder.dart b/lib/widgets/reorderable_builder.dart index 323befb..47459d2 100644 --- a/lib/widgets/reorderable_builder.dart +++ b/lib/widgets/reorderable_builder.dart @@ -30,7 +30,7 @@ class ReorderableBuilder extends StatefulWidget { static const _defaultEnableLongPress = true; static const _defaultLongPressDelay = kLongPressTimeout; static const _defaultEnableDraggable = true; - static const _defaultAutomaticScrollExtent = 80.0; + static const _defaultAutomaticScrollExtent = 150.0; static const _defaultEnableScrollingWhileDragging = true; static const _defaultFadeInDuration = Duration(milliseconds: 500); static const _defaultReleasedChildDuration = Duration(milliseconds: 150); From 2abc94f4f2155ce56a9b7801b72410d188b4e354 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= Date: Thu, 23 Jan 2025 19:22:55 +0100 Subject: [PATCH 4/6] added comments --- lib/utils/reorderable_scrollable.dart | 62 ++++++++++++++++----- test/utils/reorderable_scrollable_test.dart | 0 2 files changed, 48 insertions(+), 14 deletions(-) create mode 100644 test/utils/reorderable_scrollable_test.dart diff --git a/lib/utils/reorderable_scrollable.dart b/lib/utils/reorderable_scrollable.dart index bc6e8be..4919d41 100644 --- a/lib/utils/reorderable_scrollable.dart +++ b/lib/utils/reorderable_scrollable.dart @@ -1,12 +1,22 @@ import 'package:flutter/material.dart'; +/// A utility class to manage scrolling behavior for scrollable widgets. /// +/// The [ReorderableScrollable] class allows you to retrieve and control the +/// scroll position, direction, and offsets in both parent and child scrollable +/// contexts. It is particularly useful in scenarios involving nested +/// scrollable widgets or drag-and-drop operations. class ReorderableScrollable { + /// The build context associated with the widget using this class. static late BuildContext _context; + + /// The optional ScrollController for managing child scrollable widgets. static late ScrollController? _scrollController; + /// Private constructor to prevent direct instantiation. const ReorderableScrollable._(); + /// Factory method to create a `ReorderableScrollable` instance. static ReorderableScrollable of( BuildContext context, { required ScrollController? scrollController, @@ -17,25 +27,25 @@ class ReorderableScrollable { return const ReorderableScrollable._(); } - /// Returning the current scroll position. + /// Returns the current scroll offset as an `Offset`. /// - /// There are two possibilities to get the scroll position. + /// This method determines the scroll offset by checking if the scrollable + /// widget is managed by a parent or has its own `ScrollController`. /// - /// First one is, the returned child of [widget.builder] is a scrollable widget. - /// In this case, it is important that the [widget.scrollController] is added - /// to the scrollable widget to get the current scroll position. + /// - If a `ScrollController` is provided, it uses the controller to fetch the + /// scroll position. + /// - If no controller is provided, it attempts to retrieve the scroll position + /// from the parent scrollable widget using the context. /// - /// Another possibility is that one of the parents is scrollable. - /// In that case, the position of the scroll is accessible inside [context]. + /// - [reverse]: Whether to reverse the direction of the offset. /// - /// Otherwise, 0.0 will be returned. + /// Returns an `Offset` representing the scroll position or `Offset.zero` if + /// no position is available. Offset getScrollOffset({required bool reverse}) { var scrollPosition = Scrollable.maybeOf(_context)?.position; final scrollController = _scrollController; - // For example, in cases where there are nested scrollable widgets - // like GridViews inside a parent scrollable widget, - // the widget assigned to the controller will be used for scroll calculations + // Use the provided ScrollController's position if it has clients. if (scrollController != null && scrollController.hasClients) { scrollPosition = scrollController.position; } @@ -43,6 +53,7 @@ class ReorderableScrollable { if (scrollPosition != null) { final pixels = scrollPosition.pixels; final isScrollingVertical = scrollPosition.axis == Axis.vertical; + final offset = Offset( isScrollingVertical ? 0.0 : pixels, isScrollingVertical ? pixels : 0.0, @@ -53,14 +64,19 @@ class ReorderableScrollable { return Offset.zero; } - /// No [ScrollController] means that this widget is already in a scrollable widget. + /// Indicates whether the widget relies on a parent scrollable widget. /// - /// [widget.scrollController] should be assigned if the scrollable widget - /// is rendered inside this widget e.g. in a [GridView]. + /// If no [ScrollController] is assigned, it is assumed that the widget is + /// already inside a scrollable parent widget. bool get isScrollOutside { return _scrollController == null; } + /// Retrieves the scroll direction of the associated scrollable widget. + /// + /// - If no `ScrollController` is assigned, the scroll direction of the parent + /// scrollable widget is returned. + /// - If a `ScrollController` is assigned, its direction is returned. Axis? get scrollDirection { if (isScrollOutside) { return Scrollable.of(_context).position.axis; @@ -69,6 +85,11 @@ class ReorderableScrollable { } } + /// Retrieves the current scroll position in pixels. + /// + /// - If no `ScrollController` is assigned, the scroll position of the parent + /// scrollable widget is returned. + /// - If a `ScrollController` is assigned, its offset is returned. double? get pixels { if (isScrollOutside) { return Scrollable.of(_context).position.pixels; @@ -77,6 +98,11 @@ class ReorderableScrollable { } } + /// Retrieves the maximum scrollable extent. + /// + /// - If no `ScrollController` is assigned, the maximum scroll extent of the + /// parent scrollable widget is returned. + /// - If a `ScrollController` is assigned, its maximum scroll extent is returned. double? get maxScrollExtent { if (isScrollOutside) { return Scrollable.of(_context).position.maxScrollExtent; @@ -85,6 +111,14 @@ class ReorderableScrollable { } } + /// Jumps to a specific scroll position. + /// + /// - [value]: The target scroll position in pixels. + /// + /// - If no `ScrollController` is assigned, the scroll position of the parent + /// scrollable widget is updated. + /// - If a `ScrollController` is assigned, its `jumpTo` method is used to + /// update the scroll position. void jumpTo({required double value}) { if (isScrollOutside == true) { Scrollable.of(_context).position.moveTo(value); diff --git a/test/utils/reorderable_scrollable_test.dart b/test/utils/reorderable_scrollable_test.dart new file mode 100644 index 0000000..e69de29 From 9736315e529f9b54a632b52b3dff6a13889473c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= Date: Thu, 23 Jan 2025 19:50:56 +0100 Subject: [PATCH 5/6] added some new tests --- example/pubspec.lock | 2 +- pubspec.yaml | 3 +- test/helper/fakes/fake_build_context.dart | 4 + test/helper/fakes/fake_scroll_controller.dart | 52 +++++++ test/utils/reorderable_scrollable_test.dart | 143 ++++++++++++++++++ 5 files changed, 202 insertions(+), 2 deletions(-) create mode 100644 test/helper/fakes/fake_build_context.dart create mode 100644 test/helper/fakes/fake_scroll_controller.dart diff --git a/example/pubspec.lock b/example/pubspec.lock index d8aeab8..c788b5b 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -89,7 +89,7 @@ packages: path: ".." relative: true source: path - version: "5.4.1" + version: "5.5.0" flutter_test: dependency: "direct dev" description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 329e1e7..f094f31 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_reorderable_grid_view description: Enables animated GridViews when updating children or when trying to reorder them by using drag and drop. -version: 5.4.1 +version: 5.5.0 repository: https://github.com/karvulf/flutter-reorderable-grid-view environment: @@ -16,6 +16,7 @@ dev_dependencies: sdk: flutter flutter_lints: ^2.0.1 + # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/test/helper/fakes/fake_build_context.dart b/test/helper/fakes/fake_build_context.dart new file mode 100644 index 0000000..f028e8b --- /dev/null +++ b/test/helper/fakes/fake_build_context.dart @@ -0,0 +1,4 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter_test/flutter_test.dart'; + +class FakeBuildContext extends Fake implements BuildContext {} diff --git a/test/helper/fakes/fake_scroll_controller.dart b/test/helper/fakes/fake_scroll_controller.dart new file mode 100644 index 0000000..6945532 --- /dev/null +++ b/test/helper/fakes/fake_scroll_controller.dart @@ -0,0 +1,52 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter_test/flutter_test.dart'; + +class FakeScrollController extends Fake implements ScrollController { + final Axis _axis; + final double _offset; + final double _maxScrollExtent; + double? _jumpToValue; + + FakeScrollController({ + Axis? axis, + double? offset, + double? maxScrollExtent, + }) : _axis = axis ?? Axis.horizontal, + _offset = offset ?? 0.0, + _maxScrollExtent = maxScrollExtent ?? 1.0; + + @override + ScrollPosition get position => FakeScrollPosition( + axis: _axis, + maxScrollExtent: _maxScrollExtent, + ); + + @override + double get offset => _offset; + + @override + void jumpTo(double value) { + _jumpToValue = value; + } + + void verifyJumpTo(double expectedValue) { + expect(_jumpToValue, equals(expectedValue)); + } +} + +class FakeScrollPosition extends Fake implements ScrollPosition { + final Axis _axis; + final double _maxScrollExtent; + + FakeScrollPosition({ + required Axis axis, + required double maxScrollExtent, + }) : _axis = axis, + _maxScrollExtent = maxScrollExtent; + + @override + Axis get axis => _axis; + + @override + double get maxScrollExtent => _maxScrollExtent; +} diff --git a/test/utils/reorderable_scrollable_test.dart b/test/utils/reorderable_scrollable_test.dart index e69de29..aff6ed4 100644 --- a/test/utils/reorderable_scrollable_test.dart +++ b/test/utils/reorderable_scrollable_test.dart @@ -0,0 +1,143 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter_reorderable_grid_view/utils/reorderable_scrollable.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../helper/fakes/fake_build_context.dart'; +import '../helper/fakes/fake_scroll_controller.dart'; + +void main() { + late ReorderableScrollable reorderableScrollable; + + late FakeScrollController fakeScrollController; + late FakeBuildContext fakeBuildContext; + + setUp(() { + fakeScrollController = FakeScrollController(); + fakeBuildContext = FakeBuildContext(); + + reorderableScrollable = ReorderableScrollable.of( + fakeBuildContext, + scrollController: fakeScrollController, + ); + }); + + group('#isScrollOutside', () { + test( + 'GIVEN _scrollController = null ' + 'WHEN calling isScrollOutside ' + 'THEN should return true', () { + // given + reorderableScrollable = ReorderableScrollable.of( + fakeBuildContext, + scrollController: null, + ); + + // when + final actual = reorderableScrollable.isScrollOutside; + + // then + expect(actual, isTrue); + }); + + test( + 'GIVEN _scrollController != null ' + 'WHEN calling isScrollOutside ' + 'THEN should return true', () { + // given + reorderableScrollable = ReorderableScrollable.of( + fakeBuildContext, + scrollController: fakeScrollController, + ); + + // when + final actual = reorderableScrollable.isScrollOutside; + + // then + expect(actual, isFalse); + }); + }); + + group('#scrollDirection', () { + test( + 'GIVEN _scrollController != null ' + 'WHEN calling scrollDirection ' + 'THEN should return axis', () { + // given + const givenAxis = Axis.horizontal; + final fakeScrollController = FakeScrollController(axis: givenAxis); + + reorderableScrollable = ReorderableScrollable.of( + fakeBuildContext, + scrollController: fakeScrollController, + ); + + // when + final actual = reorderableScrollable.scrollDirection; + + // then + expect(actual, equals(givenAxis)); + }); + }); + + group('#pixels', () { + test( + 'GIVEN _scrollController != null ' + 'WHEN calling pixels ' + 'THEN should return offset', () { + // given + const givenOffset = 12.34; + final fakeScrollController = FakeScrollController(offset: givenOffset); + + reorderableScrollable = ReorderableScrollable.of( + fakeBuildContext, + scrollController: fakeScrollController, + ); + + // when + final actual = reorderableScrollable.pixels; + + // then + expect(actual, equals(givenOffset)); + }); + }); + + group('#maxScrollExtent', () { + test( + 'GIVEN _scrollController != null ' + 'WHEN calling maxScrollExtent ' + 'THEN should return maxScrollExtent', () { + // given + const givenMaxScrollExtent = 56.78; + final fakeScrollController = FakeScrollController( + maxScrollExtent: givenMaxScrollExtent, + ); + + reorderableScrollable = ReorderableScrollable.of( + fakeBuildContext, + scrollController: fakeScrollController, + ); + + // when + final actual = reorderableScrollable.maxScrollExtent; + + // then + expect(actual, equals(givenMaxScrollExtent)); + }); + }); + + group('#maxScrollExtent', () { + test( + 'GIVEN value ' + 'WHEN calling jumpTo ' + 'THEN should call jumpTo of ScrollController with given value', () { + // given + const givenValue = 9.10; + + // when + reorderableScrollable.jumpTo(value: givenValue); + + // then + fakeScrollController.verifyJumpTo(givenValue); + }); + }); +} From f7d4a57d726875df4d05aa1800e0acce17d4e95c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= Date: Thu, 23 Jan 2025 20:00:46 +0100 Subject: [PATCH 6/6] updated examples and comments --- README.md | 8 +++ .../inner_scrollable_example.dart | 0 .../scrollable/no_scrollable_example.dart | 56 +++++++++++++++++++ .../outer_scrollable_example.dart | 0 lib/utils/reorderable_scrollable.dart | 8 +-- .../reorderable_scrolling_listener.dart | 1 - .../reorderable_builder_widget_test.dart | 8 +-- 7 files changed, 72 insertions(+), 9 deletions(-) rename example/lib/other_examples/{ => scrollable}/inner_scrollable_example.dart (100%) create mode 100644 example/lib/other_examples/scrollable/no_scrollable_example.dart rename example/lib/other_examples/{ => scrollable}/outer_scrollable_example.dart (100%) diff --git a/README.md b/README.md index 6423c1b..cca9073 100644 --- a/README.md +++ b/README.md @@ -107,10 +107,18 @@ To use this widget, wrap `ReorderableBuilder` around your `GridView`. For more d When using a scrollable `GridView`, `ReorderableBuilder` requires a `ScrollController`. This means you must assign the `ScrollController` to both the scrollable widget and `ReorderableBuilder`. +*For more details, check out the example in `inner_scrollable_example.dart`.* + If the parent is a scrollable widget, you should not assign a `ScrollController`. In this case, the widget automatically looks up the `ScrollController`. Assigning one manually can cause issues with drag-and-drop functionality. +*For more details, check out the example in `outer_scrollable_example.dart`.* + +If no `ScrollController` is assigned and no `ScrollController` is found within the context, it indicates that the `GridView` is not scrollable. + +*For more details, check out the example in `no_scrollable_example.dart`.* + ### Drag and Drop The drag-and-drop functionality is enabled by default. diff --git a/example/lib/other_examples/inner_scrollable_example.dart b/example/lib/other_examples/scrollable/inner_scrollable_example.dart similarity index 100% rename from example/lib/other_examples/inner_scrollable_example.dart rename to example/lib/other_examples/scrollable/inner_scrollable_example.dart diff --git a/example/lib/other_examples/scrollable/no_scrollable_example.dart b/example/lib/other_examples/scrollable/no_scrollable_example.dart new file mode 100644 index 0000000..b079c41 --- /dev/null +++ b/example/lib/other_examples/scrollable/no_scrollable_example.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_reorderable_grid_view/widgets/widgets.dart'; + +void main() { + runApp(const MaterialApp(home: MyApp())); +} + +class MyApp extends StatefulWidget { + const MyApp({super.key}); + + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { + final _gridViewKey = GlobalKey(); + + List children = List.generate(100, (index) => index); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: ReorderableBuilder.builder( + onReorder: (ReorderedListFunction reorderedListFunction) { + setState(() { + children = reorderedListFunction(children); + }); + }, + childBuilder: (itemBuilder) { + return GridView.builder( + key: _gridViewKey, + physics: const NeverScrollableScrollPhysics(), + itemCount: children.length, + itemBuilder: (context, index) { + return itemBuilder( + ColoredBox( + key: Key(children.elementAt(index).toString()), + color: Colors.lightBlue, + child: Text( + children.elementAt(index).toString(), + ), + ), + index, + ); + }, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 4, + mainAxisSpacing: 4, + crossAxisSpacing: 8, + ), + ); + }, + ), + ); + } +} diff --git a/example/lib/other_examples/outer_scrollable_example.dart b/example/lib/other_examples/scrollable/outer_scrollable_example.dart similarity index 100% rename from example/lib/other_examples/outer_scrollable_example.dart rename to example/lib/other_examples/scrollable/outer_scrollable_example.dart diff --git a/lib/utils/reorderable_scrollable.dart b/lib/utils/reorderable_scrollable.dart index 4919d41..fa7e7f4 100644 --- a/lib/utils/reorderable_scrollable.dart +++ b/lib/utils/reorderable_scrollable.dart @@ -79,7 +79,7 @@ class ReorderableScrollable { /// - If a `ScrollController` is assigned, its direction is returned. Axis? get scrollDirection { if (isScrollOutside) { - return Scrollable.of(_context).position.axis; + return Scrollable.maybeOf(_context)?.position.axis; } else { return _scrollController?.position.axis; } @@ -92,7 +92,7 @@ class ReorderableScrollable { /// - If a `ScrollController` is assigned, its offset is returned. double? get pixels { if (isScrollOutside) { - return Scrollable.of(_context).position.pixels; + return Scrollable.maybeOf(_context)?.position.pixels; } else { return _scrollController?.offset; } @@ -105,7 +105,7 @@ class ReorderableScrollable { /// - If a `ScrollController` is assigned, its maximum scroll extent is returned. double? get maxScrollExtent { if (isScrollOutside) { - return Scrollable.of(_context).position.maxScrollExtent; + return Scrollable.maybeOf(_context)?.position.maxScrollExtent; } else { return _scrollController?.position.maxScrollExtent; } @@ -121,7 +121,7 @@ class ReorderableScrollable { /// update the scroll position. void jumpTo({required double value}) { if (isScrollOutside == true) { - Scrollable.of(_context).position.moveTo(value); + Scrollable.maybeOf(_context)?.position.moveTo(value); } else { _scrollController?.jumpTo(value); } diff --git a/lib/widgets/reorderable_scrolling_listener.dart b/lib/widgets/reorderable_scrolling_listener.dart index b079e11..1408575 100644 --- a/lib/widgets/reorderable_scrolling_listener.dart +++ b/lib/widgets/reorderable_scrolling_listener.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:flutter/cupertino.dart'; import 'package:flutter_reorderable_grid_view/utils/reorderable_scrollable.dart'; -import 'package:flutter_reorderable_grid_view/widgets/reorderable_builder.dart'; /// Uses [Listener] to indicate position updates while dragging a child and enables an autoscroll functionality. class ReorderableScrollingListener extends StatefulWidget { diff --git a/test/widgets/reorderable_builder_widget_test.dart b/test/widgets/reorderable_builder_widget_test.dart index a9a39bf..cfb8458 100644 --- a/test/widgets/reorderable_builder_widget_test.dart +++ b/test/widgets/reorderable_builder_widget_test.dart @@ -72,7 +72,7 @@ void main() { widget.nonDraggableIndices.isEmpty && widget.longPressDelay == const Duration(milliseconds: 500) && widget.enableDraggable && - widget.automaticScrollExtent == 80.0 && + widget.automaticScrollExtent == 150.0 && widget.enableScrollingWhileDragging && widget.fadeInDuration == const Duration(milliseconds: 500) && widget.releasedChildDuration == @@ -88,7 +88,7 @@ void main() { !widget.isDragging && widget.reorderableChildKey == null && widget.scrollController == null && - widget.automaticScrollExtent == 80.0 && + widget.automaticScrollExtent == 150.0 && widget.enableScrollingWhileDragging && !widget.reverse && widget.child is SingleChildScrollView), @@ -193,7 +193,7 @@ void main() { widget.nonDraggableIndices == const [1] && widget.longPressDelay == const Duration(milliseconds: 500) && widget.enableDraggable && - widget.automaticScrollExtent == 80.0 && + widget.automaticScrollExtent == 150.0 && widget.enableScrollingWhileDragging && widget.fadeInDuration == const Duration(milliseconds: 500) && widget.releasedChildDuration == @@ -208,7 +208,7 @@ void main() { !widget.isDragging && widget.reorderableChildKey == null && widget.scrollController == null && - widget.automaticScrollExtent == 80.0 && + widget.automaticScrollExtent == 150.0 && widget.enableScrollingWhileDragging && !widget.reverse && widget.child is SingleChildScrollView),