Unverified Commit 5159ab35 authored by David Shuckerow's avatar David Shuckerow Committed by GitHub

Add the ability to limit Draggables to a single axis (#17587)

* Add a draggable axis restrictor and tests
parent c4cb0ecf
...@@ -92,6 +92,7 @@ class Draggable<T> extends StatefulWidget { ...@@ -92,6 +92,7 @@ class Draggable<T> extends StatefulWidget {
@required this.child, @required this.child,
@required this.feedback, @required this.feedback,
this.data, this.data,
this.axis,
this.childWhenDragging, this.childWhenDragging,
this.feedbackOffset: Offset.zero, this.feedbackOffset: Offset.zero,
this.dragAnchor: DragAnchor.child, this.dragAnchor: DragAnchor.child,
...@@ -109,6 +110,19 @@ class Draggable<T> extends StatefulWidget { ...@@ -109,6 +110,19 @@ class Draggable<T> extends StatefulWidget {
/// The data that will be dropped by this draggable. /// The data that will be dropped by this draggable.
final T data; 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. /// The widget below this widget in the tree.
/// ///
/// This widget displays [child] when zero drags are under way. If /// This widget displays [child] when zero drags are under way. If
...@@ -164,6 +178,9 @@ class Draggable<T> extends StatefulWidget { ...@@ -164,6 +178,9 @@ class Draggable<T> extends StatefulWidget {
/// affinity, pointer motion in any direction will result in a drag rather /// affinity, pointer motion in any direction will result in a drag rather
/// than in a scroll because the draggable widget, being the more specific /// than in a scroll because the draggable widget, being the more specific
/// widget, will out-compete the [Scrollable] for vertical gestures. /// 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; final Axis affinity;
/// How many simultaneous drags to support. /// How many simultaneous drags to support.
...@@ -229,6 +246,7 @@ class LongPressDraggable<T> extends Draggable<T> { ...@@ -229,6 +246,7 @@ class LongPressDraggable<T> extends Draggable<T> {
@required Widget child, @required Widget child,
@required Widget feedback, @required Widget feedback,
T data, T data,
Axis axis,
Widget childWhenDragging, Widget childWhenDragging,
Offset feedbackOffset: Offset.zero, Offset feedbackOffset: Offset.zero,
DragAnchor dragAnchor: DragAnchor.child, DragAnchor dragAnchor: DragAnchor.child,
...@@ -241,6 +259,7 @@ class LongPressDraggable<T> extends Draggable<T> { ...@@ -241,6 +259,7 @@ class LongPressDraggable<T> extends Draggable<T> {
child: child, child: child,
feedback: feedback, feedback: feedback,
data: data, data: data,
axis: axis,
childWhenDragging: childWhenDragging, childWhenDragging: childWhenDragging,
feedbackOffset: feedbackOffset, feedbackOffset: feedbackOffset,
dragAnchor: dragAnchor, dragAnchor: dragAnchor,
...@@ -311,7 +330,7 @@ class _DraggableState<T> extends State<Draggable<T>> { ...@@ -311,7 +330,7 @@ class _DraggableState<T> extends State<Draggable<T>> {
break; break;
case DragAnchor.pointer: case DragAnchor.pointer:
dragStartPoint = Offset.zero; dragStartPoint = Offset.zero;
break; break;
} }
setState(() { setState(() {
_activeCount += 1; _activeCount += 1;
...@@ -319,6 +338,7 @@ class _DraggableState<T> extends State<Draggable<T>> { ...@@ -319,6 +338,7 @@ class _DraggableState<T> extends State<Draggable<T>> {
final _DragAvatar<T> avatar = new _DragAvatar<T>( final _DragAvatar<T> avatar = new _DragAvatar<T>(
overlayState: Overlay.of(context, debugRequiredFor: widget), overlayState: Overlay.of(context, debugRequiredFor: widget),
data: widget.data, data: widget.data,
axis: widget.axis,
initialPosition: position, initialPosition: position,
dragStartPoint: dragStartPoint, dragStartPoint: dragStartPoint,
feedback: widget.feedback, feedback: widget.feedback,
...@@ -471,6 +491,7 @@ class _DragAvatar<T> extends Drag { ...@@ -471,6 +491,7 @@ class _DragAvatar<T> extends Drag {
_DragAvatar({ _DragAvatar({
@required this.overlayState, @required this.overlayState,
this.data, this.data,
this.axis,
Offset initialPosition, Offset initialPosition,
this.dragStartPoint: Offset.zero, this.dragStartPoint: Offset.zero,
this.feedback, this.feedback,
...@@ -486,6 +507,7 @@ class _DragAvatar<T> extends Drag { ...@@ -486,6 +507,7 @@ class _DragAvatar<T> extends Drag {
} }
final T data; final T data;
final Axis axis;
final Offset dragStartPoint; final Offset dragStartPoint;
final Widget feedback; final Widget feedback;
final Offset feedbackOffset; final Offset feedbackOffset;
...@@ -500,15 +522,16 @@ class _DragAvatar<T> extends Drag { ...@@ -500,15 +522,16 @@ class _DragAvatar<T> extends Drag {
@override @override
void update(DragUpdateDetails details) { void update(DragUpdateDetails details) {
_position += details.delta; _position += _restrictAxis(details.delta);
updateDrag(_position); updateDrag(_position);
} }
@override @override
void end(DragEndDetails details) { void end(DragEndDetails details) {
finishDrag(_DragEndKind.dropped, details.velocity); finishDrag(_DragEndKind.dropped, _restrictVelocityAxis(details.velocity));
} }
@override @override
void cancel() { void cancel() {
finishDrag(_DragEndKind.canceled); finishDrag(_DragEndKind.canceled);
...@@ -600,4 +623,23 @@ class _DragAvatar<T> extends Drag { ...@@ -600,4 +623,23 @@ class _DragAvatar<T> 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);
}
} }
...@@ -586,6 +586,134 @@ void main() { ...@@ -586,6 +586,134 @@ void main() {
events.clear(); events.clear();
}); });
group('Drag and drop - Draggables with a set axis only move along that axis', () {
final List<String> events = <String>[];
Widget build() {
return new MaterialApp(
home: new ListView(
scrollDirection: Axis.horizontal,
children: <Widget>[
new DragTarget<int>(
builder: (BuildContext context, List<int> data, List<dynamic> rejects) {
return const Text('Target');
},
onAccept: (int data) {
events.add('drop $data');
}
),
new Container(width: 400.0),
const Draggable<int>(
data: 1,
child: const Text('H'),
feedback: const Text('H'),
childWhenDragging: const SizedBox(),
axis: Axis.horizontal,
),
const Draggable<int>(
data: 2,
child: const Text('V'),
feedback: const Text('V'),
childWhenDragging: const SizedBox(),
axis: Axis.vertical,
),
const Draggable<int>(
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 { testWidgets('Drag and drop - onDraggableCanceled not called if dropped on accepting target', (WidgetTester tester) async {
final List<int> accepted = <int>[]; final List<int> accepted = <int>[];
bool onDraggableCanceledCalled = false; bool onDraggableCanceledCalled = false;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment