Added support for passing in velocity and offset into Draggable.onDragCompleted (#22267)
* Added support for passing in velocity and offset into Draggable.onDragCompleted. * Fixed documentation of DragCompletedCallback. * Spun off previous onDragCompleted breaking changes into new callback called Draggable.onDragEnd. * Revert "Fixed documentation of DragCompletedCallback." This reverts commit 069051f5be0d4ec6a1f4b3f072e535ca87e5d740. * Revert "Added support for passing in velocity and offset into Draggable.onDragCompleted." This reverts commit 7ef744aa5645429b7bc92527226203ee8bff68ec. * DraggableDetails constructor is now declared first as per Flutter code style. * Draggable.onDragEnd will only call back if its widget is currently mounted to the tree. * Moved "});" in DraggableDetails constructor to new line, vertically aligned with the constructor name, as per Flutter code style. * Added space between if statement in drag_target.dart. * widget.onDragEnd call is now formated as per flutter code style. * Added more details to DraggableDetails documentation. * Added brackets to if statement block as per Flutter code style. * Fixed minor nits in DraggableDetails documentation. * Made DraggableDetails constructor public. Also added documentation for its constructor.
This commit is contained in:
parent
33f8030b32
commit
9447be7370
@ -36,6 +36,15 @@ typedef DragTargetBuilder<T> = Widget Function(BuildContext context, List<T> can
|
||||
/// Used by [Draggable.onDraggableCanceled].
|
||||
typedef DraggableCanceledCallback = void Function(Velocity velocity, Offset offset);
|
||||
|
||||
/// Signature for when the draggable is dropped.
|
||||
///
|
||||
/// The velocity and offset at which the pointer was moving when the draggable
|
||||
/// was dropped is available in the [DraggableDetails]. Also included in the
|
||||
/// `details` is whether the draggable's [DragTarget] accepted it.
|
||||
///
|
||||
/// Used by [Draggable.onDragEnd]
|
||||
typedef DragEndCallback = void Function(DraggableDetails details);
|
||||
|
||||
/// Signature for when a [Draggable] leaves a [DragTarget].
|
||||
///
|
||||
/// Used by [DragTarget.onLeave].
|
||||
@ -100,6 +109,7 @@ class Draggable<T> extends StatefulWidget {
|
||||
this.maxSimultaneousDrags,
|
||||
this.onDragStarted,
|
||||
this.onDraggableCanceled,
|
||||
this.onDragEnd,
|
||||
this.onDragCompleted,
|
||||
this.ignoringFeedbackSemantics = true,
|
||||
}) : assert(child != null),
|
||||
@ -229,6 +239,16 @@ class Draggable<T> extends StatefulWidget {
|
||||
/// callback is still in the tree.
|
||||
final VoidCallback onDragCompleted;
|
||||
|
||||
/// Called when the draggable is dropped.
|
||||
///
|
||||
/// The velocity and offset at which the pointer was moving when it was
|
||||
/// dropped is available in the [DraggableDetails]. Also included in the
|
||||
/// `details` is whether the draggable's [DragTarget] accepted it.
|
||||
///
|
||||
/// This function will only be called while this widget is still mounted to
|
||||
/// the tree (i.e. [State.mounted] is true).
|
||||
final DragEndCallback onDragEnd;
|
||||
|
||||
/// Creates a gesture recognizer that recognizes the start of the drag.
|
||||
///
|
||||
/// Subclasses can override this function to customize when they start
|
||||
@ -266,6 +286,7 @@ class LongPressDraggable<T> extends Draggable<T> {
|
||||
int maxSimultaneousDrags,
|
||||
VoidCallback onDragStarted,
|
||||
DraggableCanceledCallback onDraggableCanceled,
|
||||
DragEndCallback onDragEnd,
|
||||
VoidCallback onDragCompleted,
|
||||
this.hapticFeedbackOnStart = true,
|
||||
bool ignoringFeedbackSemantics = true,
|
||||
@ -281,6 +302,7 @@ class LongPressDraggable<T> extends Draggable<T> {
|
||||
maxSimultaneousDrags: maxSimultaneousDrags,
|
||||
onDragStarted: onDragStarted,
|
||||
onDraggableCanceled: onDraggableCanceled,
|
||||
onDragEnd: onDragEnd,
|
||||
onDragCompleted: onDragCompleted,
|
||||
ignoringFeedbackSemantics: ignoringFeedbackSemantics,
|
||||
);
|
||||
@ -372,6 +394,13 @@ class _DraggableState<T> extends State<Draggable<T>> {
|
||||
_activeCount -= 1;
|
||||
_disposeRecognizerIfInactive();
|
||||
}
|
||||
if (mounted && widget.onDragEnd != null) {
|
||||
widget.onDragEnd(DraggableDetails(
|
||||
wasAccepted: wasAccepted,
|
||||
velocity: velocity,
|
||||
offset: offset
|
||||
));
|
||||
}
|
||||
if (wasAccepted && widget.onDragCompleted != null)
|
||||
widget.onDragCompleted();
|
||||
if (!wasAccepted && widget.onDraggableCanceled != null)
|
||||
@ -396,6 +425,38 @@ class _DraggableState<T> extends State<Draggable<T>> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the details when a specific pointer event occurred on
|
||||
/// the [Draggable].
|
||||
///
|
||||
/// This includes the [Velocity] at which the pointer was moving and [Offset]
|
||||
/// when the draggable event occurred, and whether its [DragTarget] accepted it.
|
||||
///
|
||||
/// Also, this is the details object for callbacks that use [DragEndCallback].
|
||||
class DraggableDetails {
|
||||
/// Creates details for a [DraggableDetails].
|
||||
///
|
||||
/// If [wasAccepted] is not specified, it will default to `false`.
|
||||
///
|
||||
/// The [velocity] or [offset] arguments must not be `null`.
|
||||
DraggableDetails({
|
||||
this.wasAccepted = false,
|
||||
@required this.velocity,
|
||||
@required this.offset
|
||||
}) : assert(velocity != null),
|
||||
assert(offset != null);
|
||||
|
||||
/// Determines whether the [DragTarget] accepted this draggable.
|
||||
final bool wasAccepted;
|
||||
|
||||
/// The velocity at which the pointer was moving when the specific pointer
|
||||
/// event occurred on the draggable.
|
||||
final Velocity velocity;
|
||||
|
||||
/// The global position when the specific pointer event occurred on
|
||||
/// the draggable.
|
||||
final Offset offset;
|
||||
}
|
||||
|
||||
/// A widget that receives data when a [Draggable] widget is dropped.
|
||||
///
|
||||
/// When a draggable is dragged on top of a drag target, the drag target is
|
||||
|
@ -899,6 +899,77 @@ void main() {
|
||||
expect(onDraggableCanceledOffset, equals(Offset(flingStart.dx, flingStart.dy) + const Offset(0.0, 100.0)));
|
||||
});
|
||||
|
||||
testWidgets('Drag and drop - onDragEnd not called if dropped on non-accepting target', (WidgetTester tester) async {
|
||||
final List<int> accepted = <int>[];
|
||||
bool onDragEndCalled = false;
|
||||
DraggableDetails onDragEndDraggableDetails;
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: Column(
|
||||
children: <Widget>[
|
||||
Draggable<int>(
|
||||
data: 1,
|
||||
child: const Text('Source'),
|
||||
feedback: const Text('Dragging'),
|
||||
onDragEnd: (DraggableDetails details) {
|
||||
onDragEndCalled = true;
|
||||
onDragEndDraggableDetails = details;
|
||||
},
|
||||
),
|
||||
DragTarget<int>(
|
||||
builder: (BuildContext context, List<int> data, List<dynamic> rejects) {
|
||||
return Container(
|
||||
height: 100.0,
|
||||
child: const Text('Target'),
|
||||
);
|
||||
},
|
||||
onWillAccept: (int data) => false,
|
||||
),
|
||||
],
|
||||
),
|
||||
));
|
||||
|
||||
expect(accepted, isEmpty);
|
||||
expect(find.text('Source'), findsOneWidget);
|
||||
expect(find.text('Dragging'), findsNothing);
|
||||
expect(find.text('Target'), findsOneWidget);
|
||||
expect(onDragEndCalled, isFalse);
|
||||
|
||||
final Offset firstLocation = tester.getTopLeft(find.text('Source'));
|
||||
final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7);
|
||||
await tester.pump();
|
||||
|
||||
expect(accepted, isEmpty);
|
||||
expect(find.text('Source'), findsOneWidget);
|
||||
expect(find.text('Dragging'), findsOneWidget);
|
||||
expect(find.text('Target'), findsOneWidget);
|
||||
expect(onDragEndCalled, isFalse);
|
||||
|
||||
final Offset secondLocation = tester.getCenter(find.text('Target'));
|
||||
await gesture.moveTo(secondLocation);
|
||||
await tester.pump();
|
||||
|
||||
expect(accepted, isEmpty);
|
||||
expect(find.text('Source'), findsOneWidget);
|
||||
expect(find.text('Dragging'), findsOneWidget);
|
||||
expect(find.text('Target'), findsOneWidget);
|
||||
expect(onDragEndCalled, isFalse);
|
||||
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
|
||||
expect(accepted, isEmpty);
|
||||
expect(find.text('Source'), findsOneWidget);
|
||||
expect(find.text('Dragging'), findsNothing);
|
||||
expect(find.text('Target'), findsOneWidget);
|
||||
expect(onDragEndCalled, isTrue);
|
||||
expect(onDragEndDraggableDetails, isNotNull);
|
||||
expect(onDragEndDraggableDetails.wasAccepted, isFalse);
|
||||
expect(onDragEndDraggableDetails.velocity, equals(Velocity.zero));
|
||||
expect(onDragEndDraggableDetails.offset,
|
||||
equals(
|
||||
Offset(secondLocation.dx, secondLocation.dy - firstLocation.dy)));
|
||||
});
|
||||
|
||||
testWidgets('Drag and drop - onDragCompleted not called if dropped on non-accepting target', (WidgetTester tester) async {
|
||||
final List<int> accepted = <int>[];
|
||||
bool onDragCompletedCalled = false;
|
||||
@ -963,6 +1034,138 @@ void main() {
|
||||
expect(onDragCompletedCalled, isFalse);
|
||||
});
|
||||
|
||||
testWidgets('Drag and drop - onDragEnd called if dropped on accepting target', (WidgetTester tester) async {
|
||||
final List<int> accepted = <int>[];
|
||||
bool onDragEndCalled = false;
|
||||
DraggableDetails onDragEndDraggableDetails;
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: Column(
|
||||
children: <Widget>[
|
||||
Draggable<int>(
|
||||
data: 1,
|
||||
child: const Text('Source'),
|
||||
feedback: const Text('Dragging'),
|
||||
onDragEnd: (DraggableDetails details) {
|
||||
onDragEndCalled = true;
|
||||
onDragEndDraggableDetails = details;
|
||||
},
|
||||
),
|
||||
DragTarget<int>(
|
||||
builder: (BuildContext context, List<int> data, List<dynamic> rejects) {
|
||||
return Container(height: 100.0, child: const Text('Target'));
|
||||
},
|
||||
onAccept: accepted.add,
|
||||
),
|
||||
],
|
||||
),
|
||||
));
|
||||
|
||||
expect(accepted, isEmpty);
|
||||
expect(find.text('Source'), findsOneWidget);
|
||||
expect(find.text('Dragging'), findsNothing);
|
||||
expect(find.text('Target'), findsOneWidget);
|
||||
expect(onDragEndCalled, isFalse);
|
||||
|
||||
final Offset firstLocation = tester.getCenter(find.text('Source'));
|
||||
final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7);
|
||||
await tester.pump();
|
||||
|
||||
expect(accepted, isEmpty);
|
||||
expect(find.text('Source'), findsOneWidget);
|
||||
expect(find.text('Dragging'), findsOneWidget);
|
||||
expect(find.text('Target'), findsOneWidget);
|
||||
expect(onDragEndCalled, isFalse);
|
||||
|
||||
final Offset secondLocation = tester.getCenter(find.text('Target'));
|
||||
await gesture.moveTo(secondLocation);
|
||||
await tester.pump();
|
||||
|
||||
expect(accepted, isEmpty);
|
||||
expect(find.text('Source'), findsOneWidget);
|
||||
expect(find.text('Dragging'), findsOneWidget);
|
||||
expect(find.text('Target'), findsOneWidget);
|
||||
expect(onDragEndCalled, isFalse);
|
||||
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
|
||||
final Offset droppedLocation = tester.getTopLeft(find.text('Target'));
|
||||
expect(accepted, equals(<int>[1]));
|
||||
expect(find.text('Source'), findsOneWidget);
|
||||
expect(find.text('Dragging'), findsNothing);
|
||||
expect(find.text('Target'), findsOneWidget);
|
||||
expect(onDragEndCalled, isTrue);
|
||||
expect(onDragEndDraggableDetails, isNotNull);
|
||||
expect(onDragEndDraggableDetails.wasAccepted, isTrue);
|
||||
expect(onDragEndDraggableDetails.velocity, equals(Velocity.zero));
|
||||
expect(onDragEndDraggableDetails.offset,
|
||||
equals(
|
||||
Offset(droppedLocation.dx, secondLocation.dy - firstLocation.dy)));
|
||||
});
|
||||
|
||||
testWidgets('DragTarget does not call onDragEnd when remove from the tree', (WidgetTester tester) async {
|
||||
final List<String> events = <String>[];
|
||||
Offset firstLocation, secondLocation;
|
||||
int timesOnDragEndCalled = 0;
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: Column(
|
||||
children: <Widget>[
|
||||
Draggable<int>(
|
||||
data: 1,
|
||||
child: const Text('Source'),
|
||||
feedback: const Text('Dragging'),
|
||||
onDragEnd: (DraggableDetails details) {
|
||||
timesOnDragEndCalled++;
|
||||
},
|
||||
),
|
||||
DragTarget<int>(
|
||||
builder: (BuildContext context, List<int> data, List<dynamic> rejects) {
|
||||
return const Text('Target');
|
||||
},
|
||||
onAccept: (int data) {
|
||||
events.add('drop');
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
));
|
||||
|
||||
expect(events, isEmpty);
|
||||
expect(find.text('Source'), findsOneWidget);
|
||||
expect(find.text('Target'), findsOneWidget);
|
||||
|
||||
expect(events, isEmpty);
|
||||
await tester.tap(find.text('Source'));
|
||||
expect(events, isEmpty);
|
||||
|
||||
firstLocation = tester.getCenter(find.text('Source'));
|
||||
final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7);
|
||||
await tester.pump();
|
||||
|
||||
await tester.pump(const Duration(seconds: 20));
|
||||
|
||||
secondLocation = tester.getCenter(find.text('Target'));
|
||||
await gesture.moveTo(secondLocation);
|
||||
await tester.pump();
|
||||
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: Column(
|
||||
children: const <Widget>[
|
||||
Draggable<int>(
|
||||
data: 1,
|
||||
child: Text('Source'),
|
||||
feedback: Text('Dragging')
|
||||
),
|
||||
]
|
||||
)
|
||||
));
|
||||
|
||||
expect(events, isEmpty);
|
||||
expect(timesOnDragEndCalled, equals(1));
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
});
|
||||
|
||||
testWidgets('Drag and drop - onDragCompleted called if dropped on accepting target', (WidgetTester tester) async {
|
||||
final List<int> accepted = <int>[];
|
||||
bool onDragCompletedCalled = false;
|
||||
@ -1533,6 +1736,85 @@ void main() {
|
||||
expect(events, equals(<String>['tap']));
|
||||
});
|
||||
|
||||
testWidgets('long-press draggable calls onDragEnd called if dropped on accepting target', (WidgetTester tester) async {
|
||||
final List<int> accepted = <int>[];
|
||||
bool onDragEndCalled = false;
|
||||
DraggableDetails onDragEndDraggableDetails;
|
||||
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: Column(
|
||||
children: <Widget>[
|
||||
LongPressDraggable<int>(
|
||||
data: 1,
|
||||
child: const Text('Source'),
|
||||
feedback: const Text('Dragging'),
|
||||
onDragEnd: (DraggableDetails details) {
|
||||
onDragEndCalled = true;
|
||||
onDragEndDraggableDetails = details;
|
||||
},
|
||||
),
|
||||
DragTarget<int>(
|
||||
builder: (BuildContext context, List<int> data, List<dynamic> rejects) {
|
||||
return Container(height: 100.0, child: const Text('Target'));
|
||||
},
|
||||
onAccept: accepted.add,
|
||||
),
|
||||
],
|
||||
),
|
||||
));
|
||||
|
||||
expect(accepted, isEmpty);
|
||||
expect(find.text('Source'), findsOneWidget);
|
||||
expect(find.text('Dragging'), findsNothing);
|
||||
expect(find.text('Target'), findsOneWidget);
|
||||
expect(onDragEndCalled, isFalse);
|
||||
|
||||
final Offset firstLocation = tester.getCenter(find.text('Source'));
|
||||
final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7);
|
||||
await tester.pump();
|
||||
|
||||
expect(accepted, isEmpty);
|
||||
expect(find.text('Source'), findsOneWidget);
|
||||
expect(find.text('Dragging'), findsNothing);
|
||||
expect(find.text('Target'), findsOneWidget);
|
||||
expect(onDragEndCalled, isFalse);
|
||||
|
||||
await tester.pump(kLongPressTimeout);
|
||||
|
||||
expect(accepted, isEmpty);
|
||||
expect(find.text('Source'), findsOneWidget);
|
||||
expect(find.text('Dragging'), findsOneWidget);
|
||||
expect(find.text('Target'), findsOneWidget);
|
||||
expect(onDragEndCalled, isFalse);
|
||||
|
||||
|
||||
final Offset secondLocation = tester.getCenter(find.text('Target'));
|
||||
await gesture.moveTo(secondLocation);
|
||||
await tester.pump();
|
||||
|
||||
expect(accepted, isEmpty);
|
||||
expect(find.text('Source'), findsOneWidget);
|
||||
expect(find.text('Dragging'), findsOneWidget);
|
||||
expect(find.text('Target'), findsOneWidget);
|
||||
expect(onDragEndCalled, isFalse);
|
||||
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
|
||||
final Offset droppedLocation = tester.getTopLeft(find.text('Target'));
|
||||
expect(accepted, equals(<int>[1]));
|
||||
expect(find.text('Source'), findsOneWidget);
|
||||
expect(find.text('Dragging'), findsNothing);
|
||||
expect(find.text('Target'), findsOneWidget);
|
||||
expect(onDragEndCalled, isTrue);
|
||||
expect(onDragEndDraggableDetails, isNotNull);
|
||||
expect(onDragEndDraggableDetails.wasAccepted, isTrue);
|
||||
expect(onDragEndDraggableDetails.velocity, equals(Velocity.zero));
|
||||
expect(onDragEndDraggableDetails.offset,
|
||||
equals(
|
||||
Offset(droppedLocation.dx, secondLocation.dy - firstLocation.dy)));
|
||||
});
|
||||
|
||||
testWidgets('long-press draggable calls onDragCompleted called if dropped on accepting target', (WidgetTester tester) async {
|
||||
final List<int> accepted = <int>[];
|
||||
bool onDragCompletedCalled = false;
|
||||
|
Loading…
x
Reference in New Issue
Block a user