From 5f9e069111add4231fbb1817cb6d6fa0e37cd43b Mon Sep 17 00:00:00 2001 From: Tim Lehmann Date: Wed, 1 May 2024 18:56:23 +0200 Subject: [PATCH] Draggable feedback positioning (#145647) Fixes a calculation in Draggable that was previously wrong when the target was transformed. --- .../flutter/lib/src/widgets/drag_target.dart | 11 +- .../flutter/test/widgets/draggable_test.dart | 127 ++++++++++++++++++ 2 files changed, 134 insertions(+), 4 deletions(-) diff --git a/packages/flutter/lib/src/widgets/drag_target.dart b/packages/flutter/lib/src/widgets/drag_target.dart index 8cc341582eb..98501daca49 100644 --- a/packages/flutter/lib/src/widgets/drag_target.dart +++ b/packages/flutter/lib/src/widgets/drag_target.dart @@ -833,6 +833,7 @@ class _DragAvatar extends Drag { final List<_DragTargetState> _enteredTargets = <_DragTargetState>[]; Offset _position; Offset? _lastOffset; + late Offset _overlayOffset; OverlayEntry? _entry; @override @@ -858,6 +859,10 @@ class _DragAvatar extends Drag { void updateDrag(Offset globalPosition) { _lastOffset = globalPosition - dragStartPoint; + final RenderBox box = overlayState.context.findRenderObject()! as RenderBox; + final Offset overlaySpaceOffset = box.globalToLocal(globalPosition); + _overlayOffset = overlaySpaceOffset - dragStartPoint; + _entry!.markNeedsBuild(); final HitTestResult result = HitTestResult(); WidgetsBinding.instance.hitTestInView(result, globalPosition + feedbackOffset, viewId); @@ -943,11 +948,9 @@ class _DragAvatar extends Drag { } Widget _build(BuildContext context) { - final RenderBox box = overlayState.context.findRenderObject()! as RenderBox; - final Offset overlayTopLeft = box.localToGlobal(Offset.zero); return Positioned( - left: _lastOffset!.dx - overlayTopLeft.dx, - top: _lastOffset!.dy - overlayTopLeft.dy, + left: _overlayOffset.dx, + top: _overlayOffset.dy, child: ExcludeSemantics( excluding: ignoringFeedbackSemantics, child: IgnorePointer( diff --git a/packages/flutter/test/widgets/draggable_test.dart b/packages/flutter/test/widgets/draggable_test.dart index 0432b8530c6..62fc73b19d3 100644 --- a/packages/flutter/test/widgets/draggable_test.dart +++ b/packages/flutter/test/widgets/draggable_test.dart @@ -967,6 +967,10 @@ void main() { await gesture.moveTo(thirdLocation); await tester.pump(); expect(tester.getTopLeft(find.text('N')), thirdLocation); + + // Finish gesture to release resources. + await gesture.up(); + await tester.pump(); }); testWidgets('Horizontal axis draggable moves horizontally', (WidgetTester tester) async { @@ -982,6 +986,10 @@ void main() { await gesture.moveTo(thirdLocation); await tester.pump(); expect(tester.getTopLeft(find.text('H')), thirdLocation); + + // Finish gesture to release resources. + await gesture.up(); + await tester.pump(); }); testWidgets('Horizontal axis draggable does not move vertically', (WidgetTester tester) async { @@ -1000,6 +1008,10 @@ void main() { await gesture.moveTo(thirdDragLocation); await tester.pump(); expect(tester.getTopLeft(find.text('H')), thirdWidgetLocation); + + // Finish gesture to release resources. + await gesture.up(); + await tester.pump(); }); testWidgets('Vertical axis draggable moves vertically', (WidgetTester tester) async { @@ -1015,6 +1027,10 @@ void main() { await gesture.moveTo(thirdLocation); await tester.pump(); expect(tester.getTopLeft(find.text('V')), thirdLocation); + + // Finish gesture to release resources. + await gesture.up(); + await tester.pump(); }); testWidgets('Vertical axis draggable does not move horizontally', (WidgetTester tester) async { @@ -1033,6 +1049,10 @@ void main() { await gesture.moveTo(thirdDragLocation); await tester.pump(); expect(tester.getTopLeft(find.text('V')), thirdWidgetLocation); + + // Finish gesture to release resources. + await gesture.up(); + await tester.pump(); }); }); @@ -1666,6 +1686,10 @@ void main() { expect(find.text('Dragging'), findsOneWidget); expect(find.text('Target'), findsOneWidget); expect(find.text('Rejected'), findsNothing); + + // Finish gesture to release resources. + await gesture.up(); + await tester.pump(); }); @@ -3099,6 +3123,10 @@ void main() { ), findsNothing, ); + + // Finish gesture to release resources. + await gesture.up(); + await tester.pump(); }); testWidgets('Drag feedback is put on root overlay with [rootOverlay] flag', (WidgetTester tester) async { @@ -3497,6 +3525,93 @@ void main() { await tester.pumpAndSettle(); }); + testWidgets('Drag and drop - feedback matches pointer in scaled MaterialApp', (WidgetTester tester) async { + await tester.pumpWidget(Transform.scale( + scale: 0.5, + child: const MaterialApp( + home: Scaffold( + body: Draggable( + data: 42, + feedback: Text('Feedback'), + child: Text('Source'), + ), + ), + ), + )); + + final Offset location = tester.getTopLeft(find.text('Source')); + final TestGesture gesture = await tester.startGesture(location); + final Offset secondLocation = location + const Offset(100, 100); + await gesture.moveTo(secondLocation); + await tester.pump(); + final Offset appTopLeft = tester.getTopLeft(find.byType(MaterialApp)); + expect(tester.getTopLeft(find.text('Source')), appTopLeft); + expect(tester.getTopLeft(find.text('Feedback')), secondLocation); + + // Finish gesture to release resources. + await gesture.up(); + await tester.pumpAndSettle(); + }); + + testWidgets('Drag and drop - childDragAnchorStrategy works in scaled MaterialApp', (WidgetTester tester) async { + final Key sourceKey = UniqueKey(); + final Key feedbackKey = UniqueKey(); + await tester.pumpWidget(Transform.scale( + scale: 0.5, + child: MaterialApp( + home: Scaffold( + body: Draggable( + data: 42, + feedback: Text('Text', key: feedbackKey), + child: Text('Text', key: sourceKey), + ), + ), + ), + )); + final Finder source = find.byKey(sourceKey); + final Finder feedback = find.byKey(feedbackKey); + + final TestGesture gesture = await tester.startGesture(tester.getCenter(source)); + await tester.pump(); + expect(tester.getTopLeft(source), tester.getTopLeft(feedback)); + + // Finish gesture to release resources. + await gesture.up(); + await tester.pumpAndSettle(); + }); + + testWidgets('Drag and drop - feedback matches pointer in rotated MaterialApp', (WidgetTester tester) async { + await tester.pumpWidget(Transform.rotate( + angle: 1, // ~57 degrees + child: const MaterialApp( + home: Scaffold( + body: Draggable( + data: 42, + feedback: Text('Feedback'), + child: Text('Source'), + ), + ), + ), + )); + + final Offset location = tester.getTopLeft(find.text('Source')); + final TestGesture gesture = await tester.startGesture(location); + final Offset secondLocation = location + const Offset(100, 100); + await gesture.moveTo(secondLocation); + await tester.pump(); + final Offset appTopLeft = tester.getTopLeft(find.byType(MaterialApp)); + expect(tester.getTopLeft(find.text('Source')), appTopLeft); + final Offset feedbackTopLeft = tester.getTopLeft(find.text('Feedback')); + + // Different rotations can incur rounding errors, this makes it more robust + expect(feedbackTopLeft.dx, moreOrLessEquals(secondLocation.dx)); + expect(feedbackTopLeft.dy, moreOrLessEquals(secondLocation.dy)); + + // Finish gesture to release resources. + await gesture.up(); + await tester.pumpAndSettle(); + }); + testWidgets('configurable Draggable hit test behavior', (WidgetTester tester) async { const HitTestBehavior hitTestBehavior = HitTestBehavior.deferToChild; @@ -3573,6 +3688,10 @@ void main() { await tester.tap(find.text('Draggable')); expect(onTap, true); + + // Finish gesture to release resources. + await gesture.up(); + await tester.pump(); }); testWidgets('configurable feedback ignore pointer behavior - LongPressDraggable', (WidgetTester tester) async { @@ -3604,6 +3723,10 @@ void main() { await tester.tap(find.text('Draggable')); expect(onTap, true); + + // Finish gesture to release resources. + await gesture.up(); + await tester.pump(); }); testWidgets('configurable DragTarget hit test behavior', (WidgetTester tester) async { @@ -3811,6 +3934,10 @@ Future _testChildAnchorFeedbackPosition({ required WidgetTester tester, do final Offset sourceTopLeft = tester.getTopLeft(find.text('Source')); final Offset dragOffset = secondLocation - firstLocation; expect(feedbackTopLeft, equals(sourceTopLeft + dragOffset)); + + // Finish gesture to release resources. + await gesture.up(); + await tester.pump(); } class DragTargetData { }