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 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:sky' as sky;
import 'package:sky/material.dart';
import 'package:sky/painting.dart';
import 'package:sky/rendering.dart';
import 'package:sky/src/fn3.dart';
final double kTop = 10.0 + sky.view.paddingTop;
final double kLeft = 10.0;
class DragData {
DragData(this.text);
......@@ -21,11 +18,11 @@ class ExampleDragTarget extends StatefulComponent {
}
class ExampleDragTargetState extends State<ExampleDragTarget> {
String _text = 'ready';
String _text = 'Drag Target';
void _handleAccept(DragData data) {
setState(() {
_text = data.text;
_text = 'dropped: ${data.text}';
});
}
......@@ -34,7 +31,6 @@ class ExampleDragTargetState extends State<ExampleDragTarget> {
onAccept: _handleAccept,
builder: (BuildContext context, List<DragData> data, _) {
return new Container(
width: 100.0,
height: 100.0,
margin: new EdgeDims.all(10.0),
decoration: new BoxDecoration(
......@@ -54,100 +50,76 @@ class ExampleDragTargetState extends State<ExampleDragTarget> {
}
class Dot extends StatelessComponent {
Dot({ Key key, this.color }): super(key: key);
final Color color;
Widget build(BuildContext context) {
return new Container(
width: 50.0,
height: 50.0,
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 {
DragAndDropApp({ this.navigator });
final NavigatorState navigator;
DragAndDropAppState createState() => new DragAndDropAppState();
}
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) {
List<Widget> layers = <Widget>[
new Row([
new ExampleDragTarget(),
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()
)
return new Scaffold(
toolbar: new ToolBar(
center: new Text('Drag and Drop Flutter Demo')
),
];
if (_dragController != null) {
layers.add(
new Positioned(
top: kTop + _displacement.dy,
left: kLeft + _displacement.dx,
child: new IgnorePointer(
child: new Opacity(
opacity: 0.5,
child: new Dot()
)
)
body: new Material(
child: new DefaultTextStyle(
style: Theme.of(context).text.body1.copyWith(textAlign: TextAlign.center),
child: new Column([
new Flexible(child: new Row([
new ExampleDragSource(navigator: config.navigator, name: 'Orange', color: const Color(0xFFFF9000)),
new ExampleDragSource(navigator: config.navigator, name: 'Teal', color: const Color(0xFF00FFFF)),
new ExampleDragSource(navigator: config.navigator, name: 'Yellow', color: const Color(0xFFFFF000)),
],
alignItems: FlexAlignItems.center,
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() {
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> {
Widget build(BuildContext context) {
return new Theme(
data: config.theme,
data: config.theme ?? new ThemeData.fallback(),
child: new DefaultTextStyle(
style: _errorTextStyle,
child: new Title(
......
......@@ -139,8 +139,8 @@ class DialogRoute extends Route {
final RouteBuilder builder;
Duration get transitionDuration => _kTransitionDuration;
bool get isOpaque => false;
Widget build(Key key, NavigatorState navigator, WatchableAnimationPerformance performance) {
bool get opaque => false;
Widget build(Key key, NavigatorState navigator) {
return new FadeTransition(
performance: performance,
opacity: new AnimatedValue<double>(0.0, end: 1.0, curve: easeOut),
......@@ -148,8 +148,9 @@ class DialogRoute extends Route {
);
}
void popState([dynamic result]) {
void didPop([dynamic result]) {
completer.complete(result);
super.didPop(result);
}
}
......
......@@ -3,15 +3,87 @@
// found in the LICENSE file.
import 'dart:collection';
import 'dart:sky' as sky;
import 'package:sky/rendering.dart';
import 'package:sky/src/fn3/basic.dart';
import 'package:sky/src/fn3/binding.dart';
import 'package:sky/src/fn3/framework.dart';
import 'package:sky/src/fn3/navigator.dart';
typedef bool DragTargetWillAccept<T>(T data);
typedef void DragTargetAccept<T>(T data);
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 {
const DragTarget({
......@@ -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 Point dragStartPoint;
final Widget feedback;
final DragFinishedNotification onDragFinished;
DragTargetState _activeTarget;
bool _activeTargetWillAcceptDrop = false;
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;
}
Offset _lastOffset;
void update(Point globalPosition) {
_lastOffset = globalPosition - dragStartPoint;
HitTestResult result = WidgetFlutterBinding.instance.hitTest(globalPosition);
DragTargetState target = _getDragTarget(result.path);
if (target == _activeTarget)
......@@ -103,21 +171,47 @@ class DragController {
_activeTargetWillAcceptDrop = _activeTarget != null && _activeTarget.didEnter(data);
}
void cancel() {
if (_activeTarget != null)
_activeTarget.didLeave(data);
_activeTarget = null;
_activeTargetWillAcceptDrop = false;
DragTargetState _getDragTarget(List<HitTestEntry> path) {
// TODO(abarth): Why do 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 drop() {
if (_activeTarget == null)
return;
if (_activeTargetWillAcceptDrop)
_activeTarget.didDrop(data);
else
_activeTarget.didLeave(data);
void didPop([DragEndKind endKind]) {
if (_activeTarget != null) {
if (endKind == DragEndKind.dropped && _activeTargetWillAcceptDrop)
_activeTarget.didDrop(data);
else
_activeTarget.didLeave(data);
}
_activeTarget = null;
_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 {
return result;
}
bool get ephemeral => true;
bool get modal => true;
Duration get transitionDuration => _kMenuDuration;
bool get isOpaque => false;
Widget build(Key key, NavigatorState navigator, WatchableAnimationPerformance performance) {
bool get opaque => false;
Widget build(Key key, NavigatorState navigator) {
return new Positioned(
top: position?.top,
right: position?.right,
......@@ -189,8 +192,9 @@ class MenuRoute extends Route {
);
}
void popState([dynamic result]) {
void didPop([dynamic 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