From 5159ab35133d02e5d205d73ecc2375548a44fabe Mon Sep 17 00:00:00 2001 From: David Shuckerow Date: Tue, 15 May 2018 10:09:25 -0700 Subject: [PATCH] Add the ability to limit Draggables to a single axis (#17587) * Add a draggable axis restrictor and tests --- .../flutter/lib/src/widgets/drag_target.dart | 48 ++++++- .../flutter/test/widgets/draggable_test.dart | 128 ++++++++++++++++++ 2 files changed, 173 insertions(+), 3 deletions(-) diff --git a/packages/flutter/lib/src/widgets/drag_target.dart b/packages/flutter/lib/src/widgets/drag_target.dart index 2d8c250ccc..67adc9923a 100644 --- a/packages/flutter/lib/src/widgets/drag_target.dart +++ b/packages/flutter/lib/src/widgets/drag_target.dart @@ -92,6 +92,7 @@ class Draggable extends StatefulWidget { @required this.child, @required this.feedback, this.data, + this.axis, this.childWhenDragging, this.feedbackOffset: Offset.zero, this.dragAnchor: DragAnchor.child, @@ -109,6 +110,19 @@ class Draggable extends StatefulWidget { /// The data that will be dropped by this draggable. final T data; + /// The [Axis] to restrict this draggable's movement, if specified. + /// + /// When axis is set to [Axis.horizontal], this widget can only be dragged + /// horizontally. Behavior is similar for [Axis.vertical]. + /// + /// Defaults to allowing drag on both [Axis.horizontal] and [Axis.vertical]. + /// + /// When null, allows drag on both [Axis.horizontal] and [Axis.vertical]. + /// + /// For the direction of gestures this widget competes with to start a drag + /// event, see [Draggable.affinity]. + final Axis axis; + /// The widget below this widget in the tree. /// /// This widget displays [child] when zero drags are under way. If @@ -164,6 +178,9 @@ class Draggable extends StatefulWidget { /// affinity, pointer motion in any direction will result in a drag rather /// than in a scroll because the draggable widget, being the more specific /// widget, will out-compete the [Scrollable] for vertical gestures. + /// + /// For the directions this widget can be dragged in after the drag event + /// starts, see [Draggable.axis]. final Axis affinity; /// How many simultaneous drags to support. @@ -229,6 +246,7 @@ class LongPressDraggable extends Draggable { @required Widget child, @required Widget feedback, T data, + Axis axis, Widget childWhenDragging, Offset feedbackOffset: Offset.zero, DragAnchor dragAnchor: DragAnchor.child, @@ -241,6 +259,7 @@ class LongPressDraggable extends Draggable { child: child, feedback: feedback, data: data, + axis: axis, childWhenDragging: childWhenDragging, feedbackOffset: feedbackOffset, dragAnchor: dragAnchor, @@ -311,7 +330,7 @@ class _DraggableState extends State> { break; case DragAnchor.pointer: dragStartPoint = Offset.zero; - break; + break; } setState(() { _activeCount += 1; @@ -319,6 +338,7 @@ class _DraggableState extends State> { final _DragAvatar avatar = new _DragAvatar( overlayState: Overlay.of(context, debugRequiredFor: widget), data: widget.data, + axis: widget.axis, initialPosition: position, dragStartPoint: dragStartPoint, feedback: widget.feedback, @@ -471,6 +491,7 @@ class _DragAvatar extends Drag { _DragAvatar({ @required this.overlayState, this.data, + this.axis, Offset initialPosition, this.dragStartPoint: Offset.zero, this.feedback, @@ -486,6 +507,7 @@ class _DragAvatar extends Drag { } final T data; + final Axis axis; final Offset dragStartPoint; final Widget feedback; final Offset feedbackOffset; @@ -500,15 +522,16 @@ class _DragAvatar extends Drag { @override void update(DragUpdateDetails details) { - _position += details.delta; + _position += _restrictAxis(details.delta); updateDrag(_position); } @override void end(DragEndDetails details) { - finishDrag(_DragEndKind.dropped, details.velocity); + finishDrag(_DragEndKind.dropped, _restrictVelocityAxis(details.velocity)); } + @override void cancel() { finishDrag(_DragEndKind.canceled); @@ -600,4 +623,23 @@ class _DragAvatar extends Drag { ) ); } + + Velocity _restrictVelocityAxis(Velocity velocity) { + if (axis == null) { + return velocity; + } + return new Velocity( + pixelsPerSecond: _restrictAxis(velocity.pixelsPerSecond), + ); + } + + Offset _restrictAxis(Offset offset) { + if (axis == null) { + return offset; + } + if (axis == Axis.horizontal) { + return new Offset(offset.dx, 0.0); + } + return new Offset(0.0, offset.dy); + } } diff --git a/packages/flutter/test/widgets/draggable_test.dart b/packages/flutter/test/widgets/draggable_test.dart index 0c68d1ccb9..40da011982 100644 --- a/packages/flutter/test/widgets/draggable_test.dart +++ b/packages/flutter/test/widgets/draggable_test.dart @@ -586,6 +586,134 @@ void main() { events.clear(); }); + group('Drag and drop - Draggables with a set axis only move along that axis', () { + final List events = []; + + Widget build() { + return new MaterialApp( + home: new ListView( + scrollDirection: Axis.horizontal, + children: [ + new DragTarget( + builder: (BuildContext context, List data, List rejects) { + return const Text('Target'); + }, + onAccept: (int data) { + events.add('drop $data'); + } + ), + new Container(width: 400.0), + const Draggable( + data: 1, + child: const Text('H'), + feedback: const Text('H'), + childWhenDragging: const SizedBox(), + axis: Axis.horizontal, + ), + const Draggable( + data: 2, + child: const Text('V'), + feedback: const Text('V'), + childWhenDragging: const SizedBox(), + axis: Axis.vertical, + ), + const Draggable( + data: 3, + child: const Text('N'), + feedback: const Text('N'), + childWhenDragging: const SizedBox(), + ), + new Container(width: 500.0), + new Container(width: 500.0), + new Container(width: 500.0), + new Container(width: 500.0), + ], + ), + ); + } + testWidgets('Null axis draggable moves along all axes', (WidgetTester tester) async { + await tester.pumpWidget(build()); + final Offset firstLocation = tester.getTopLeft(find.text('N')); + final Offset secondLocation = firstLocation + const Offset(300.0, 300.0); + final Offset thirdLocation = firstLocation + const Offset(-300.0, -300.0); + final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7); + await tester.pump(); + await gesture.moveTo(secondLocation); + await tester.pump(); + expect(tester.getTopLeft(find.text('N')), secondLocation); + await gesture.moveTo(thirdLocation); + await tester.pump(); + expect(tester.getTopLeft(find.text('N')), thirdLocation); + }); + + testWidgets('Horizontal axis draggable moves horizontally', (WidgetTester tester) async { + await tester.pumpWidget(build()); + final Offset firstLocation = tester.getTopLeft(find.text('H')); + final Offset secondLocation = firstLocation + const Offset(300.0, 0.0); + final Offset thirdLocation = firstLocation + const Offset(-300.0, 0.0); + final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7); + await tester.pump(); + await gesture.moveTo(secondLocation); + await tester.pump(); + expect(tester.getTopLeft(find.text('H')), secondLocation); + await gesture.moveTo(thirdLocation); + await tester.pump(); + expect(tester.getTopLeft(find.text('H')), thirdLocation); + }); + + testWidgets('Horizontal axis draggable does not move vertically', (WidgetTester tester) async { + await tester.pumpWidget(build()); + final Offset firstLocation = tester.getTopLeft(find.text('H')); + final Offset secondDragLocation = firstLocation + const Offset(300.0, 200.0); + // The horizontal drag widget won't scroll vertically. + final Offset secondWidgetLocation = firstLocation + const Offset(300.0, 0.0); + final Offset thirdDragLocation = firstLocation + const Offset(-300.0, -200.0); + final Offset thirdWidgetLocation = firstLocation + const Offset(-300.0, 0.0); + final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7); + await tester.pump(); + await gesture.moveTo(secondDragLocation); + await tester.pump(); + expect(tester.getTopLeft(find.text('H')), secondWidgetLocation); + await gesture.moveTo(thirdDragLocation); + await tester.pump(); + expect(tester.getTopLeft(find.text('H')), thirdWidgetLocation); + }); + + testWidgets('Vertical axis draggable moves vertically', (WidgetTester tester) async { + await tester.pumpWidget(build()); + final Offset firstLocation = tester.getTopLeft(find.text('V')); + final Offset secondLocation = firstLocation + const Offset(0.0, 300.0); + final Offset thirdLocation = firstLocation + const Offset(0.0, -300.0); + final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7); + await tester.pump(); + await gesture.moveTo(secondLocation); + await tester.pump(); + expect(tester.getTopLeft(find.text('V')), secondLocation); + await gesture.moveTo(thirdLocation); + await tester.pump(); + expect(tester.getTopLeft(find.text('V')), thirdLocation); + }); + + testWidgets('Vertical axis draggable does not move horizontally', (WidgetTester tester) async { + await tester.pumpWidget(build()); + final Offset firstLocation = tester.getTopLeft(find.text('V')); + final Offset secondDragLocation = firstLocation + const Offset(200.0, 300.0); + // The vertical drag widget won't scroll horizontally. + final Offset secondWidgetLocation = firstLocation + const Offset(0.0, 300.0); + final Offset thirdDragLocation = firstLocation + const Offset(-200.0, -300.0); + final Offset thirdWidgetLocation = firstLocation + const Offset(0.0, -300.0); + final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7); + await tester.pump(); + await gesture.moveTo(secondDragLocation); + await tester.pump(); + expect(tester.getTopLeft(find.text('V')), secondWidgetLocation); + await gesture.moveTo(thirdDragLocation); + await tester.pump(); + expect(tester.getTopLeft(find.text('V')), thirdWidgetLocation); + }); + }); + + testWidgets('Drag and drop - onDraggableCanceled not called if dropped on accepting target', (WidgetTester tester) async { final List accepted = []; bool onDraggableCanceledCalled = false;