diff --git a/packages/flutter/lib/src/widgets/drag_target.dart b/packages/flutter/lib/src/widgets/drag_target.dart index de892e5145..5d20c6bf30 100644 --- a/packages/flutter/lib/src/widgets/drag_target.dart +++ b/packages/flutter/lib/src/widgets/drag_target.dart @@ -154,6 +154,11 @@ enum DragAnchor { /// user lifts their finger while on top of a [DragTarget], that target is given /// the opportunity to accept the [data] carried by the draggable. /// +/// The [ignoringFeedbackPointer] defaults to true, which means that +/// the [feedback] widget ignores the pointer during hit testing. Similarly, +/// [ignoringFeedbackSemantics] defaults to true, and the [feedback] also ignores +/// semantics when building the semantics tree. +/// /// On multitouch devices, multiple drags can occur simultaneously because there /// can be multiple pointers in contact with the device at once. To limit the /// number of simultaneous drags, use the [maxSimultaneousDrags] property. The @@ -207,11 +212,13 @@ class Draggable extends StatefulWidget { this.onDragEnd, this.onDragCompleted, this.ignoringFeedbackSemantics = true, + this.ignoringFeedbackPointer = true, this.rootOverlay = false, this.hitTestBehavior = HitTestBehavior.deferToChild, }) : assert(child != null), assert(feedback != null), assert(ignoringFeedbackSemantics != null), + assert(ignoringFeedbackPointer != null), assert(maxSimultaneousDrags == null || maxSimultaneousDrags >= 0); /// The data that will be dropped by this draggable. @@ -310,6 +317,14 @@ class Draggable extends StatefulWidget { /// Defaults to true. final bool ignoringFeedbackSemantics; + /// Whether the [feedback] widget is ignored during hit testing. + /// + /// Regardless of whether this widget is ignored during hit testing, it will + /// still consume space during layout and be visible during painting. + /// + /// Defaults to true. + final bool ignoringFeedbackPointer; + /// Controls how this widget competes with other gestures to initiate a drag. /// /// If affinity is null, this widget initiates a drag as soon as it recognizes @@ -447,6 +462,7 @@ class LongPressDraggable extends Draggable { super.onDragCompleted, this.hapticFeedbackOnStart = true, super.ignoringFeedbackSemantics, + super.ignoringFeedbackPointer, this.delay = kLongPressTimeout, }); @@ -542,6 +558,7 @@ class _DraggableState extends State> { feedback: widget.feedback, feedbackOffset: widget.feedbackOffset, ignoringFeedbackSemantics: widget.ignoringFeedbackSemantics, + ignoringFeedbackPointer: widget.ignoringFeedbackPointer, onDragUpdate: (DragUpdateDetails details) { if (mounted && widget.onDragUpdate != null) { widget.onDragUpdate!(details); @@ -796,8 +813,10 @@ class _DragAvatar extends Drag { this.onDragUpdate, this.onDragEnd, required this.ignoringFeedbackSemantics, + required this.ignoringFeedbackPointer, }) : assert(overlayState != null), assert(ignoringFeedbackSemantics != null), + assert(ignoringFeedbackPointer != null), assert(dragStartPoint != null), assert(feedbackOffset != null), _position = initialPosition { @@ -815,6 +834,7 @@ class _DragAvatar extends Drag { final _OnDragEnd? onDragEnd; final OverlayState overlayState; final bool ignoringFeedbackSemantics; + final bool ignoringFeedbackPointer; _DragTargetState? _activeTarget; final List<_DragTargetState> _enteredTargets = <_DragTargetState>[]; @@ -937,6 +957,7 @@ class _DragAvatar extends Drag { left: _lastOffset!.dx - overlayTopLeft.dx, top: _lastOffset!.dy - overlayTopLeft.dy, child: IgnorePointer( + ignoring: ignoringFeedbackPointer, ignoringSemantics: ignoringFeedbackSemantics, child: feedback, ), diff --git a/packages/flutter/test/widgets/draggable_test.dart b/packages/flutter/test/widgets/draggable_test.dart index e58ee03713..4b78bee9be 100644 --- a/packages/flutter/test/widgets/draggable_test.dart +++ b/packages/flutter/test/widgets/draggable_test.dart @@ -10,7 +10,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/semantics.dart'; +import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -3074,6 +3074,96 @@ void main() { expect(tester.widget(find.byType(Listener).first).behavior, hitTestBehavior); }); + // Regression test for https://github.com/flutter/flutter/issues/92083 + testWidgets('feedback respect the MouseRegion cursor configure', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Column( + children: const [ + Draggable( + ignoringFeedbackPointer: false, + feedback: MouseRegion( + cursor: SystemMouseCursors.grabbing, + child: SizedBox(height: 50.0, child: Text('Draggable')), + ), + child: SizedBox(height: 50.0, child: Text('Target')), + ), + ], + ), + ), + ); + + final Offset location = tester.getCenter(find.text('Target')); + final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.addPointer(location: location); + addTearDown(gesture.removePointer); + + await gesture.down(location); + await tester.pump(); + + expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.grabbing); + }); + + testWidgets('configurable feedback ignore pointer behavior', (WidgetTester tester) async { + bool onTap = false; + await tester.pumpWidget( + MaterialApp( + home: Column( + children: [ + Draggable( + ignoringFeedbackPointer: false, + feedback: GestureDetector( + onTap: () => onTap = true, + child: const SizedBox(height: 50.0, child: Text('Draggable')), + ), + child: const SizedBox(height: 50.0, child: Text('Target')), + ), + ], + ), + ), + ); + + final Offset location = tester.getCenter(find.text('Target')); + final TestGesture gesture = await tester.startGesture(location, pointer: 7); + final Offset secondLocation = location + const Offset(7.0, 7.0); + await gesture.moveTo(secondLocation); + await tester.pump(); + + await tester.tap(find.text('Draggable')); + expect(onTap, true); + }); + + testWidgets('configurable feedback ignore pointer behavior - LongPressDraggable', (WidgetTester tester) async { + bool onTap = false; + await tester.pumpWidget( + MaterialApp( + home: Column( + children: [ + LongPressDraggable( + ignoringFeedbackPointer: false, + feedback: GestureDetector( + onTap: () => onTap = true, + child: const SizedBox(height: 50.0, child: Text('Draggable')), + ), + child: const SizedBox(height: 50.0, child: Text('Target')), + ), + ], + ), + ), + ); + + final Offset location = tester.getCenter(find.text('Target')); + final TestGesture gesture = await tester.startGesture(location, pointer: 7); + await tester.pump(kLongPressTimeout); + + final Offset secondLocation = location + const Offset(7.0, 7.0); + await gesture.moveTo(secondLocation); + await tester.pump(); + + await tester.tap(find.text('Draggable')); + expect(onTap, true); + }); + testWidgets('configurable DragTarget hit test behavior', (WidgetTester tester) async { const HitTestBehavior hitTestBehavior = HitTestBehavior.deferToChild;