diff --git a/packages/flutter/lib/src/gestures/multidrag.dart b/packages/flutter/lib/src/gestures/multidrag.dart index eeb929707c..9e6fd719db 100644 --- a/packages/flutter/lib/src/gestures/multidrag.dart +++ b/packages/flutter/lib/src/gestures/multidrag.dart @@ -252,6 +252,61 @@ class ImmediateMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_Im } } + +class _HorizontalPointerState extends MultiDragPointerState { + _HorizontalPointerState(Point initialPosition) : super(initialPosition); + + void checkForResolutionAfterMove() { + assert(pendingDelta != null); + if (pendingDelta.dx.abs() > kTouchSlop) + resolve(GestureDisposition.accepted); + } + + void accepted(GestureMultiDragStartCallback starter) { + starter(initialPosition); + } +} + +class HorizontalMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_HorizontalPointerState> { + HorizontalMultiDragGestureRecognizer({ + PointerRouter pointerRouter, + GestureArena gestureArena, + GestureMultiDragStartCallback onStart + }) : super(pointerRouter: pointerRouter, gestureArena: gestureArena, onStart: onStart); + + _HorizontalPointerState createNewPointerState(PointerDownEvent event) { + return new _HorizontalPointerState(event.position); + } +} + + +class _VerticalPointerState extends MultiDragPointerState { + _VerticalPointerState(Point initialPosition) : super(initialPosition); + + void checkForResolutionAfterMove() { + assert(pendingDelta != null); + if (pendingDelta.dy.abs() > kTouchSlop) + resolve(GestureDisposition.accepted); + } + + void accepted(GestureMultiDragStartCallback starter) { + starter(initialPosition); + } +} + +class VerticalMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_VerticalPointerState> { + VerticalMultiDragGestureRecognizer({ + PointerRouter pointerRouter, + GestureArena gestureArena, + GestureMultiDragStartCallback onStart + }) : super(pointerRouter: pointerRouter, gestureArena: gestureArena, onStart: onStart); + + _VerticalPointerState createNewPointerState(PointerDownEvent event) { + return new _VerticalPointerState(event.position); + } +} + + class _DelayedPointerState extends MultiDragPointerState { _DelayedPointerState(Point initialPosition, Duration delay) : super(initialPosition) { assert(delay != null); diff --git a/packages/flutter/lib/src/widgets/drag_target.dart b/packages/flutter/lib/src/widgets/drag_target.dart index e045999d43..fc1ceaa3df 100644 --- a/packages/flutter/lib/src/widgets/drag_target.dart +++ b/packages/flutter/lib/src/widgets/drag_target.dart @@ -118,6 +118,70 @@ class Draggable extends DraggableBase { } } +/// Makes its child draggable. When competing with other gestures, +/// this will only start the drag horizontally. +class HorizontalDraggable extends DraggableBase { + HorizontalDraggable({ + Key key, + T data, + Widget child, + Widget childWhenDragging, + Widget feedback, + Offset feedbackOffset: Offset.zero, + DragAnchor dragAnchor: DragAnchor.child, + int maxSimultaneousDrags + }) : super( + key: key, + data: data, + child: child, + childWhenDragging: childWhenDragging, + feedback: feedback, + feedbackOffset: feedbackOffset, + dragAnchor: dragAnchor, + maxSimultaneousDrags: maxSimultaneousDrags + ); + + MultiDragGestureRecognizer createRecognizer(PointerRouter router, GestureArena arena, GestureMultiDragStartCallback starter) { + return new HorizontalMultiDragGestureRecognizer( + pointerRouter: router, + gestureArena: arena, + onStart: starter + ); + } +} + +/// Makes its child draggable. When competing with other gestures, +/// this will only start the drag vertically. +class VerticalDraggable extends DraggableBase { + VerticalDraggable({ + Key key, + T data, + Widget child, + Widget childWhenDragging, + Widget feedback, + Offset feedbackOffset: Offset.zero, + DragAnchor dragAnchor: DragAnchor.child, + int maxSimultaneousDrags + }) : super( + key: key, + data: data, + child: child, + childWhenDragging: childWhenDragging, + feedback: feedback, + feedbackOffset: feedbackOffset, + dragAnchor: dragAnchor, + maxSimultaneousDrags: maxSimultaneousDrags + ); + + MultiDragGestureRecognizer createRecognizer(PointerRouter router, GestureArena arena, GestureMultiDragStartCallback starter) { + return new VerticalMultiDragGestureRecognizer( + pointerRouter: router, + gestureArena: arena, + onStart: starter + ); + } +} + /// Makes its child draggable starting from long press. class LongPressDraggable extends DraggableBase { LongPressDraggable({ diff --git a/packages/flutter/test/widget/draggable_test.dart b/packages/flutter/test/widget/draggable_test.dart index f7b473f4db..9bd3c31406 100644 --- a/packages/flutter/test/widget/draggable_test.dart +++ b/packages/flutter/test/widget/draggable_test.dart @@ -338,6 +338,232 @@ void main() { tester.dispatchEvent(pointer.up(), firstLocation); tester.pump(); expect(events, equals(['drop'])); + }); + }); + + test('Drag and drop - horizontal and vertical draggables in vertical block', () { + testWidgets((WidgetTester tester) { + TestPointer pointer = new TestPointer(7); + + List events = []; + Point firstLocation, secondLocation, thirdLocation; + + tester.pumpWidget(new MaterialApp( + routes: { + '/': (RouteArguments args) { + return new Block( + children: [ + new DragTarget( + builder: (context, data, rejects) { + return new Text('Target'); + }, + onAccept: (data) { + events.add('drop $data'); + } + ), + new Container(height: 400.0), + new HorizontalDraggable( + data: 1, + child: new Text('H'), + feedback: new Text('Dragging') + ), + new VerticalDraggable( + data: 2, + child: new Text('V'), + feedback: new Text('Dragging') + ), + new Container(height: 500.0), + new Container(height: 500.0), + new Container(height: 500.0), + new Container(height: 500.0), + ] + ); + }, + } + )); + + expect(events, isEmpty); + expect(tester.findText('Target'), isNotNull); + expect(tester.findText('H'), isNotNull); + expect(tester.findText('V'), isNotNull); + + // vertical draggable drags vertically + expect(events, isEmpty); + firstLocation = tester.getCenter(tester.findText('V')); + secondLocation = tester.getCenter(tester.findText('Target')); + tester.dispatchEvent(pointer.down(firstLocation), firstLocation); + tester.pump(); + tester.dispatchEvent(pointer.move(secondLocation), firstLocation); + tester.pump(); + tester.dispatchEvent(pointer.up(), firstLocation); + tester.pump(); + expect(events, equals(['drop 2'])); + expect(tester.getCenter(tester.findText('Target')).y, greaterThan(0.0)); + events.clear(); + + // horizontal draggable drags horizontally + expect(events, isEmpty); + firstLocation = tester.getTopLeft(tester.findText('H')); + secondLocation = tester.getTopRight(tester.findText('H')); + thirdLocation = tester.getCenter(tester.findText('Target')); + tester.dispatchEvent(pointer.down(firstLocation), firstLocation); + tester.pump(); + tester.dispatchEvent(pointer.move(secondLocation), firstLocation); + tester.pump(); + tester.dispatchEvent(pointer.move(thirdLocation), firstLocation); + tester.pump(); + tester.dispatchEvent(pointer.up(), firstLocation); + tester.pump(); + expect(events, equals(['drop 1'])); + expect(tester.getCenter(tester.findText('Target')).y, greaterThan(0.0)); + events.clear(); + + // vertical draggable drags horizontally when there's no competition + // from other gesture detectors + expect(events, isEmpty); + firstLocation = tester.getTopLeft(tester.findText('V')); + secondLocation = tester.getTopRight(tester.findText('V')); + thirdLocation = tester.getCenter(tester.findText('Target')); + tester.dispatchEvent(pointer.down(firstLocation), firstLocation); + tester.pump(); + tester.dispatchEvent(pointer.move(secondLocation), firstLocation); + tester.pump(); + tester.dispatchEvent(pointer.move(thirdLocation), firstLocation); + tester.pump(); + tester.dispatchEvent(pointer.up(), firstLocation); + tester.pump(); + expect(events, equals(['drop 2'])); + expect(tester.getCenter(tester.findText('Target')).y, greaterThan(0.0)); + events.clear(); + + // horizontal draggable doesn't drag vertically when there is competition + // for vertical gestures + expect(events, isEmpty); + firstLocation = tester.getCenter(tester.findText('H')); + secondLocation = tester.getCenter(tester.findText('Target')); + tester.dispatchEvent(pointer.down(firstLocation), firstLocation); + tester.pump(); + tester.dispatchEvent(pointer.move(secondLocation), firstLocation); + tester.pump(); // scrolls off screen! + tester.dispatchEvent(pointer.up(), firstLocation); + tester.pump(); + expect(events, equals([])); + expect(tester.getCenter(tester.findText('Target')).y, lessThan(0.0)); + events.clear(); + + }); + }); + + test('Drag and drop - horizontal and vertical draggables in horizontal block', () { + testWidgets((WidgetTester tester) { + TestPointer pointer = new TestPointer(7); + + List events = []; + Point firstLocation, secondLocation, thirdLocation; + + tester.pumpWidget(new MaterialApp( + routes: { + '/': (RouteArguments args) { + return new Block( + scrollDirection: Axis.horizontal, + children: [ + new DragTarget( + builder: (context, data, rejects) { + return new Text('Target'); + }, + onAccept: (data) { + events.add('drop $data'); + } + ), + new Container(width: 400.0), + new HorizontalDraggable( + data: 1, + child: new Text('H'), + feedback: new Text('Dragging') + ), + new VerticalDraggable( + data: 2, + child: new Text('V'), + feedback: new Text('Dragging') + ), + new Container(width: 500.0), + new Container(width: 500.0), + new Container(width: 500.0), + new Container(width: 500.0), + ] + ); + }, + } + )); + + expect(events, isEmpty); + expect(tester.findText('Target'), isNotNull); + expect(tester.findText('H'), isNotNull); + expect(tester.findText('V'), isNotNull); + + // horizontal draggable drags horizontally + expect(events, isEmpty); + firstLocation = tester.getCenter(tester.findText('H')); + secondLocation = tester.getCenter(tester.findText('Target')); + tester.dispatchEvent(pointer.down(firstLocation), firstLocation); + tester.pump(); + tester.dispatchEvent(pointer.move(secondLocation), firstLocation); + tester.pump(); + tester.dispatchEvent(pointer.up(), firstLocation); + tester.pump(); + expect(events, equals(['drop 1'])); + expect(tester.getCenter(tester.findText('Target')).x, greaterThan(0.0)); + events.clear(); + + // vertical draggable drags vertically + expect(events, isEmpty); + firstLocation = tester.getTopLeft(tester.findText('V')); + secondLocation = tester.getBottomLeft(tester.findText('V')); + thirdLocation = tester.getCenter(tester.findText('Target')); + tester.dispatchEvent(pointer.down(firstLocation), firstLocation); + tester.pump(); + tester.dispatchEvent(pointer.move(secondLocation), firstLocation); + tester.pump(); + tester.dispatchEvent(pointer.move(thirdLocation), firstLocation); + tester.pump(); + tester.dispatchEvent(pointer.up(), firstLocation); + tester.pump(); + expect(events, equals(['drop 2'])); + expect(tester.getCenter(tester.findText('Target')).x, greaterThan(0.0)); + events.clear(); + + // horizontal draggable drags vertically when there's no competition + // from other gesture detectors + expect(events, isEmpty); + firstLocation = tester.getTopLeft(tester.findText('H')); + secondLocation = tester.getBottomLeft(tester.findText('H')); + thirdLocation = tester.getCenter(tester.findText('Target')); + tester.dispatchEvent(pointer.down(firstLocation), firstLocation); + tester.pump(); + tester.dispatchEvent(pointer.move(secondLocation), firstLocation); + tester.pump(); + tester.dispatchEvent(pointer.move(thirdLocation), firstLocation); + tester.pump(); + tester.dispatchEvent(pointer.up(), firstLocation); + tester.pump(); + expect(events, equals(['drop 1'])); + expect(tester.getCenter(tester.findText('Target')).x, greaterThan(0.0)); + events.clear(); + + // vertical draggable doesn't drag horizontally when there is competition + // for horizontal gestures + expect(events, isEmpty); + firstLocation = tester.getCenter(tester.findText('V')); + secondLocation = tester.getCenter(tester.findText('Target')); + tester.dispatchEvent(pointer.down(firstLocation), firstLocation); + tester.pump(); + tester.dispatchEvent(pointer.move(secondLocation), firstLocation); + tester.pump(); // scrolls off screen! + tester.dispatchEvent(pointer.up(), firstLocation); + tester.pump(); + expect(events, equals([])); + expect(tester.getCenter(tester.findText('Target')).x, lessThan(0.0)); + events.clear(); }); });