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.md
This commit is contained in:
Valentin Vignal
2025-01-10 06:29:02 +08:00
committed by GitHub
parent 59f630b7b5
commit 34081dc2c9
2 changed files with 142 additions and 10 deletions

View File

@@ -103,6 +103,7 @@ class ReorderableListView extends StatefulWidget {
this.clipBehavior = Clip.hardEdge,
this.autoScrollerVelocityScalar,
this.dragBoundaryProvider,
this.mouseCursor,
}) : assert(
(itemExtent == null && prototypeItem == null) ||
(itemExtent == null && itemExtentBuilder == null) ||
@@ -173,6 +174,7 @@ class ReorderableListView extends StatefulWidget {
this.clipBehavior = Clip.hardEdge,
this.autoScrollerVelocityScalar,
this.dragBoundaryProvider,
this.mouseCursor,
}) : assert(itemCount >= 0),
assert(
(itemExtent == null && prototypeItem == null) ||
@@ -297,11 +299,25 @@ class ReorderableListView extends StatefulWidget {
/// {@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 @@ class _ReorderableListViewState extends State<ReorderableListView> {
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 @@ class _ReorderableListViewState extends State<ReorderableListView> {
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 @@ class _ReorderableListViewState extends State<ReorderableListView> {
end: 8,
child: Align(
alignment: AlignmentDirectional.centerEnd,
child: ReorderableDragStartListener(
index: index,
child: const Icon(Icons.drag_handle),
),
child: ReorderableDragStartListener(index: index, child: dragHandle),
),
),
],
@@ -383,6 +408,12 @@ class _ReorderableListViewState extends State<ReorderableListView> {
);
}
@override
void dispose() {
_dragging.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterialLocalizations(context));
@@ -440,8 +471,14 @@ class _ReorderableListViewState extends State<ReorderableListView> {
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,

View File

@@ -2318,6 +2318,101 @@ void main() {
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 {