Add the ability to limit Draggables to a single axis (#17587)
* Add a draggable axis restrictor and tests
This commit is contained in:
parent
c4cb0ecff3
commit
5159ab3513
@ -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 {
|
|||||||
/// 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 {
|
|||||||
/// 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> {
|
|||||||
@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> {
|
|||||||
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,
|
||||||
@ -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 {
|
|||||||
_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 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
|||||||
|
|
||||||
@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 {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() {
|
|||||||
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;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user