diff --git a/packages/flutter/lib/src/widgets/drag_target.dart b/packages/flutter/lib/src/widgets/drag_target.dart index 3081c954dab..85825109df5 100644 --- a/packages/flutter/lib/src/widgets/drag_target.dart +++ b/packages/flutter/lib/src/widgets/drag_target.dart @@ -36,6 +36,11 @@ typedef DragTargetAcceptWithDetails = void Function(DragTargetDetails deta /// Used by [DragTarget.builder]. typedef DragTargetBuilder = Widget Function(BuildContext context, List candidateData, List rejectedData); +/// Signature for when a [Draggable] is dragged across the screen. +/// +/// Used by [Draggable.onDragUpdate]. +typedef DragUpdateCallback = void Function(DragUpdateDetails details); + /// Signature for when a [Draggable] is dropped without being accepted by a [DragTarget]. /// /// Used by [Draggable.onDraggableCanceled]. @@ -187,6 +192,7 @@ class Draggable extends StatefulWidget { this.affinity, this.maxSimultaneousDrags, this.onDragStarted, + this.onDragUpdate, this.onDraggableCanceled, this.onDragEnd, this.onDragCompleted, @@ -298,6 +304,12 @@ class Draggable extends StatefulWidget { /// Called when the draggable starts being dragged. final VoidCallback? onDragStarted; + /// Called when the draggable is being dragged. + /// + /// This function will only be called while this widget is still mounted to + /// the tree (i.e. [State.mounted] is true), and if this widget has actually moved. + final DragUpdateCallback? onDragUpdate; + /// Called when the draggable is dropped without being accepted by a [DragTarget]. /// /// This function might be called after this widget has been removed from the @@ -374,6 +386,7 @@ class LongPressDraggable extends Draggable { DragAnchor dragAnchor = DragAnchor.child, int? maxSimultaneousDrags, VoidCallback? onDragStarted, + DragUpdateCallback? onDragUpdate, DraggableCanceledCallback? onDraggableCanceled, DragEndCallback? onDragEnd, VoidCallback? onDragCompleted, @@ -390,6 +403,7 @@ class LongPressDraggable extends Draggable { dragAnchor: dragAnchor, maxSimultaneousDrags: maxSimultaneousDrags, onDragStarted: onDragStarted, + onDragUpdate: onDragUpdate, onDraggableCanceled: onDraggableCanceled, onDragEnd: onDragEnd, onDragCompleted: onDragCompleted, @@ -474,6 +488,11 @@ class _DraggableState extends State> { feedback: widget.feedback, feedbackOffset: widget.feedbackOffset, ignoringFeedbackSemantics: widget.ignoringFeedbackSemantics, + onDragUpdate: (DragUpdateDetails details) { + if (mounted && widget.onDragUpdate != null) { + widget.onDragUpdate!(details); + } + }, onDragEnd: (Velocity velocity, Offset offset, bool wasAccepted) { if (mounted) { setState(() { @@ -708,6 +727,7 @@ class _DragAvatar extends Drag { this.dragStartPoint = Offset.zero, this.feedback, this.feedbackOffset = Offset.zero, + this.onDragUpdate, this.onDragEnd, required this.ignoringFeedbackSemantics, }) : assert(overlayState != null), @@ -725,6 +745,7 @@ class _DragAvatar extends Drag { final Offset dragStartPoint; final Widget? feedback; final Offset feedbackOffset; + final DragUpdateCallback? onDragUpdate; final _OnDragEnd? onDragEnd; final OverlayState overlayState; final bool ignoringFeedbackSemantics; @@ -737,8 +758,12 @@ class _DragAvatar extends Drag { @override void update(DragUpdateDetails details) { + final Offset oldPosition = _position; _position += _restrictAxis(details.delta); updateDrag(_position); + if (onDragUpdate != null && _position != oldPosition) { + onDragUpdate!(details); + } } @override diff --git a/packages/flutter/test/widgets/draggable_test.dart b/packages/flutter/test/widgets/draggable_test.dart index 66488fd7b9d..f1f40afe52a 100644 --- a/packages/flutter/test/widgets/draggable_test.dart +++ b/packages/flutter/test/widgets/draggable_test.dart @@ -840,6 +840,158 @@ void main() { }); }); + group('Drag and drop - onDragUpdate called if draggable moves along a set axis', () { + int updated = 0; + Offset dragDelta = Offset.zero; + + setUp(() { + updated = 0; + dragDelta = Offset.zero; + }); + + Widget build() { + return MaterialApp( + home: Column( + children: [ + Draggable( + data: 1, + child: const Text('Source'), + feedback: const Text('Dragging'), + onDragUpdate: (DragUpdateDetails details) { + dragDelta += details.delta; + updated++; + }, + ), + Draggable( + data: 2, + child: const Text('Vertical Source'), + feedback: const Text('Vertical Dragging'), + onDragUpdate: (DragUpdateDetails details) { + dragDelta += details.delta; + updated++; + }, + axis: Axis.vertical, + ), + Draggable( + data: 3, + child: const Text('Horizontal Source'), + feedback: const Text('Horizontal Dragging'), + onDragUpdate: (DragUpdateDetails details) { + dragDelta += details.delta; + updated++; + }, + axis: Axis.horizontal, + ), + ], + ), + ); + } + + testWidgets('Null axis onDragUpdate called only if draggable moves in any direction', (WidgetTester tester) async { + await tester.pumpWidget(build()); + + expect(updated, 0); + expect(find.text('Source'), findsOneWidget); + expect(find.text('Dragging'), findsNothing); + + final Offset firstLocation = tester.getCenter(find.text('Source')); + final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7); + await tester.pump(); + + expect(updated, 0); + expect(find.text('Source'), findsOneWidget); + expect(find.text('Dragging'), findsOneWidget); + + await gesture.moveBy(const Offset(10, 10)); + await tester.pump(); + + expect(updated, 1); + + await gesture.moveBy(Offset.zero); + await tester.pump(); + + expect(updated, 1); + + await gesture.up(); + await tester.pump(); + + expect(updated, 1); + expect(find.text('Source'), findsOneWidget); + expect(find.text('Dragging'), findsNothing); + expect(dragDelta.dx, 10); + expect(dragDelta.dy, 10); + }); + + testWidgets('Vertical axis onDragUpdate only called if draggable moves vertical', (WidgetTester tester) async { + await tester.pumpWidget(build()); + + expect(updated, 0); + expect(find.text('Vertical Source'), findsOneWidget); + expect(find.text('Vertical Dragging'), findsNothing); + + final Offset firstLocation = tester.getCenter(find.text('Vertical Source')); + final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7); + await tester.pump(); + + expect(updated, 0); + expect(find.text('Vertical Source'), findsOneWidget); + expect(find.text('Vertical Dragging'), findsOneWidget); + + await gesture.moveBy(const Offset(0, 10)); + await tester.pump(); + + expect(updated, 1); + + await gesture.moveBy(const Offset(10 , 0)); + await tester.pump(); + + expect(updated, 1); + + await gesture.up(); + await tester.pump(); + + expect(updated, 1); + expect(find.text('Vertical Source'), findsOneWidget); + expect(find.text('Vertical Dragging'), findsNothing); + expect(dragDelta.dx, 0); + expect(dragDelta.dy, 10); + }); + + testWidgets('Horizontal axis onDragUpdate only called if draggable moves horizontal', (WidgetTester tester) async { + await tester.pumpWidget(build()); + + expect(updated, 0); + expect(find.text('Horizontal Source'), findsOneWidget); + expect(find.text('Horizontal Dragging'), findsNothing); + + final Offset firstLocation = tester.getCenter(find.text('Horizontal Source')); + final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7); + await tester.pump(); + + expect(updated, 0); + expect(find.text('Horizontal Source'), findsOneWidget); + expect(find.text('Horizontal Dragging'), findsOneWidget); + + await gesture.moveBy(const Offset(0, 10)); + await tester.pump(); + + expect(updated, 0); + + await gesture.moveBy(const Offset(10 , 0)); + await tester.pump(); + + expect(updated, 1); + + await gesture.up(); + await tester.pump(); + + expect(updated, 1); + expect(find.text('Horizontal Source'), findsOneWidget); + expect(find.text('Horizontal Dragging'), findsNothing); + expect(dragDelta.dx, 10); + expect(dragDelta.dy, 0); + }); + }); testWidgets('Drag and drop - onDraggableCanceled not called if dropped on accepting target', (WidgetTester tester) async { final List accepted = [];