Commit 12628333 authored by Adam Barth's avatar Adam Barth Committed by GitHub

Support zero for Draggable.maxSimultaneousDrags (#6342)

This patch also makes it possible for a single DragTarget to handle
multiple draggables with the same data without asserting.

Fixes #6086
parent ea3c3f53
...@@ -64,7 +64,7 @@ class Draggable<T> extends StatefulWidget { ...@@ -64,7 +64,7 @@ class Draggable<T> extends StatefulWidget {
/// Creates a widget that can be dragged to a [DragTarget]. /// Creates a widget that can be dragged to a [DragTarget].
/// ///
/// The [child] and [feedback] arguments must not be null. If /// The [child] and [feedback] arguments must not be null. If
/// [maxSimultaneousDrags] is non-null, it must be positive. /// [maxSimultaneousDrags] is non-null, it must be non-negative.
Draggable({ Draggable({
Key key, Key key,
@required this.child, @required this.child,
...@@ -80,7 +80,7 @@ class Draggable<T> extends StatefulWidget { ...@@ -80,7 +80,7 @@ class Draggable<T> extends StatefulWidget {
}) : super(key: key) { }) : super(key: key) {
assert(child != null); assert(child != null);
assert(feedback != null); assert(feedback != null);
assert(maxSimultaneousDrags == null || maxSimultaneousDrags > 0); assert(maxSimultaneousDrags == null || maxSimultaneousDrags >= 0);
} }
/// The data that will be dropped by this draggable. /// The data that will be dropped by this draggable.
...@@ -123,9 +123,11 @@ class Draggable<T> extends StatefulWidget { ...@@ -123,9 +123,11 @@ class Draggable<T> extends StatefulWidget {
/// widget, will out-compete the [Scrollable] for vertical gestures. /// widget, will out-compete the [Scrollable] for vertical gestures.
final Axis affinity; final Axis affinity;
/// How many simultaneous drags to support. When null, no limit is applied. /// How many simultaneous drags to support.
/// Set this to 1 if you want to only allow the drag source to have one item ///
/// dragged at a time. /// When null, no limit is applied. Set this to 1 if you want to only allow
/// the drag source to have one item dragged at a time. Set this to 0 if you
/// want to prevent the draggable from actually being dragged.
final int maxSimultaneousDrags; final int maxSimultaneousDrags;
/// Called when the draggable starts being dragged. /// Called when the draggable starts being dragged.
...@@ -158,7 +160,7 @@ class LongPressDraggable<T> extends Draggable<T> { ...@@ -158,7 +160,7 @@ class LongPressDraggable<T> extends Draggable<T> {
/// Creates a widget that can be dragged starting from long press. /// Creates a widget that can be dragged starting from long press.
/// ///
/// The [child] and [feedback] arguments must not be null. If /// The [child] and [feedback] arguments must not be null. If
/// [maxSimultaneousDrags] is non-null, it must be positive. /// [maxSimultaneousDrags] is non-null, it must be non-negative.
LongPressDraggable({ LongPressDraggable({
Key key, Key key,
@required Widget child, @required Widget child,
...@@ -306,42 +308,46 @@ class DragTarget<T> extends StatefulWidget { ...@@ -306,42 +308,46 @@ class DragTarget<T> extends StatefulWidget {
_DragTargetState<T> createState() => new _DragTargetState<T>(); _DragTargetState<T> createState() => new _DragTargetState<T>();
} }
List/*<T>*/ _mapAvatarsToData/*<T>*/(List/*<_DragAvatar<T>>*/ avatars) {
return avatars.map/*<T>*/((_DragAvatar/*<T>*/ avatar) => avatar.data).toList();
}
class _DragTargetState<T> extends State<DragTarget<T>> { class _DragTargetState<T> extends State<DragTarget<T>> {
final List<T> _candidateData = new List<T>(); final List<_DragAvatar<T>> _candidateAvatars = new List<_DragAvatar<T>>();
final List<dynamic> _rejectedData = new List<dynamic>(); final List<_DragAvatar<dynamic>> _rejectedAvatars = new List<_DragAvatar<dynamic>>();
bool didEnter(dynamic data) { bool didEnter(_DragAvatar<dynamic> avatar) {
assert(!_candidateData.contains(data)); assert(!_candidateAvatars.contains(avatar));
assert(!_rejectedData.contains(data)); assert(!_rejectedAvatars.contains(avatar));
if (data is T && (config.onWillAccept == null || config.onWillAccept(data))) { if (avatar.data is T && (config.onWillAccept == null || config.onWillAccept(avatar.data))) {
setState(() { setState(() {
_candidateData.add(data); _candidateAvatars.add(avatar);
}); });
return true; return true;
} }
_rejectedData.add(data); _rejectedAvatars.add(avatar);
return false; return false;
} }
void didLeave(dynamic data) { void didLeave(_DragAvatar<dynamic> avatar) {
assert(_candidateData.contains(data) || _rejectedData.contains(data)); assert(_candidateAvatars.contains(avatar) || _rejectedAvatars.contains(avatar));
if (!mounted) if (!mounted)
return; return;
setState(() { setState(() {
_candidateData.remove(data); _candidateAvatars.remove(avatar);
_rejectedData.remove(data); _rejectedAvatars.remove(avatar);
}); });
} }
void didDrop(dynamic data) { void didDrop(_DragAvatar<dynamic> avatar) {
assert(_candidateData.contains(data)); assert(_candidateAvatars.contains(avatar));
if (!mounted) if (!mounted)
return; return;
setState(() { setState(() {
_candidateData.remove(data); _candidateAvatars.remove(avatar);
}); });
if (config.onAccept != null) if (config.onAccept != null)
config.onAccept(data); config.onAccept(avatar.data);
} }
@override @override
...@@ -350,12 +356,11 @@ class _DragTargetState<T> extends State<DragTarget<T>> { ...@@ -350,12 +356,11 @@ class _DragTargetState<T> extends State<DragTarget<T>> {
return new MetaData( return new MetaData(
metaData: this, metaData: this,
behavior: HitTestBehavior.translucent, behavior: HitTestBehavior.translucent,
child: config.builder(context, _candidateData, _rejectedData) child: config.builder(context, _mapAvatarsToData/*<T>*/(_candidateAvatars), _mapAvatarsToData(_rejectedAvatars))
); );
} }
} }
enum _DragEndKind { dropped, canceled } enum _DragEndKind { dropped, canceled }
typedef void _OnDragEnd(Velocity velocity, Offset offset, bool wasAccepted); typedef void _OnDragEnd(Velocity velocity, Offset offset, bool wasAccepted);
...@@ -443,7 +448,7 @@ class _DragAvatar<T> extends Drag { ...@@ -443,7 +448,7 @@ class _DragAvatar<T> extends Drag {
// Enter new targets. // Enter new targets.
_DragTargetState<T> newTarget = targets.firstWhere((_DragTargetState<T> target) { _DragTargetState<T> newTarget = targets.firstWhere((_DragTargetState<T> target) {
_enteredTargets.add(target); _enteredTargets.add(target);
return target.didEnter(data); return target.didEnter(this);
}, },
orElse: () => null orElse: () => null
); );
...@@ -465,14 +470,14 @@ class _DragAvatar<T> extends Drag { ...@@ -465,14 +470,14 @@ class _DragAvatar<T> extends Drag {
void _leaveAllEntered() { void _leaveAllEntered() {
for (int i = 0; i < _enteredTargets.length; i += 1) for (int i = 0; i < _enteredTargets.length; i += 1)
_enteredTargets[i].didLeave(data); _enteredTargets[i].didLeave(this);
_enteredTargets.clear(); _enteredTargets.clear();
} }
void finishDrag(_DragEndKind endKind, [Velocity velocity]) { void finishDrag(_DragEndKind endKind, [Velocity velocity]) {
bool wasAccepted = false; bool wasAccepted = false;
if (endKind == _DragEndKind.dropped && _activeTarget != null) { if (endKind == _DragEndKind.dropped && _activeTarget != null) {
_activeTarget.didDrop(data); _activeTarget.didDrop(this);
wasAccepted = true; wasAccepted = true;
_enteredTargets.remove(_activeTarget); _enteredTargets.remove(_activeTarget);
} }
......
...@@ -884,6 +884,118 @@ void main() { ...@@ -884,6 +884,118 @@ void main() {
} }
}); });
testWidgets('Drag and drop - maxSimultaneousDrags', (WidgetTester tester) async {
List<int> accepted = <int>[];
Widget build(int maxSimultaneousDrags) {
return new MaterialApp(
home: new Column(
children: <Widget>[
new Draggable<int>(
data: 1,
maxSimultaneousDrags: maxSimultaneousDrags,
child: new Text('Source'),
feedback: new Text('Dragging')
),
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);
}
),
]
)
);
}
await tester.pumpWidget(build(0));
Point firstLocation = tester.getCenter(find.text('Source'));
Point secondLocation = tester.getCenter(find.text('Target'));
expect(accepted, isEmpty);
expect(find.text('Source'), findsOneWidget);
expect(find.text('Dragging'), findsNothing);
expect(find.text('Target'), findsOneWidget);
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);
await gesture.up();
await tester.pumpWidget(build(2));
expect(accepted, isEmpty);
expect(find.text('Source'), findsOneWidget);
expect(find.text('Dragging'), findsNothing);
expect(find.text('Target'), findsOneWidget);
TestGesture gesture1 = await tester.startGesture(firstLocation, pointer: 8);
await tester.pump();
expect(accepted, isEmpty);
expect(find.text('Source'), findsOneWidget);
expect(find.text('Dragging'), findsOneWidget);
expect(find.text('Target'), findsOneWidget);
TestGesture gesture2 = await tester.startGesture(firstLocation, pointer: 9);
await tester.pump();
expect(accepted, isEmpty);
expect(find.text('Source'), findsOneWidget);
expect(find.text('Dragging'), findsNWidgets(2));
expect(find.text('Target'), findsOneWidget);
TestGesture gesture3 = await tester.startGesture(firstLocation, pointer: 10);
await tester.pump();
expect(accepted, isEmpty);
expect(find.text('Source'), findsOneWidget);
expect(find.text('Dragging'), findsNWidgets(2));
expect(find.text('Target'), findsOneWidget);
await gesture1.moveTo(secondLocation);
await gesture2.moveTo(secondLocation);
await gesture3.moveTo(secondLocation);
await tester.pump();
expect(accepted, isEmpty);
expect(find.text('Source'), findsOneWidget);
expect(find.text('Dragging'), findsNWidgets(2));
expect(find.text('Target'), findsOneWidget);
await gesture1.up();
await tester.pump();
expect(accepted, equals(<int>[1]));
expect(find.text('Source'), findsOneWidget);
expect(find.text('Dragging'), findsOneWidget);
expect(find.text('Target'), findsOneWidget);
await gesture2.up();
await tester.pump();
expect(accepted, equals(<int>[1, 1]));
expect(find.text('Source'), findsOneWidget);
expect(find.text('Dragging'), findsNothing);
expect(find.text('Target'), findsOneWidget);
await gesture3.up();
await tester.pump();
expect(accepted, equals(<int>[1, 1]));
expect(find.text('Source'), findsOneWidget);
expect(find.text('Dragging'), findsNothing);
expect(find.text('Target'), findsOneWidget);
});
testWidgets('Draggable disposes recognizer', (WidgetTester tester) async { testWidgets('Draggable disposes recognizer', (WidgetTester tester) async {
bool didTap = false; bool didTap = false;
await tester.pumpWidget(new Overlay( await tester.pumpWidget(new Overlay(
......
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