diff --git a/dev/manual_tests/lib/drag_and_drop.dart b/dev/manual_tests/lib/drag_and_drop.dart index 9af6d44c591..d3d6d8fa35e 100644 --- a/dev/manual_tests/lib/drag_and_drop.dart +++ b/dev/manual_tests/lib/drag_and_drop.dart @@ -116,6 +116,7 @@ class ExampleDragSource extends StatelessWidget { Offset feedbackOffset; DragAnchor anchor; + DragAnchorStrategy dragAnchorStrategy; if (!under) { feedback = Transform( transform: Matrix4.identity() @@ -124,9 +125,11 @@ class ExampleDragSource extends StatelessWidget { ); feedbackOffset = const Offset(0.0, -kFingerSize); anchor = DragAnchor.pointer; + dragAnchorStrategy = pointerDragAnchorStrategy; } else { feedbackOffset = Offset.zero; anchor = DragAnchor.child; + dragAnchorStrategy = childDragAnchorStrategy; } if (heavy) { @@ -143,7 +146,7 @@ class ExampleDragSource extends StatelessWidget { child: contents, feedback: feedback, feedbackOffset: feedbackOffset, - dragAnchor: anchor, + dragAnchorStrategy: dragAnchorStrategy, ); } } diff --git a/packages/flutter/lib/src/widgets/drag_target.dart b/packages/flutter/lib/src/widgets/drag_target.dart index 819f289f572..8304e8c8d8a 100644 --- a/packages/flutter/lib/src/widgets/drag_target.dart +++ b/packages/flutter/lib/src/widgets/drag_target.dart @@ -66,6 +66,24 @@ typedef DragTargetLeave = void Function(T? data); /// Used by [DragTarget.onMove]. typedef DragTargetMove = void Function(DragTargetDetails details); +/// Signature for the strategy that determines the drag start point. +/// +/// Used for the built-in strategies switched via [DragAnchor] and the optinally +/// injectable [Draggable.dragAnchorStrategy] +typedef DragAnchorStrategy = Offset Function(Draggable draggable, BuildContext context, Offset position); + +/// The default [DragAnchorStrategy] used when [Draggable.dragAnchor] is not set +/// or set to [DragAnchor.child] +Offset childDragAnchorStrategy(Draggable draggable, BuildContext context, Offset position) { + final RenderBox renderObject = context.findRenderObject()! as RenderBox; + return renderObject.globalToLocal(position); +} + +/// The [DragAnchorStrategy] used when [Draggable.dragAnchor] set to +/// [DragAnchor.pointer] +Offset pointerDragAnchorStrategy(Draggable draggable, BuildContext context, Offset position) { + return Offset.zero; +} /// Where the [Draggable] should be anchored during a drag. enum DragAnchor { /// Display the feedback anchored at the position of the original child. If @@ -191,7 +209,12 @@ class Draggable extends StatefulWidget { this.axis, this.childWhenDragging, this.feedbackOffset = Offset.zero, + @Deprecated( + 'Use dragAnchorStrategy instead. ' + 'This feature was deprecated after v2.1.0-10.0.pre.' + ) this.dragAnchor = DragAnchor.child, + this.dragAnchorStrategy, this.affinity, this.maxSimultaneousDrags, this.onDragStarted, @@ -263,8 +286,23 @@ class Draggable extends StatefulWidget { final Offset feedbackOffset; /// Where this widget should be anchored during a drag. + /// + /// This property is overridden by the [dragAnchorStrategy] if the latter is provided. + /// + /// Defaults to [DragAnchor.child]. + @Deprecated( + 'Use dragAnchorStrategy instead. ' + 'This feature was deprecated after v2.1.0-10.0.pre.' + ) final DragAnchor dragAnchor; + /// A strategy that is used by this draggable to get the the anchor offset when it is dragged. + /// + /// The anchor offset refers to the distance between the users' fingers and the [feedback] widget when this draggable is dragged. + /// + /// Defaults to [childDragAnchorStrategy] if the [dragAnchor] is set to [DragAnchor.child] or [pointerDragAnchorStrategy] if the [dragAnchor] is set to [DragAnchor.pointer]. + final DragAnchorStrategy? dragAnchorStrategy; + /// Whether the semantics of the [feedback] widget is ignored when building /// the semantics tree. /// @@ -308,7 +346,7 @@ class Draggable extends StatefulWidget { /// Called when the draggable starts being dragged. final VoidCallback? onDragStarted; - /// Called when the draggable is being dragged. + /// Called when the draggable is 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. @@ -487,14 +525,17 @@ class _DraggableState extends State> { if (widget.maxSimultaneousDrags != null && _activeCount >= widget.maxSimultaneousDrags!) return null; final Offset dragStartPoint; - switch (widget.dragAnchor) { - case DragAnchor.child: - final RenderBox renderObject = context.findRenderObject()! as RenderBox; - dragStartPoint = renderObject.globalToLocal(position); - break; - case DragAnchor.pointer: - dragStartPoint = Offset.zero; - break; + if (widget.dragAnchorStrategy == null) { + switch (widget.dragAnchor) { + case DragAnchor.child: + dragStartPoint = childDragAnchorStrategy(widget, context, position); + break; + case DragAnchor.pointer: + dragStartPoint = pointerDragAnchorStrategy(widget, context, position); + break; + } + } else { + dragStartPoint = widget.dragAnchorStrategy!(widget, context, position); } setState(() { _activeCount += 1; diff --git a/packages/flutter/test/widgets/draggable_test.dart b/packages/flutter/test/widgets/draggable_test.dart index 73671e6e043..f63194a11b2 100644 --- a/packages/flutter/test/widgets/draggable_test.dart +++ b/packages/flutter/test/widgets/draggable_test.dart @@ -2540,7 +2540,6 @@ void main() { expect(find.text('Target'), findsOneWidget); expect(onDragCompletedCalled, isFalse); - final Offset secondLocation = tester.getCenter(find.text('Target')); await gesture.moveTo(secondLocation); await tester.pump(); @@ -3019,6 +3018,30 @@ void main() { semantics.dispose(); }); + testWidgets('Drag and drop - when a dragAnchorStrategy is provided it gets called', (WidgetTester tester) async { + bool dragAnchorStrategyCalled = false; + + await tester.pumpWidget(MaterialApp( + home: Column( + children: [ + Draggable( + child: const Text('Source'), + feedback: const Text('Feedback'), + dragAnchorStrategy: (Draggable widget, BuildContext context, Offset position) { + dragAnchorStrategyCalled = true; + return const Offset(0, 0); + } + ) + ], + ), + )); + + final Offset location = tester.getCenter(find.text('Source')); + await tester.startGesture(location, pointer: 7); + + expect(dragAnchorStrategyCalled, true); + }); + testWidgets('configurable Draggable hit test behavior', (WidgetTester tester) async { const HitTestBehavior hitTestBehavior = HitTestBehavior.deferToChild;