Commit f90ccf48 authored by Andrew Wilson's avatar Andrew Wilson

Add a callabck when a Draggable is dropped without being accepted.

parent 03830d56
...@@ -15,6 +15,9 @@ typedef bool DragTargetWillAccept<T>(T data); ...@@ -15,6 +15,9 @@ typedef bool DragTargetWillAccept<T>(T data);
typedef void DragTargetAccept<T>(T data); typedef void DragTargetAccept<T>(T data);
typedef Widget DragTargetBuilder<T>(BuildContext context, List<T> candidateData, List<dynamic> rejectedData); typedef Widget DragTargetBuilder<T>(BuildContext context, List<T> candidateData, List<dynamic> rejectedData);
/// Called when a [Draggable] is dropped without being accepted by a [DragTarget].
typedef void OnDraggableCanceled(Velocity velocity, Offset offset);
/// Where the [Draggable] should be anchored during a drag. /// Where the [Draggable] should be anchored during a drag.
enum DragAnchor { enum DragAnchor {
/// Display the feedback anchored at the position of the original child. If /// Display the feedback anchored at the position of the original child. If
...@@ -45,7 +48,8 @@ abstract class DraggableBase<T> extends StatefulWidget { ...@@ -45,7 +48,8 @@ abstract class DraggableBase<T> extends StatefulWidget {
this.feedback, this.feedback,
this.feedbackOffset: Offset.zero, this.feedbackOffset: Offset.zero,
this.dragAnchor: DragAnchor.child, this.dragAnchor: DragAnchor.child,
this.maxSimultaneousDrags this.maxSimultaneousDrags,
this.onDraggableCanceled
}) : super(key: key) { }) : super(key: key) {
assert(child != null); assert(child != null);
assert(feedback != null); assert(feedback != null);
...@@ -80,6 +84,9 @@ abstract class DraggableBase<T> extends StatefulWidget { ...@@ -80,6 +84,9 @@ abstract class DraggableBase<T> extends StatefulWidget {
/// dragged at a time. /// dragged at a time.
final int maxSimultaneousDrags; final int maxSimultaneousDrags;
/// Called when the draggable is dropped without being accepted by a [DragTarget].
final OnDraggableCanceled onDraggableCanceled;
/// Should return a new MultiDragGestureRecognizer instance /// Should return a new MultiDragGestureRecognizer instance
/// constructed with the given arguments. /// constructed with the given arguments.
MultiDragGestureRecognizer<MultiDragPointerState> createRecognizer(GestureMultiDragStartCallback onStart); MultiDragGestureRecognizer<MultiDragPointerState> createRecognizer(GestureMultiDragStartCallback onStart);
...@@ -98,7 +105,8 @@ class Draggable<T> extends DraggableBase<T> { ...@@ -98,7 +105,8 @@ class Draggable<T> extends DraggableBase<T> {
Widget feedback, Widget feedback,
Offset feedbackOffset: Offset.zero, Offset feedbackOffset: Offset.zero,
DragAnchor dragAnchor: DragAnchor.child, DragAnchor dragAnchor: DragAnchor.child,
int maxSimultaneousDrags int maxSimultaneousDrags,
OnDraggableCanceled onDraggableCanceled
}) : super( }) : super(
key: key, key: key,
data: data, data: data,
...@@ -107,7 +115,8 @@ class Draggable<T> extends DraggableBase<T> { ...@@ -107,7 +115,8 @@ class Draggable<T> extends DraggableBase<T> {
feedback: feedback, feedback: feedback,
feedbackOffset: feedbackOffset, feedbackOffset: feedbackOffset,
dragAnchor: dragAnchor, dragAnchor: dragAnchor,
maxSimultaneousDrags: maxSimultaneousDrags maxSimultaneousDrags: maxSimultaneousDrags,
onDraggableCanceled: onDraggableCanceled
); );
@override @override
...@@ -127,7 +136,8 @@ class HorizontalDraggable<T> extends DraggableBase<T> { ...@@ -127,7 +136,8 @@ class HorizontalDraggable<T> extends DraggableBase<T> {
Widget feedback, Widget feedback,
Offset feedbackOffset: Offset.zero, Offset feedbackOffset: Offset.zero,
DragAnchor dragAnchor: DragAnchor.child, DragAnchor dragAnchor: DragAnchor.child,
int maxSimultaneousDrags int maxSimultaneousDrags,
OnDraggableCanceled onDraggableCanceled
}) : super( }) : super(
key: key, key: key,
data: data, data: data,
...@@ -136,7 +146,8 @@ class HorizontalDraggable<T> extends DraggableBase<T> { ...@@ -136,7 +146,8 @@ class HorizontalDraggable<T> extends DraggableBase<T> {
feedback: feedback, feedback: feedback,
feedbackOffset: feedbackOffset, feedbackOffset: feedbackOffset,
dragAnchor: dragAnchor, dragAnchor: dragAnchor,
maxSimultaneousDrags: maxSimultaneousDrags maxSimultaneousDrags: maxSimultaneousDrags,
onDraggableCanceled: onDraggableCanceled
); );
@override @override
...@@ -156,7 +167,8 @@ class VerticalDraggable<T> extends DraggableBase<T> { ...@@ -156,7 +167,8 @@ class VerticalDraggable<T> extends DraggableBase<T> {
Widget feedback, Widget feedback,
Offset feedbackOffset: Offset.zero, Offset feedbackOffset: Offset.zero,
DragAnchor dragAnchor: DragAnchor.child, DragAnchor dragAnchor: DragAnchor.child,
int maxSimultaneousDrags int maxSimultaneousDrags,
OnDraggableCanceled onDraggableCanceled
}) : super( }) : super(
key: key, key: key,
data: data, data: data,
...@@ -165,7 +177,8 @@ class VerticalDraggable<T> extends DraggableBase<T> { ...@@ -165,7 +177,8 @@ class VerticalDraggable<T> extends DraggableBase<T> {
feedback: feedback, feedback: feedback,
feedbackOffset: feedbackOffset, feedbackOffset: feedbackOffset,
dragAnchor: dragAnchor, dragAnchor: dragAnchor,
maxSimultaneousDrags: maxSimultaneousDrags maxSimultaneousDrags: maxSimultaneousDrags,
onDraggableCanceled: onDraggableCanceled
); );
@override @override
...@@ -184,7 +197,8 @@ class LongPressDraggable<T> extends DraggableBase<T> { ...@@ -184,7 +197,8 @@ class LongPressDraggable<T> extends DraggableBase<T> {
Widget feedback, Widget feedback,
Offset feedbackOffset: Offset.zero, Offset feedbackOffset: Offset.zero,
DragAnchor dragAnchor: DragAnchor.child, DragAnchor dragAnchor: DragAnchor.child,
int maxSimultaneousDrags int maxSimultaneousDrags,
OnDraggableCanceled onDraggableCanceled
}) : super( }) : super(
key: key, key: key,
data: data, data: data,
...@@ -193,7 +207,8 @@ class LongPressDraggable<T> extends DraggableBase<T> { ...@@ -193,7 +207,8 @@ class LongPressDraggable<T> extends DraggableBase<T> {
feedback: feedback, feedback: feedback,
feedbackOffset: feedbackOffset, feedbackOffset: feedbackOffset,
dragAnchor: dragAnchor, dragAnchor: dragAnchor,
maxSimultaneousDrags: maxSimultaneousDrags maxSimultaneousDrags: maxSimultaneousDrags,
onDraggableCanceled: onDraggableCanceled
); );
@override @override
...@@ -248,9 +263,11 @@ class _DraggableState<T> extends State<DraggableBase<T>> { ...@@ -248,9 +263,11 @@ class _DraggableState<T> extends State<DraggableBase<T>> {
dragStartPoint: dragStartPoint, dragStartPoint: dragStartPoint,
feedback: config.feedback, feedback: config.feedback,
feedbackOffset: config.feedbackOffset, feedbackOffset: config.feedbackOffset,
onDragEnd: () { onDragEnd: (Velocity velocity, Offset offset, bool wasAccepted) {
setState(() { setState(() {
_activeCount -= 1; _activeCount -= 1;
if (!wasAccepted && config.onDraggableCanceled != null)
config.onDraggableCanceled(velocity, offset);
}); });
} }
); );
...@@ -341,6 +358,7 @@ class _DragTargetState<T> extends State<DragTarget<T>> { ...@@ -341,6 +358,7 @@ class _DragTargetState<T> extends State<DragTarget<T>> {
enum _DragEndKind { dropped, canceled } enum _DragEndKind { dropped, canceled }
typedef void _OnDragEnd(Velocity velocity, Offset offset, bool wasAccepted);
// The lifetime of this object is a little dubious right now. Specifically, it // The lifetime of this object is a little dubious right now. Specifically, it
// lives as long as the pointer is down. Arguably it should self-immolate if the // lives as long as the pointer is down. Arguably it should self-immolate if the
...@@ -370,7 +388,7 @@ class _DragAvatar<T> extends Drag { ...@@ -370,7 +388,7 @@ class _DragAvatar<T> extends Drag {
final Point dragStartPoint; final Point dragStartPoint;
final Widget feedback; final Widget feedback;
final Offset feedbackOffset; final Offset feedbackOffset;
final VoidCallback onDragEnd; final _OnDragEnd onDragEnd;
_DragTargetState<T> _activeTarget; _DragTargetState<T> _activeTarget;
bool _activeTargetWillAcceptDrop = false; bool _activeTargetWillAcceptDrop = false;
...@@ -387,7 +405,7 @@ class _DragAvatar<T> extends Drag { ...@@ -387,7 +405,7 @@ class _DragAvatar<T> extends Drag {
@override @override
void end(Velocity velocity) { void end(Velocity velocity) {
finish(_DragEndKind.dropped); finish(_DragEndKind.dropped, velocity);
} }
@override @override
...@@ -422,19 +440,23 @@ class _DragAvatar<T> extends Drag { ...@@ -422,19 +440,23 @@ class _DragAvatar<T> extends Drag {
return null; return null;
} }
void finish(_DragEndKind endKind) { void finish(_DragEndKind endKind, [Velocity velocity]) {
bool wasAccepted = false;
if (_activeTarget != null) { if (_activeTarget != null) {
if (endKind == _DragEndKind.dropped && _activeTargetWillAcceptDrop) if (endKind == _DragEndKind.dropped && _activeTargetWillAcceptDrop) {
_activeTarget.didDrop(data); _activeTarget.didDrop(data);
else wasAccepted = true;
} else {
_activeTarget.didLeave(data); _activeTarget.didLeave(data);
} }
}
_activeTarget = null; _activeTarget = null;
_activeTargetWillAcceptDrop = false; _activeTargetWillAcceptDrop = false;
_entry.remove(); _entry.remove();
_entry = null; _entry = null;
// TODO(ianh): consider passing _entry as well so the client can perform an animation.
if (onDragEnd != null) if (onDragEnd != null)
onDragEnd(); onDragEnd(velocity ?? Velocity.zero, _lastOffset, wasAccepted);
} }
Widget _build(BuildContext context) { Widget _build(BuildContext context) {
......
...@@ -9,7 +9,7 @@ import 'package:test/test.dart'; ...@@ -9,7 +9,7 @@ import 'package:test/test.dart';
void main() { void main() {
test('Drag and drop - control test', () { test('Drag and drop - control test', () {
testWidgets((WidgetTester tester) { testWidgets((WidgetTester tester) {
List<dynamic> accepted = <dynamic>[]; List<int> accepted = <int>[];
tester.pumpWidget(new MaterialApp( tester.pumpWidget(new MaterialApp(
routes: <String, WidgetBuilder>{ routes: <String, WidgetBuilder>{
...@@ -62,7 +62,7 @@ void main() { ...@@ -62,7 +62,7 @@ void main() {
gesture.up(); gesture.up();
tester.pump(); tester.pump();
expect(accepted, equals([1])); expect(accepted, equals(<int>[1]));
expect(tester.findText('Source'), isNotNull); expect(tester.findText('Source'), isNotNull);
expect(tester.findText('Dragging'), isNull); expect(tester.findText('Dragging'), isNull);
expect(tester.findText('Target'), isNotNull); expect(tester.findText('Target'), isNotNull);
...@@ -553,4 +553,204 @@ void main() { ...@@ -553,4 +553,204 @@ void main() {
}); });
}); });
test('Drag and drop - onDraggableDropped not called if dropped on accepting target', () {
testWidgets((WidgetTester tester) {
List<int> accepted = <int>[];
bool onDraggableCanceledCalled = false;
tester.pumpWidget(new MaterialApp(
routes: <String, WidgetBuilder>{
'/': (BuildContext context) { return new Column(
children: <Widget>[
new Draggable<int>(
data: 1,
child: new Text('Source'),
feedback: new Text('Dragging'),
onDraggableCanceled: (Velocity velocity, Offset offset) {
onDraggableCanceledCalled = true;
}
),
new DragTarget<int>(
builder: (BuildContext context, List<int> data, List<dynamic> rejects) {
return new Container(
height: 100.0,
child: new Text('Target')
);
},
onAccept: (int data) {
accepted.add(data);
}
),
]);
},
}
));
expect(accepted, isEmpty);
expect(tester.findText('Source'), isNotNull);
expect(tester.findText('Dragging'), isNull);
expect(tester.findText('Target'), isNotNull);
expect(onDraggableCanceledCalled, isFalse);
Point firstLocation = tester.getCenter(tester.findText('Source'));
TestGesture gesture = tester.startGesture(firstLocation, pointer: 7);
tester.pump();
expect(accepted, isEmpty);
expect(tester.findText('Source'), isNotNull);
expect(tester.findText('Dragging'), isNotNull);
expect(tester.findText('Target'), isNotNull);
expect(onDraggableCanceledCalled, isFalse);
Point secondLocation = tester.getCenter(tester.findText('Target'));
gesture.moveTo(secondLocation);
tester.pump();
expect(accepted, isEmpty);
expect(tester.findText('Source'), isNotNull);
expect(tester.findText('Dragging'), isNotNull);
expect(tester.findText('Target'), isNotNull);
expect(onDraggableCanceledCalled, isFalse);
gesture.up();
tester.pump();
expect(accepted, equals(<int>[1]));
expect(tester.findText('Source'), isNotNull);
expect(tester.findText('Dragging'), isNull);
expect(tester.findText('Target'), isNotNull);
expect(onDraggableCanceledCalled, isFalse);
});
});
test('Drag and drop - onDraggableDropped called if dropped on non-accepting target', () {
testWidgets((WidgetTester tester) {
List<int> accepted = <int>[];
bool onDraggableCanceledCalled = false;
Velocity onDraggableCanceledVelocity;
Offset onDraggableCanceledOffset;
tester.pumpWidget(new MaterialApp(
routes: <String, WidgetBuilder>{
'/': (BuildContext context) { return new Column(
children: <Widget>[
new Draggable<int>(
data: 1,
child: new Text('Source'),
feedback: new Text('Dragging'),
onDraggableCanceled: (Velocity velocity, Offset offset) {
onDraggableCanceledCalled = true;
onDraggableCanceledVelocity = velocity;
onDraggableCanceledOffset = offset;
}
),
new DragTarget<int>(
builder: (BuildContext context, List<int> data, List<dynamic> rejects) {
return new Container(
height: 100.0,
child: new Text('Target')
);
},
onWillAccept: (int data) => false
),
]);
},
}
));
expect(accepted, isEmpty);
expect(tester.findText('Source'), isNotNull);
expect(tester.findText('Dragging'), isNull);
expect(tester.findText('Target'), isNotNull);
expect(onDraggableCanceledCalled, isFalse);
Point firstLocation = tester.getTopLeft(tester.findText('Source'));
TestGesture gesture = tester.startGesture(firstLocation, pointer: 7);
tester.pump();
expect(accepted, isEmpty);
expect(tester.findText('Source'), isNotNull);
expect(tester.findText('Dragging'), isNotNull);
expect(tester.findText('Target'), isNotNull);
expect(onDraggableCanceledCalled, isFalse);
Point secondLocation = tester.getCenter(tester.findText('Target'));
gesture.moveTo(secondLocation);
tester.pump();
expect(accepted, isEmpty);
expect(tester.findText('Source'), isNotNull);
expect(tester.findText('Dragging'), isNotNull);
expect(tester.findText('Target'), isNotNull);
expect(onDraggableCanceledCalled, isFalse);
gesture.up();
tester.pump();
expect(accepted, isEmpty);
expect(tester.findText('Source'), isNotNull);
expect(tester.findText('Dragging'), isNull);
expect(tester.findText('Target'), isNotNull);
expect(onDraggableCanceledCalled, isTrue);
expect(onDraggableCanceledVelocity, equals(Velocity.zero));
expect(onDraggableCanceledOffset, equals(new Offset(secondLocation.x, secondLocation.y)));
});
});
test('Drag and drop - onDraggableDropped called if dropped on non-accepting target with correct velocity', () {
testWidgets((WidgetTester tester) {
List<int> accepted = <int>[];
bool onDraggableCanceledCalled = false;
Velocity onDraggableCanceledVelocity;
Offset onDraggableCanceledOffset;
tester.pumpWidget(new MaterialApp(
routes: <String, WidgetBuilder>{
'/': (BuildContext context) { return new Column(
children: <Widget>[
new Draggable<int>(
data: 1,
child: new Text('Source'),
feedback: new Text('Source'),
onDraggableCanceled: (Velocity velocity, Offset offset) {
onDraggableCanceledCalled = true;
onDraggableCanceledVelocity = velocity;
onDraggableCanceledOffset = offset;
}
),
new DragTarget<int>(
builder: (BuildContext context, List<int> data, List<dynamic> rejects) {
return new Container(
height: 100.0,
child: new Text('Target')
);
},
onWillAccept: (int data) => false
),
]);
},
}
));
expect(accepted, isEmpty);
expect(tester.findText('Source'), isNotNull);
expect(tester.findText('Dragging'), isNull);
expect(tester.findText('Target'), isNotNull);
expect(onDraggableCanceledCalled, isFalse);
Point flingStart = tester.getTopLeft(tester.findText('Source'));
tester.flingFrom(flingStart, new Offset(0.0,100.0), 1000.0);
tester.pump();
expect(accepted, isEmpty);
expect(tester.findText('Source'), isNotNull);
expect(tester.findText('Dragging'), isNull);
expect(tester.findText('Target'), isNotNull);
expect(onDraggableCanceledCalled, isTrue);
expect(onDraggableCanceledVelocity.pixelsPerSecond.dx.abs(), lessThan(0.0000001));
expect((onDraggableCanceledVelocity.pixelsPerSecond.dy - 1000.0).abs(), lessThan(0.0000001));
expect(onDraggableCanceledOffset, equals(new Offset(flingStart.x, flingStart.y) + new Offset(0.0, 100.0)));
});
});
} }
...@@ -190,7 +190,7 @@ class Instrumentation { ...@@ -190,7 +190,7 @@ class Instrumentation {
final double timeStampDelta = 1000.0 * offset.distance / (kMoveCount * velocity); final double timeStampDelta = 1000.0 * offset.distance / (kMoveCount * velocity);
double timeStamp = 0.0; double timeStamp = 0.0;
dispatchEvent(p.down(startLocation, timeStamp: new Duration(milliseconds: timeStamp.round())), result); dispatchEvent(p.down(startLocation, timeStamp: new Duration(milliseconds: timeStamp.round())), result);
for(int i = 0; i < kMoveCount; i++) { for(int i = 0; i <= kMoveCount; i++) {
final Point location = startLocation + Offset.lerp(Offset.zero, offset, i / kMoveCount); final Point location = startLocation + Offset.lerp(Offset.zero, offset, i / kMoveCount);
dispatchEvent(p.move(location, timeStamp: new Duration(milliseconds: timeStamp.round())), result); dispatchEvent(p.move(location, timeStamp: new Duration(milliseconds: timeStamp.round())), result);
timeStamp += timeStampDelta; timeStamp += timeStampDelta;
......
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