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

152 bug drag and drop doesnt work with outside scrollable area #154

Merged
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,22 @@ 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.

*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.
Expand Down
2 changes: 1 addition & 1 deletion example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ SPEC CHECKSUMS:

PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796

COCOAPODS: 1.15.2
COCOAPODS: 1.16.2
56 changes: 56 additions & 0 deletions example/lib/other_examples/scrollable/no_scrollable_example.dart
Original file line number Diff line number Diff line change
@@ -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<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
final _gridViewKey = GlobalKey();

List<int> children = List.generate(100, (index) => index);

@override
Widget build(BuildContext context) {
return Scaffold(
body: ReorderableBuilder.builder(
onReorder: (ReorderedListFunction<int> 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,
),
);
},
),
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@ class MyApp extends StatefulWidget {

class _MyAppState extends State<MyApp> {
final _gridViewKey = GlobalKey();
final bool reverse = false;

List<int> children = List.generate(200, (index) => index);

@override
Widget build(BuildContext context) {
return Scaffold(
body: SingleChildScrollView(
reverse: true,
reverse: reverse,
child: Column(
children: [
const SizedBox(
Expand All @@ -35,11 +36,11 @@ class _MyAppState extends State<MyApp> {
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,
Expand Down
2 changes: 1 addition & 1 deletion example/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ packages:
path: ".."
relative: true
source: path
version: "5.4.0"
version: "5.5.0"
flutter_test:
dependency: "direct dev"
description: flutter
Expand Down
129 changes: 129 additions & 0 deletions lib/utils/reorderable_scrollable.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
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,
}) {
_context = context;
_scrollController = scrollController;

return const ReorderableScrollable._();
}

/// Returns the current scroll offset as an `Offset`.
///
/// This method determines the scroll offset by checking if the scrollable
/// widget is managed by a parent or has its own `ScrollController`.
///
/// - 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.
///
/// - [reverse]: Whether to reverse the direction of the offset.
///
/// 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;

// Use the provided ScrollController's position if it has clients.
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;
}

/// Indicates whether the widget relies on a parent scrollable widget.
///
/// 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.maybeOf(_context)?.position.axis;
} else {
return _scrollController?.position.axis;
}
}

/// 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.maybeOf(_context)?.position.pixels;
} else {
return _scrollController?.offset;
}
}

/// 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.maybeOf(_context)?.position.maxScrollExtent;
} else {
return _scrollController?.position.maxScrollExtent;
}
}

/// 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.maybeOf(_context)?.position.moveTo(value);
} else {
_scrollController?.jumpTo(value);
}
}
}
64 changes: 19 additions & 45 deletions lib/widgets/reorderable_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -29,7 +30,7 @@ class ReorderableBuilder<T> 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);
Expand Down Expand Up @@ -293,7 +294,7 @@ class _ReorderableBuilderState<T> extends State<ReorderableBuilder<T>>
if (orientationBefore != orientationAfter ||
screenSizeBefore != screenSizeAfter) {
_reorderableController.handleDeviceOrientationChanged();
_reorderableController.scrollOffset == _getScrollOffset();
_reorderableController.scrollOffset == _scrollOffset;
setState(() {});
}
});
Expand Down Expand Up @@ -393,7 +394,7 @@ class _ReorderableBuilderState<T> extends State<ReorderableBuilder<T>>
onCreated: _handleCreatedChild,
releasedReorderableEntity:
_reorderableController.releasedReorderableEntity,
scrollOffset: _getScrollOffset(),
scrollOffset: _scrollOffset,
releasedChildDuration: widget.releasedChildDuration,
enableDraggable: widget.enableDraggable && isDraggable,
currentDraggedEntity: currentDraggedEntity,
Expand All @@ -416,7 +417,7 @@ class _ReorderableBuilderState<T> extends State<ReorderableBuilder<T>>
void _handleDragStarted(ReorderableEntity reorderableEntity) {
_reorderableController.handleDragStarted(
reorderableEntity: reorderableEntity,
currentScrollOffset: _getScrollOffset(),
currentScrollOffset: _scrollOffset,
lockedIndices: widget.lockedIndices,
isScrollableOutside: _isScrollOutside,
);
Expand All @@ -432,7 +433,7 @@ class _ReorderableBuilderState<T> extends State<ReorderableBuilder<T>>

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;
Expand All @@ -458,7 +459,7 @@ class _ReorderableBuilderState<T> extends State<ReorderableBuilder<T>>

// scrollable part is outside this widget
if (_isScrollOutside) {
offset -= _getScrollOffset();
offset -= _scrollOffset;
}

// call to ensure animation to dropped item
Expand Down Expand Up @@ -548,7 +549,7 @@ class _ReorderableBuilderState<T> extends State<ReorderableBuilder<T>>
offset = parentRenderObject.globalToLocal(
renderBox.localToGlobal(Offset.zero),
);
offset += _getScrollOffset();
offset += _scrollOffset;
}
}

Expand All @@ -567,45 +568,18 @@ class _ReorderableBuilderState<T> extends State<ReorderableBuilder<T>>
}
}

/// 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,
);
}
Loading
Loading