From 4b7e3494dd7540bc34bfe5ea907f28e2c3f18e8f Mon Sep 17 00:00:00 2001 From: "P.Y. Laligand" Date: Mon, 5 Jun 2017 16:38:33 -0700 Subject: [PATCH] Add drag completion callback to Draggable. (#10455) Fixes #10350 --- .../flutter/lib/src/widgets/drag_target.dart | 15 +- .../flutter/test/widgets/draggable_test.dart | 131 +++++++++++++++++- 2 files changed, 142 insertions(+), 4 deletions(-) diff --git a/packages/flutter/lib/src/widgets/drag_target.dart b/packages/flutter/lib/src/widgets/drag_target.dart index 2cba6ea3bc..2b289fbea7 100644 --- a/packages/flutter/lib/src/widgets/drag_target.dart +++ b/packages/flutter/lib/src/widgets/drag_target.dart @@ -94,7 +94,8 @@ class Draggable extends StatefulWidget { this.affinity, this.maxSimultaneousDrags, this.onDragStarted, - this.onDraggableCanceled + this.onDraggableCanceled, + this.onDragCompleted, }) : assert(child != null), assert(feedback != null), assert(maxSimultaneousDrags == null || maxSimultaneousDrags >= 0), @@ -182,6 +183,16 @@ class Draggable extends StatefulWidget { /// callback is still in the tree. final DraggableCanceledCallback onDraggableCanceled; + /// Called when the draggable is dropped and accepted by a [DragTarget]. + /// + /// This function might be called after this widget has been removed from the + /// tree. For example, if a drag was in progress when this widget was removed + /// from the tree and the drag ended up completing, this callback will + /// still be called. For this reason, implementations of this callback might + /// need to check [State.mounted] to check whether the state receiving the + /// callback is still in the tree. + final VoidCallback onDragCompleted; + /// Creates a gesture recognizer that recognizes the start of the drag. /// /// Subclasses can override this function to customize when they start @@ -313,6 +324,8 @@ class _DraggableState extends State> { _activeCount -= 1; _disposeRecognizerIfInactive(); } + if (wasAccepted && widget.onDragCompleted != null) + widget.onDragCompleted(); if (!wasAccepted && widget.onDraggableCanceled != null) widget.onDraggableCanceled(velocity, offset); } diff --git a/packages/flutter/test/widgets/draggable_test.dart b/packages/flutter/test/widgets/draggable_test.dart index ace5891501..4e6acb2666 100644 --- a/packages/flutter/test/widgets/draggable_test.dart +++ b/packages/flutter/test/widgets/draggable_test.dart @@ -518,7 +518,7 @@ void main() { events.clear(); }); - testWidgets('Drag and drop - onDraggableDropped not called if dropped on accepting target', (WidgetTester tester) async { + testWidgets('Drag and drop - onDraggableCanceled not called if dropped on accepting target', (WidgetTester tester) async { final List accepted = []; bool onDraggableCanceledCalled = false; @@ -579,7 +579,7 @@ void main() { expect(onDraggableCanceledCalled, isFalse); }); - testWidgets('Drag and drop - onDraggableDropped called if dropped on non-accepting target', (WidgetTester tester) async { + testWidgets('Drag and drop - onDraggableCanceled called if dropped on non-accepting target', (WidgetTester tester) async { final List accepted = []; bool onDraggableCanceledCalled = false; Velocity onDraggableCanceledVelocity; @@ -649,7 +649,7 @@ void main() { expect(onDraggableCanceledOffset, equals(new Offset(secondLocation.dx, secondLocation.dy))); }); - testWidgets('Drag and drop - onDraggableDropped called if dropped on non-accepting target with correct velocity', (WidgetTester tester) async { + testWidgets('Drag and drop - onDraggableCanceled called if dropped on non-accepting target with correct velocity', (WidgetTester tester) async { final List accepted = []; bool onDraggableCanceledCalled = false; Velocity onDraggableCanceledVelocity; @@ -699,6 +699,131 @@ void main() { expect(onDraggableCanceledOffset, equals(new Offset(flingStart.dx, flingStart.dy) + const Offset(0.0, 100.0))); }); + testWidgets('Drag and drop - onDragCompleted not called if dropped on non-accepting target', (WidgetTester tester) async { + final List accepted = []; + bool onDragCompletedCalled = false; + + await tester.pumpWidget(new MaterialApp( + home: new Column( + children: [ + new Draggable( + data: 1, + child: const Text('Source'), + feedback: const Text('Dragging'), + onDragCompleted: () { + onDragCompletedCalled = true; + } + ), + new DragTarget( + builder: (BuildContext context, List data, List rejects) { + return new Container( + height: 100.0, + child: const Text('Target') + ); + }, + onWillAccept: (int data) => false + ), + ] + ) + )); + + expect(accepted, isEmpty); + expect(find.text('Source'), findsOneWidget); + expect(find.text('Dragging'), findsNothing); + expect(find.text('Target'), findsOneWidget); + expect(onDragCompletedCalled, isFalse); + + final Offset firstLocation = tester.getTopLeft(find.text('Source')); + final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7); + await tester.pump(); + + expect(accepted, isEmpty); + expect(find.text('Source'), findsOneWidget); + expect(find.text('Dragging'), findsOneWidget); + expect(find.text('Target'), findsOneWidget); + expect(onDragCompletedCalled, isFalse); + + final Offset secondLocation = tester.getCenter(find.text('Target')); + await gesture.moveTo(secondLocation); + await tester.pump(); + + expect(accepted, isEmpty); + expect(find.text('Source'), findsOneWidget); + expect(find.text('Dragging'), findsOneWidget); + expect(find.text('Target'), findsOneWidget); + expect(onDragCompletedCalled, isFalse); + + await gesture.up(); + await tester.pump(); + + expect(accepted, isEmpty); + expect(find.text('Source'), findsOneWidget); + expect(find.text('Dragging'), findsNothing); + expect(find.text('Target'), findsOneWidget); + expect(onDragCompletedCalled, isFalse); + }); + + testWidgets('Drag and drop - onDragCompleted called if dropped on accepting target', (WidgetTester tester) async { + final List accepted = []; + bool onDragCompletedCalled = false; + + await tester.pumpWidget(new MaterialApp( + home: new Column( + children: [ + new Draggable( + data: 1, + child: const Text('Source'), + feedback: const Text('Dragging'), + onDragCompleted: () { + onDragCompletedCalled = true; + } + ), + new DragTarget( + builder: (BuildContext context, List data, List rejects) { + return new Container(height: 100.0, child: const Text('Target')); + }, + onAccept: accepted.add + ), + ] + ) + )); + + expect(accepted, isEmpty); + expect(find.text('Source'), findsOneWidget); + expect(find.text('Dragging'), findsNothing); + expect(find.text('Target'), findsOneWidget); + expect(onDragCompletedCalled, isFalse); + + final Offset firstLocation = tester.getCenter(find.text('Source')); + final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7); + await tester.pump(); + + expect(accepted, isEmpty); + expect(find.text('Source'), findsOneWidget); + expect(find.text('Dragging'), findsOneWidget); + expect(find.text('Target'), findsOneWidget); + expect(onDragCompletedCalled, isFalse); + + final Offset secondLocation = tester.getCenter(find.text('Target')); + await gesture.moveTo(secondLocation); + await tester.pump(); + + expect(accepted, isEmpty); + expect(find.text('Source'), findsOneWidget); + expect(find.text('Dragging'), findsOneWidget); + expect(find.text('Target'), findsOneWidget); + expect(onDragCompletedCalled, isFalse); + + await gesture.up(); + await tester.pump(); + + expect(accepted, equals([1])); + expect(find.text('Source'), findsOneWidget); + expect(find.text('Dragging'), findsNothing); + expect(find.text('Target'), findsOneWidget); + expect(onDragCompletedCalled, isTrue); + }); + testWidgets('Drag and drop - allow pass thru of unaccepted data test', (WidgetTester tester) async { final List acceptedInts = []; final List acceptedDoubles = [];