Commit a91dd07c authored by Hixie's avatar Hixie

Draggable

Introduce a Draggable class that wraps all the logic of dragging
something and dropping it on a DragTarget.

Update examples/widgets/drag_and_drop.dart accordingly.

Make the performance/transition part of routes optional.
parent 4150615e
...@@ -2,14 +2,11 @@ ...@@ -2,14 +2,11 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:sky' as sky;
import 'package:sky/material.dart'; import 'package:sky/material.dart';
import 'package:sky/painting.dart';
import 'package:sky/rendering.dart';
import 'package:sky/src/fn3.dart'; import 'package:sky/src/fn3.dart';
final double kTop = 10.0 + sky.view.paddingTop;
final double kLeft = 10.0;
class DragData { class DragData {
DragData(this.text); DragData(this.text);
...@@ -21,11 +18,11 @@ class ExampleDragTarget extends StatefulComponent { ...@@ -21,11 +18,11 @@ class ExampleDragTarget extends StatefulComponent {
} }
class ExampleDragTargetState extends State<ExampleDragTarget> { class ExampleDragTargetState extends State<ExampleDragTarget> {
String _text = 'ready'; String _text = 'Drag Target';
void _handleAccept(DragData data) { void _handleAccept(DragData data) {
setState(() { setState(() {
_text = data.text; _text = 'dropped: ${data.text}';
}); });
} }
...@@ -34,7 +31,6 @@ class ExampleDragTargetState extends State<ExampleDragTarget> { ...@@ -34,7 +31,6 @@ class ExampleDragTargetState extends State<ExampleDragTarget> {
onAccept: _handleAccept, onAccept: _handleAccept,
builder: (BuildContext context, List<DragData> data, _) { builder: (BuildContext context, List<DragData> data, _) {
return new Container( return new Container(
width: 100.0,
height: 100.0, height: 100.0,
margin: new EdgeDims.all(10.0), margin: new EdgeDims.all(10.0),
decoration: new BoxDecoration( decoration: new BoxDecoration(
...@@ -54,100 +50,76 @@ class ExampleDragTargetState extends State<ExampleDragTarget> { ...@@ -54,100 +50,76 @@ class ExampleDragTargetState extends State<ExampleDragTarget> {
} }
class Dot extends StatelessComponent { class Dot extends StatelessComponent {
Dot({ Key key, this.color }): super(key: key);
final Color color;
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new Container( return new Container(
width: 50.0, width: 50.0,
height: 50.0, height: 50.0,
decoration: new BoxDecoration( decoration: new BoxDecoration(
backgroundColor: Colors.deepOrange[500] backgroundColor: color
) )
); );
} }
} }
class ExampleDragSource extends StatelessComponent {
ExampleDragSource({ Key key, this.navigator, this.name, this.color }): super(key: key);
final NavigatorState navigator;
final String name;
final Color color;
Widget build(BuildContext context) {
return new Draggable(
navigator: navigator,
data: new DragData(name),
child: new Dot(color: color),
feedback: new Dot(color: color)
);
}
}
class DragAndDropApp extends StatefulComponent { class DragAndDropApp extends StatefulComponent {
DragAndDropApp({ this.navigator });
final NavigatorState navigator;
DragAndDropAppState createState() => new DragAndDropAppState(); DragAndDropAppState createState() => new DragAndDropAppState();
} }
class DragAndDropAppState extends State<DragAndDropApp> { class DragAndDropAppState extends State<DragAndDropApp> {
DragController _dragController;
Offset _displacement = Offset.zero;
void _startDrag(sky.PointerEvent event) {
setState(() {
_dragController = new DragController(new DragData("Orange"));
_dragController.update(new Point(event.x, event.y));
_displacement = Offset.zero;
});
}
void _updateDrag(sky.PointerEvent event) {
setState(() {
_dragController.update(new Point(event.x, event.y));
_displacement += new Offset(event.dx, event.dy);
});
}
void _cancelDrag(sky.PointerEvent event) {
setState(() {
_dragController.cancel();
_dragController = null;
});
}
void _drop(sky.PointerEvent event) {
setState(() {
_dragController.update(new Point(event.x, event.y));
_dragController.drop();
_dragController = null;
_displacement = Offset.zero;
});
}
Widget build(BuildContext context) { Widget build(BuildContext context) {
List<Widget> layers = <Widget>[ return new Scaffold(
new Row([ toolbar: new ToolBar(
new ExampleDragTarget(), center: new Text('Drag and Drop Flutter Demo')
new ExampleDragTarget(),
new ExampleDragTarget(),
new ExampleDragTarget(),
]),
new Positioned(
top: kTop,
left: kLeft,
// TODO(abarth): We should be using a GestureDetector
child: new Listener(
onPointerDown: _startDrag,
onPointerMove: _updateDrag,
onPointerCancel: _cancelDrag,
onPointerUp: _drop,
child: new Dot()
)
), ),
]; body: new Material(
child: new DefaultTextStyle(
if (_dragController != null) { style: Theme.of(context).text.body1.copyWith(textAlign: TextAlign.center),
layers.add( child: new Column([
new Positioned( new Flexible(child: new Row([
top: kTop + _displacement.dy, new ExampleDragSource(navigator: config.navigator, name: 'Orange', color: const Color(0xFFFF9000)),
left: kLeft + _displacement.dx, new ExampleDragSource(navigator: config.navigator, name: 'Teal', color: const Color(0xFF00FFFF)),
child: new IgnorePointer( new ExampleDragSource(navigator: config.navigator, name: 'Yellow', color: const Color(0xFFFFF000)),
child: new Opacity( ],
opacity: 0.5, alignItems: FlexAlignItems.center,
child: new Dot() justifyContent: FlexJustifyContent.spaceAround
) )),
) new Flexible(child: new Row([
new Flexible(child: new ExampleDragTarget()),
new Flexible(child: new ExampleDragTarget()),
new Flexible(child: new ExampleDragTarget()),
new Flexible(child: new ExampleDragTarget()),
])),
])
) )
); )
}
return new Container(
decoration: new BoxDecoration(backgroundColor: Colors.pink[500]),
child: new Stack(layers)
); );
} }
} }
void main() { void main() {
runApp(new DragAndDropApp()); runApp(new App(
title: 'Drag and Drop Flutter Demo',
routes: {
'/': (NavigatorState navigator, Route route) => new DragAndDropApp(navigator: navigator)
}
));
} }
...@@ -69,7 +69,7 @@ class AppState extends State<App> { ...@@ -69,7 +69,7 @@ class AppState extends State<App> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new Theme( return new Theme(
data: config.theme, data: config.theme ?? new ThemeData.fallback(),
child: new DefaultTextStyle( child: new DefaultTextStyle(
style: _errorTextStyle, style: _errorTextStyle,
child: new Title( child: new Title(
......
...@@ -139,8 +139,8 @@ class DialogRoute extends Route { ...@@ -139,8 +139,8 @@ class DialogRoute extends Route {
final RouteBuilder builder; final RouteBuilder builder;
Duration get transitionDuration => _kTransitionDuration; Duration get transitionDuration => _kTransitionDuration;
bool get isOpaque => false; bool get opaque => false;
Widget build(Key key, NavigatorState navigator, WatchableAnimationPerformance performance) { Widget build(Key key, NavigatorState navigator) {
return new FadeTransition( return new FadeTransition(
performance: performance, performance: performance,
opacity: new AnimatedValue<double>(0.0, end: 1.0, curve: easeOut), opacity: new AnimatedValue<double>(0.0, end: 1.0, curve: easeOut),
...@@ -148,8 +148,9 @@ class DialogRoute extends Route { ...@@ -148,8 +148,9 @@ class DialogRoute extends Route {
); );
} }
void popState([dynamic result]) { void didPop([dynamic result]) {
completer.complete(result); completer.complete(result);
super.didPop(result);
} }
} }
......
...@@ -3,15 +3,87 @@ ...@@ -3,15 +3,87 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:collection'; import 'dart:collection';
import 'dart:sky' as sky;
import 'package:sky/rendering.dart'; import 'package:sky/rendering.dart';
import 'package:sky/src/fn3/basic.dart'; import 'package:sky/src/fn3/basic.dart';
import 'package:sky/src/fn3/binding.dart'; import 'package:sky/src/fn3/binding.dart';
import 'package:sky/src/fn3/framework.dart'; import 'package:sky/src/fn3/framework.dart';
import 'package:sky/src/fn3/navigator.dart';
typedef bool DragTargetWillAccept<T>(T data); 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);
typedef void DragFinishedNotification();
class Draggable extends StatefulComponent {
Draggable({ Key key, this.navigator, this.data, this.child, this.feedback }): super(key: key) {
assert(navigator != null);
}
final NavigatorState navigator;
final dynamic data;
final Widget child;
final Widget feedback;
DraggableState createState() => new DraggableState();
}
class DraggableState extends State<Draggable> {
DragRoute _route;
void _startDrag(sky.PointerEvent event) {
if (_route != null)
return; // TODO(ianh): once we switch to using gestures, just hand the gesture to the route so it can do everything itself. then we can have multiple drags at the same time.
Point point = new Point(event.x, event.y);
RenderBox renderObject = context.findRenderObject();
_route = new DragRoute(
data: config.data,
dragStartPoint: renderObject.globalToLocal(point),
feedback: config.feedback,
onDragFinished: () {
_route = null;
}
);
_route.update(point);
config.navigator.push(_route);
}
void _updateDrag(sky.PointerEvent event) {
if (_route != null) {
config.navigator.setState(() {
_route.update(new Point(event.x, event.y));
});
}
}
void _cancelDrag(sky.PointerEvent event) {
if (_route != null) {
config.navigator.popRoute(_route, DragEndKind.canceled);
assert(_route == null);
}
}
void _drop(sky.PointerEvent event) {
if (_route != null) {
_route.update(new Point(event.x, event.y));
config.navigator.popRoute(_route, DragEndKind.dropped);
assert(_route == null);
}
}
Widget build(BuildContext context) {
// TODO(abarth): We should be using a GestureDetector
return new Listener(
onPointerDown: _startDrag,
onPointerMove: _updateDrag,
onPointerCancel: _cancelDrag,
onPointerUp: _drop,
child: config.child
);
}
}
class DragTarget<T> extends StatefulComponent { class DragTarget<T> extends StatefulComponent {
const DragTarget({ const DragTarget({
...@@ -72,27 +144,23 @@ class DragTargetState<T> extends State<DragTarget<T>> { ...@@ -72,27 +144,23 @@ class DragTargetState<T> extends State<DragTarget<T>> {
} }
} }
class DragController {
DragController(this.data); enum DragEndKind { dropped, canceled }
class DragRoute extends Route {
DragRoute({ this.data, this.dragStartPoint: Point.origin, this.feedback, this.onDragFinished });
final dynamic data; final dynamic data;
final Point dragStartPoint;
final Widget feedback;
final DragFinishedNotification onDragFinished;
DragTargetState _activeTarget; DragTargetState _activeTarget;
bool _activeTargetWillAcceptDrop = false; bool _activeTargetWillAcceptDrop = false;
Offset _lastOffset;
DragTargetState _getDragTarget(List<HitTestEntry> path) {
// TODO(abarth): Why to we reverse the path here?
for (HitTestEntry entry in path.reversed) {
if (entry.target is RenderMetaData) {
RenderMetaData renderMetaData = entry.target;
if (renderMetaData.metaData is DragTargetState)
return renderMetaData.metaData;
}
}
return null;
}
void update(Point globalPosition) { void update(Point globalPosition) {
_lastOffset = globalPosition - dragStartPoint;
HitTestResult result = WidgetFlutterBinding.instance.hitTest(globalPosition); HitTestResult result = WidgetFlutterBinding.instance.hitTest(globalPosition);
DragTargetState target = _getDragTarget(result.path); DragTargetState target = _getDragTarget(result.path);
if (target == _activeTarget) if (target == _activeTarget)
...@@ -103,21 +171,47 @@ class DragController { ...@@ -103,21 +171,47 @@ class DragController {
_activeTargetWillAcceptDrop = _activeTarget != null && _activeTarget.didEnter(data); _activeTargetWillAcceptDrop = _activeTarget != null && _activeTarget.didEnter(data);
} }
void cancel() { DragTargetState _getDragTarget(List<HitTestEntry> path) {
if (_activeTarget != null) // TODO(abarth): Why do we reverse the path here?
_activeTarget.didLeave(data); for (HitTestEntry entry in path.reversed) {
_activeTarget = null; if (entry.target is RenderMetaData) {
_activeTargetWillAcceptDrop = false; RenderMetaData renderMetaData = entry.target;
if (renderMetaData.metaData is DragTargetState)
return renderMetaData.metaData;
}
}
return null;
} }
void drop() { void didPop([DragEndKind endKind]) {
if (_activeTarget == null) if (_activeTarget != null) {
return; if (endKind == DragEndKind.dropped && _activeTargetWillAcceptDrop)
if (_activeTargetWillAcceptDrop) _activeTarget.didDrop(data);
_activeTarget.didDrop(data); else
else _activeTarget.didLeave(data);
_activeTarget.didLeave(data); }
_activeTarget = null; _activeTarget = null;
_activeTargetWillAcceptDrop = false; _activeTargetWillAcceptDrop = false;
if (onDragFinished != null)
onDragFinished();
super.didPop(endKind);
}
bool get ephemeral => true;
bool get modal => false;
Duration get transitionDuration => const Duration();
bool get opaque => false;
Widget build(Key key, NavigatorState navigator) {
return new Positioned(
left: _lastOffset.dx,
top: _lastOffset.dy,
child: new IgnorePointer(
child: new Opacity(
opacity: 0.5,
child: feedback
)
)
);
} }
} }
This diff is collapsed.
...@@ -167,9 +167,12 @@ class MenuRoute extends Route { ...@@ -167,9 +167,12 @@ class MenuRoute extends Route {
return result; return result;
} }
bool get ephemeral => true;
bool get modal => true;
Duration get transitionDuration => _kMenuDuration; Duration get transitionDuration => _kMenuDuration;
bool get isOpaque => false; bool get opaque => false;
Widget build(Key key, NavigatorState navigator, WatchableAnimationPerformance performance) { Widget build(Key key, NavigatorState navigator) {
return new Positioned( return new Positioned(
top: position?.top, top: position?.top,
right: position?.right, right: position?.right,
...@@ -189,8 +192,9 @@ class MenuRoute extends Route { ...@@ -189,8 +192,9 @@ class MenuRoute extends Route {
); );
} }
void popState([dynamic result]) { void didPop([dynamic result]) {
completer.complete(result); completer.complete(result);
super.didPop(result);
} }
} }
......
import 'package:sky/src/fn3.dart';
import 'package:test/test.dart';
import '../engine/mock_events.dart';
import '../fn3/widget_tester.dart';
void main() {
test('Drag and drop - control test', () {
WidgetTester tester = new WidgetTester();
TestPointer pointer = new TestPointer(7);
List accepted = [];
tester.pumpFrame(new Navigator(
routes: {
'/': (NavigatorState navigator, Route route) { return new Column([
new Draggable(
navigator: navigator,
data: 1,
child: new Text('Source'),
feedback: new Text('Dragging')
),
new DragTarget(
builder: (context, data, rejects) {
return new Container(
height: 100.0,
child: new Text('Target')
);
},
onAccept: (data) {
accepted.add(data);
}
),
]);
},
}
));
expect(accepted, isEmpty);
expect(tester.findText('Source'), isNotNull);
expect(tester.findText('Dragging'), isNull);
expect(tester.findText('Target'), isNotNull);
Point firstLocation = tester.getCenter(tester.findText('Source'));
tester.dispatchEvent(pointer.down(firstLocation), firstLocation);
tester.pumpFrameWithoutChange();
expect(accepted, isEmpty);
expect(tester.findText('Source'), isNotNull);
expect(tester.findText('Dragging'), isNotNull);
expect(tester.findText('Target'), isNotNull);
Point secondLocation = tester.getCenter(tester.findText('Target'));
tester.dispatchEvent(pointer.move(secondLocation), firstLocation);
tester.pumpFrameWithoutChange();
expect(accepted, isEmpty);
expect(tester.findText('Source'), isNotNull);
expect(tester.findText('Dragging'), isNotNull);
expect(tester.findText('Target'), isNotNull);
tester.dispatchEvent(pointer.up(), firstLocation);
tester.pumpFrameWithoutChange();
expect(accepted, equals([1]));
expect(tester.findText('Source'), isNotNull);
expect(tester.findText('Dragging'), isNull);
expect(tester.findText('Target'), isNotNull);
});
}
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