From 347f7bac9434c49e5a800691d6bf62aeddf80515 Mon Sep 17 00:00:00 2001 From: Chinmoy Date: Sat, 26 Aug 2023 02:54:05 +0530 Subject: [PATCH] Adds callback onWillAcceptWithDetails in DragTarget. (#131545) This PR adds onWillAcceptWithDetails callback to DragTarget to get information about offset. Fixes: #131378 This PR is subject to changes based on #131542 --- .../flutter/lib/src/widgets/drag_target.dart | 34 ++- .../flutter/test/widgets/draggable_test.dart | 234 ++++++++++++++++++ 2 files changed, 266 insertions(+), 2 deletions(-) diff --git a/packages/flutter/lib/src/widgets/drag_target.dart b/packages/flutter/lib/src/widgets/drag_target.dart index f55806353b..a2ed56a7f3 100644 --- a/packages/flutter/lib/src/widgets/drag_target.dart +++ b/packages/flutter/lib/src/widgets/drag_target.dart @@ -20,6 +20,12 @@ import 'view.dart'; /// Used by [DragTarget.onWillAccept]. typedef DragTargetWillAccept = bool Function(T? data); +/// Signature for determining whether the given data will be accepted by a [DragTarget], +/// based on provided information. +/// +/// Used by [DragTarget.onWillAcceptWithDetails]. +typedef DragTargetWillAcceptWithDetails = bool Function(DragTargetDetails details); + /// Signature for causing a [DragTarget] to accept the given data. /// /// Used by [DragTarget.onAccept]. @@ -612,12 +618,13 @@ class DragTarget extends StatefulWidget { super.key, required this.builder, this.onWillAccept, + this.onWillAcceptWithDetails, this.onAccept, this.onAcceptWithDetails, this.onLeave, this.onMove, this.hitTestBehavior = HitTestBehavior.translucent, - }); + }) : assert(onWillAccept == null || onWillAcceptWithDetails == null, "Don't pass both onWillAccept and onWillAcceptWithDetails."); /// Called to build the contents of this widget. /// @@ -631,8 +638,25 @@ class DragTarget extends StatefulWidget { /// Called when a piece of data enters the target. This will be followed by /// either [onAccept] and [onAcceptWithDetails], if the data is dropped, or /// [onLeave], if the drag leaves the target. + /// + /// Equivalent to [onWillAcceptWithDetails], but only includes the data. + /// + /// Must not be provided if [onWillAcceptWithDetails] is provided. final DragTargetWillAccept? onWillAccept; + /// Called to determine whether this widget is interested in receiving a given + /// piece of data being dragged over this drag target. + /// + /// Called when a piece of data enters the target. This will be followed by + /// either [onAccept] and [onAcceptWithDetails], if the data is dropped, or + /// [onLeave], if the drag leaves the target. + /// + /// Equivalent to [onWillAccept], but with information, including the data, + /// in a [DragTargetDetails]. + /// + /// Must not be provided if [onWillAccept] is provided. + final DragTargetWillAcceptWithDetails? onWillAcceptWithDetails; + /// Called when an acceptable piece of data was dropped over this drag target. /// /// Equivalent to [onAcceptWithDetails], but only includes the data. @@ -684,7 +708,13 @@ class _DragTargetState extends State> { bool didEnter(_DragAvatar avatar) { assert(!_candidateAvatars.contains(avatar)); assert(!_rejectedAvatars.contains(avatar)); - if (widget.onWillAccept == null || widget.onWillAccept!(avatar.data as T?)) { + final bool resolvedWillAccept = (widget.onWillAccept == null && + widget.onWillAcceptWithDetails == null) || + (widget.onWillAccept != null && + widget.onWillAccept!(avatar.data as T?)) || + (widget.onWillAcceptWithDetails != null && + widget.onWillAcceptWithDetails!(DragTargetDetails(data: avatar.data! as T, offset: avatar._lastOffset!))); + if (resolvedWillAccept) { setState(() { _candidateAvatars.add(avatar); }); diff --git a/packages/flutter/test/widgets/draggable_test.dart b/packages/flutter/test/widgets/draggable_test.dart index 3336a6feb6..ac91266e41 100644 --- a/packages/flutter/test/widgets/draggable_test.dart +++ b/packages/flutter/test/widgets/draggable_test.dart @@ -1293,6 +1293,83 @@ void main() { expect(onDraggableCanceledOffset, equals(Offset(secondLocation.dx, secondLocation.dy))); }); + testWidgets('Drag and drop - onDraggableCanceled called if dropped on non-accepting target with details', (WidgetTester tester) async { + final List accepted = []; + final List> acceptedDetails = >[]; + bool onDraggableCanceledCalled = false; + late Velocity onDraggableCanceledVelocity; + late Offset onDraggableCanceledOffset; + + await tester.pumpWidget(MaterialApp( + home: Column( + children: [ + Draggable( + data: 1, + feedback: const Text('Dragging'), + onDraggableCanceled: (Velocity velocity, Offset offset) { + onDraggableCanceledCalled = true; + onDraggableCanceledVelocity = velocity; + onDraggableCanceledOffset = offset; + }, + child: const Text('Source'), + ), + DragTarget( + builder: (BuildContext context, List data, List rejects) { + return const SizedBox( + height: 100.0, + child: Text('Target'), + ); + }, + onWillAcceptWithDetails: (DragTargetDetails details) => false, + onAccept: accepted.add, + onAcceptWithDetails: acceptedDetails.add, + ), + ], + ), + )); + + expect(accepted, isEmpty); + expect(acceptedDetails, isEmpty); + expect(find.text('Source'), findsOneWidget); + expect(find.text('Dragging'), findsNothing); + expect(find.text('Target'), findsOneWidget); + expect(onDraggableCanceledCalled, 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(acceptedDetails, isEmpty); + expect(find.text('Source'), findsOneWidget); + expect(find.text('Dragging'), findsOneWidget); + expect(find.text('Target'), findsOneWidget); + expect(onDraggableCanceledCalled, isFalse); + + final Offset secondLocation = tester.getCenter(find.text('Target')); + await gesture.moveTo(secondLocation); + await tester.pump(); + + expect(accepted, isEmpty); + expect(acceptedDetails, isEmpty); + expect(find.text('Source'), findsOneWidget); + expect(find.text('Dragging'), findsOneWidget); + expect(find.text('Target'), findsOneWidget); + expect(onDraggableCanceledCalled, isFalse); + + await gesture.up(); + await tester.pump(); + + expect(accepted, isEmpty); + expect(acceptedDetails, isEmpty); + expect(find.text('Source'), findsOneWidget); + expect(find.text('Dragging'), findsNothing); + expect(find.text('Target'), findsOneWidget); + expect(onDraggableCanceledCalled, isTrue); + expect(onDraggableCanceledVelocity, equals(Velocity.zero)); + expect(onDraggableCanceledOffset, equals(Offset(secondLocation.dx, secondLocation.dy))); + }); + testWidgets('Drag and drop - onDraggableCanceled called if dropped on non-accepting target with correct velocity', (WidgetTester tester) async { final List accepted = []; final List> acceptedDetails = >[]; @@ -1421,6 +1498,82 @@ void main() { ); }); + testWidgets('Drag and drop - onDragEnd not called if dropped on non-accepting target with details', (WidgetTester tester) async { + final List accepted = []; + final List> acceptedDetails = >[]; + bool onDragEndCalled = false; + late DraggableDetails onDragEndDraggableDetails; + await tester.pumpWidget(MaterialApp( + home: Column( + children: [ + Draggable( + data: 1, + feedback: const Text('Dragging'), + onDragEnd: (DraggableDetails details) { + onDragEndCalled = true; + onDragEndDraggableDetails = details; + }, + child: const Text('Source'), + ), + DragTarget( + builder: (BuildContext context, List data, List rejects) { + return const SizedBox(height: 100.0, child: Text('Target')); + }, + onWillAcceptWithDetails: (DragTargetDetails data) => false, + onAccept: accepted.add, + onAcceptWithDetails: acceptedDetails.add, + ), + ], + ), + )); + + expect(accepted, isEmpty); + expect(acceptedDetails, isEmpty); + expect(find.text('Source'), findsOneWidget); + expect(find.text('Dragging'), findsNothing); + expect(find.text('Target'), findsOneWidget); + expect(onDragEndCalled, 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(acceptedDetails, isEmpty); + expect(find.text('Source'), findsOneWidget); + expect(find.text('Dragging'), findsOneWidget); + expect(find.text('Target'), findsOneWidget); + expect(onDragEndCalled, isFalse); + + final Offset secondLocation = tester.getCenter(find.text('Target')); + await gesture.moveTo(secondLocation); + await tester.pump(); + + expect(accepted, isEmpty); + expect(acceptedDetails, isEmpty); + expect(find.text('Source'), findsOneWidget); + expect(find.text('Dragging'), findsOneWidget); + expect(find.text('Target'), findsOneWidget); + expect(onDragEndCalled, isFalse); + + await gesture.up(); + await tester.pump(); + + expect(accepted, isEmpty); + expect(acceptedDetails, isEmpty); + expect(find.text('Source'), findsOneWidget); + expect(find.text('Dragging'), findsNothing); + expect(find.text('Target'), findsOneWidget); + expect(onDragEndCalled, isTrue); + expect(onDragEndDraggableDetails, isNotNull); + expect(onDragEndDraggableDetails.wasAccepted, isFalse); + expect(onDragEndDraggableDetails.velocity, equals(Velocity.zero)); + expect( + onDragEndDraggableDetails.offset, + equals(Offset(secondLocation.dx, secondLocation.dy - firstLocation.dy)), + ); + }); + testWidgets('Drag and drop - DragTarget rebuilds with and without rejected data when a rejected draggable enters and leaves', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: Column( @@ -1628,6 +1781,77 @@ void main() { expect(onDragCompletedCalled, isFalse); }); + testWidgets('Drag and drop - onDragCompleted not called if dropped on non-accepting target with details', (WidgetTester tester) async { + final List accepted = []; + final List> acceptedDetails = >[]; + bool onDragCompletedCalled = false; + + await tester.pumpWidget(MaterialApp( + home: Column( + children: [ + Draggable( + data: 1, + feedback: const Text('Dragging'), + onDragCompleted: () { + onDragCompletedCalled = true; + }, + child: const Text('Source'), + ), + DragTarget( + builder: (BuildContext context, List data, List rejects) { + return const SizedBox( + height: 100.0, + child: Text('Target'), + ); + }, + onWillAcceptWithDetails: (DragTargetDetails data) => false, + onAccept: accepted.add, + onAcceptWithDetails: acceptedDetails.add, + ), + ], + ), + )); + + expect(accepted, isEmpty); + expect(acceptedDetails, 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(acceptedDetails, 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(acceptedDetails, 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(acceptedDetails, isEmpty); + expect(find.text('Source'), findsOneWidget); + expect(find.text('Dragging'), findsNothing); + expect(find.text('Target'), findsOneWidget); + expect(onDragCompletedCalled, isFalse); + }); + testWidgets('Drag and drop - onDragEnd called if dropped on accepting target', (WidgetTester tester) async { final List accepted = []; final List> acceptedDetails = >[]; @@ -3237,6 +3461,16 @@ void main() { expect(find.text('Dragging'), findsNothing); await gesture3.up(); }); + + testWidgets('throws error when both onWillAccept and onWillAcceptWithDetails are provided', (WidgetTester tester) async { + expect(() => DragTarget( + builder: (BuildContext context, List data, List rejects) { + return const SizedBox(height: 100.0, child: Text('Target')); + }, + onWillAccept: (int? data) => true, + onWillAcceptWithDetails: (DragTargetDetails details) => false, + ), throwsAssertionError); + }); } Future _testLongPressDraggableHapticFeedback({ required WidgetTester tester, required bool hapticFeedbackOnStart, required int expectedHapticFeedbackCount }) async {