Add `mouseCursor` parameter to `ReorderableListView` (#160246)
Part of https://github.com/flutter/flutter/issues/58192#issue-626789189
<table>
<tr>
<th>Defautl</th>
<th>Customized</th>
</tr>
<tr>
<td>
https://github.com/user-attachments/assets/91819d7e-472b-481d-84ff-bec0d812ab53
</td>
<td>
https://github.com/user-attachments/assets/61a6841f-1845-4444-a03a-8870f0b67969
</td>
</tr>
</table>
## Pre-launch Checklist
- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.
If you need help, consider asking for advice on the #hackers-new channel
on [Discord].
<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.mddiff --git a/packages/flutter/lib/src/material/reorderable_list.dart b/packages/flutter/lib/src/material/reorderable_list.dart
index 49a62f6..0ec5f8b 100644
--- a/packages/flutter/lib/src/material/reorderable_list.dart
+++ b/packages/flutter/lib/src/material/reorderable_list.dart
@@ -103,6 +103,7 @@
this.clipBehavior = Clip.hardEdge,
this.autoScrollerVelocityScalar,
this.dragBoundaryProvider,
+ this.mouseCursor,
}) : assert(
(itemExtent == null && prototypeItem == null) ||
(itemExtent == null && itemExtentBuilder == null) ||
@@ -173,6 +174,7 @@
this.clipBehavior = Clip.hardEdge,
this.autoScrollerVelocityScalar,
this.dragBoundaryProvider,
+ this.mouseCursor,
}) : assert(itemCount >= 0),
assert(
(itemExtent == null && prototypeItem == null) ||
@@ -297,11 +299,25 @@
/// {@macro flutter.widgets.reorderable_list.dragBoundaryProvider}
final ReorderDragBoundaryProvider? dragBoundaryProvider;
+ /// The cursor for a mouse pointer when it enters or is hovering over the drag
+ /// handle.
+ ///
+ /// If [mouseCursor] is a [WidgetStateMouseCursor],
+ /// [WidgetStateProperty.resolve] is used for the following [WidgetState]s:
+ ///
+ /// * [WidgetState.dragged].
+ ///
+ /// If this property is null, [SystemMouseCursors.grab] will be used when
+ /// hovering, and [SystemMouseCursors.grabbing] when dragging.
+ final MouseCursor? mouseCursor;
+
@override
State<ReorderableListView> createState() => _ReorderableListViewState();
}
class _ReorderableListViewState extends State<ReorderableListView> {
+ final ValueNotifier<bool> _dragging = ValueNotifier<bool>(false);
+
Widget _itemBuilder(BuildContext context, int index) {
final Widget item = widget.itemBuilder(context, index);
assert(() {
@@ -318,6 +334,21 @@
case TargetPlatform.linux:
case TargetPlatform.windows:
case TargetPlatform.macOS:
+ final ListenableBuilder dragHandle = ListenableBuilder(
+ listenable: _dragging,
+ builder: (BuildContext context, Widget? child) {
+ final MouseCursor effectiveMouseCursor = WidgetStateProperty.resolveAs<MouseCursor>(
+ widget.mouseCursor ??
+ const WidgetStateMouseCursor.fromMap(<WidgetStatesConstraint, MouseCursor>{
+ WidgetState.dragged: SystemMouseCursors.grabbing,
+ WidgetState.any: SystemMouseCursors.grab,
+ }),
+ <WidgetState>{if (_dragging.value) WidgetState.dragged},
+ );
+ return MouseRegion(cursor: effectiveMouseCursor, child: child);
+ },
+ child: const Icon(Icons.drag_handle),
+ );
switch (widget.scrollDirection) {
case Axis.horizontal:
return Stack(
@@ -331,10 +362,7 @@
bottom: 8,
child: Align(
alignment: AlignmentDirectional.bottomCenter,
- child: ReorderableDragStartListener(
- index: index,
- child: const Icon(Icons.drag_handle),
- ),
+ child: ReorderableDragStartListener(index: index, child: dragHandle),
),
),
],
@@ -351,10 +379,7 @@
end: 8,
child: Align(
alignment: AlignmentDirectional.centerEnd,
- child: ReorderableDragStartListener(
- index: index,
- child: const Icon(Icons.drag_handle),
- ),
+ child: ReorderableDragStartListener(index: index, child: dragHandle),
),
),
],
@@ -384,6 +409,12 @@
}
@override
+ void dispose() {
+ _dragging.dispose();
+ super.dispose();
+ }
+
+ @override
Widget build(BuildContext context) {
assert(debugCheckHasMaterialLocalizations(context));
assert(debugCheckHasOverlay(context));
@@ -440,8 +471,14 @@
prototypeItem: widget.prototypeItem,
itemCount: widget.itemCount,
onReorder: widget.onReorder,
- onReorderStart: widget.onReorderStart,
- onReorderEnd: widget.onReorderEnd,
+ onReorderStart: (int index) {
+ _dragging.value = true;
+ widget.onReorderStart?.call(index);
+ },
+ onReorderEnd: (int index) {
+ _dragging.value = false;
+ widget.onReorderEnd?.call(index);
+ },
proxyDecorator: widget.proxyDecorator ?? _proxyDecorator,
autoScrollerVelocityScalar: widget.autoScrollerVelocityScalar,
dragBoundaryProvider: widget.dragBoundaryProvider,
diff --git a/packages/flutter/test/material/reorderable_list_test.dart b/packages/flutter/test/material/reorderable_list_test.dart
index 080b3ba..4193071 100644
--- a/packages/flutter/test/material/reorderable_list_test.dart
+++ b/packages/flutter/test/material/reorderable_list_test.dart
@@ -2318,6 +2318,101 @@
await drag.up();
await tester.pumpAndSettle();
});
+
+ testWidgets('Mouse cursor behavior on the drag handle', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(
+ body: ReorderableListView.builder(
+ itemBuilder: (BuildContext context, int index) {
+ return ReorderableDragStartListener(
+ key: ValueKey<int>(index),
+ index: index,
+ child: Text('$index'),
+ );
+ },
+ itemCount: 5,
+ onReorder: (int fromIndex, int toIndex) {},
+ ),
+ ),
+ ),
+ );
+
+ final TestGesture gesture = await tester.createGesture(
+ kind: PointerDeviceKind.mouse,
+ pointer: 1,
+ );
+ await gesture.addPointer(location: tester.getCenter(find.byIcon(Icons.drag_handle).first));
+ await tester.pump();
+ expect(
+ RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
+ SystemMouseCursors.grab,
+ );
+ await gesture.down(tester.getCenter(find.byIcon(Icons.drag_handle).first));
+ await tester.pump(kLongPressTimeout);
+ expect(
+ RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
+ SystemMouseCursors.grabbing,
+ );
+ await gesture.up();
+ await tester.pumpAndSettle();
+ expect(
+ RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
+ SystemMouseCursors.grab,
+ );
+ }, variant: TargetPlatformVariant.desktop());
+
+ testWidgets(
+ 'Mouse cursor behavior on the drag handle can be provided',
+ (WidgetTester tester) async {
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(
+ body: ReorderableListView.builder(
+ mouseCursor:
+ const WidgetStateMouseCursor.fromMap(<WidgetStatesConstraint, MouseCursor>{
+ WidgetState.dragged: SystemMouseCursors.copy,
+ WidgetState.any: SystemMouseCursors.resizeColumn,
+ }),
+ itemBuilder: (BuildContext context, int index) {
+ return ReorderableDragStartListener(
+ key: ValueKey<int>(index),
+ index: index,
+ child: Text('$index'),
+ );
+ },
+ itemCount: 5,
+ onReorder: (int fromIndex, int toIndex) {},
+ ),
+ ),
+ ),
+ );
+
+ final TestGesture gesture = await tester.createGesture(
+ kind: PointerDeviceKind.mouse,
+ pointer: 1,
+ );
+ await gesture.addPointer(location: tester.getCenter(find.byIcon(Icons.drag_handle).first));
+ await tester.pump();
+ expect(
+ RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
+ SystemMouseCursors.resizeColumn,
+ );
+ await gesture.down(tester.getCenter(find.byIcon(Icons.drag_handle).first));
+ await tester.pump(kLongPressTimeout);
+ expect(
+ RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
+ SystemMouseCursors.copy,
+ );
+ await gesture.up();
+ await tester.pumpAndSettle();
+ expect(
+ RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
+ SystemMouseCursors.resizeColumn,
+ );
+ },
+ variant: TargetPlatformVariant.desktop(),
+ );
}
Future<void> longPressDrag(WidgetTester tester, Offset start, Offset end) async {