mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
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.
This commit is contained in:
parent
4150615e26
commit
a91dd07cb3
@ -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 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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> {
|
|||||||
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> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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> {
|
|||||||
|
|
||||||
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 {
|
|||||||
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 {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void popState([dynamic result]) {
|
void didPop([dynamic result]) {
|
||||||
completer.complete(result);
|
completer.complete(result);
|
||||||
|
super.didPop(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
|||||||
_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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,102 +9,8 @@ import 'package:sky/src/fn3/framework.dart';
|
|||||||
import 'package:sky/src/fn3/transitions.dart';
|
import 'package:sky/src/fn3/transitions.dart';
|
||||||
|
|
||||||
typedef Widget RouteBuilder(NavigatorState navigator, Route route);
|
typedef Widget RouteBuilder(NavigatorState navigator, Route route);
|
||||||
|
|
||||||
typedef void NotificationCallback();
|
typedef void NotificationCallback();
|
||||||
|
|
||||||
abstract class Route {
|
|
||||||
AnimationPerformance _performance;
|
|
||||||
NotificationCallback onDismissed;
|
|
||||||
NotificationCallback onCompleted;
|
|
||||||
AnimationPerformance createPerformance() {
|
|
||||||
AnimationPerformance result = new AnimationPerformance(duration: transitionDuration);
|
|
||||||
result.addStatusListener((AnimationStatus status) {
|
|
||||||
switch (status) {
|
|
||||||
case AnimationStatus.dismissed:
|
|
||||||
if (onDismissed != null)
|
|
||||||
onDismissed();
|
|
||||||
break;
|
|
||||||
case AnimationStatus.completed:
|
|
||||||
if (onCompleted != null)
|
|
||||||
onCompleted();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
WatchableAnimationPerformance ensurePerformance({ Direction direction }) {
|
|
||||||
assert(direction != null);
|
|
||||||
if (_performance == null)
|
|
||||||
_performance = createPerformance();
|
|
||||||
AnimationStatus desiredStatus = direction == Direction.forward ? AnimationStatus.forward : AnimationStatus.reverse;
|
|
||||||
if (_performance.status != desiredStatus)
|
|
||||||
_performance.play(direction);
|
|
||||||
return _performance.view;
|
|
||||||
}
|
|
||||||
bool get isActuallyOpaque => _performance != null && _performance.isCompleted && isOpaque;
|
|
||||||
|
|
||||||
bool get hasContent => true; // set to false if you have nothing useful to return from build()
|
|
||||||
|
|
||||||
Duration get transitionDuration;
|
|
||||||
bool get isOpaque;
|
|
||||||
Widget build(Key key, NavigatorState navigator, WatchableAnimationPerformance performance);
|
|
||||||
void popState([dynamic result]) { assert(result == null); }
|
|
||||||
|
|
||||||
String toString() => '$runtimeType()';
|
|
||||||
}
|
|
||||||
|
|
||||||
const Duration _kTransitionDuration = const Duration(milliseconds: 150);
|
|
||||||
const Point _kTransitionStartPoint = const Point(0.0, 75.0);
|
|
||||||
|
|
||||||
class PageRoute extends Route {
|
|
||||||
PageRoute(this.builder);
|
|
||||||
|
|
||||||
final RouteBuilder builder;
|
|
||||||
|
|
||||||
bool get isOpaque => true;
|
|
||||||
|
|
||||||
Duration get transitionDuration => _kTransitionDuration;
|
|
||||||
|
|
||||||
Widget build(Key key, NavigatorState navigator, WatchableAnimationPerformance performance) {
|
|
||||||
// TODO(jackson): Hit testing should ignore transform
|
|
||||||
// TODO(jackson): Block input unless content is interactive
|
|
||||||
return new SlideTransition(
|
|
||||||
key: key,
|
|
||||||
performance: performance,
|
|
||||||
position: new AnimatedValue<Point>(_kTransitionStartPoint, end: Point.origin, curve: easeOut),
|
|
||||||
child: new FadeTransition(
|
|
||||||
performance: performance,
|
|
||||||
opacity: new AnimatedValue<double>(0.0, end: 1.0, curve: easeOut),
|
|
||||||
child: builder(navigator, this)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
typedef void RouteStateCallback(RouteState route);
|
|
||||||
|
|
||||||
class RouteState extends Route {
|
|
||||||
RouteState({ this.route, this.owner, this.callback });
|
|
||||||
|
|
||||||
Route route;
|
|
||||||
State owner;
|
|
||||||
RouteStateCallback callback;
|
|
||||||
|
|
||||||
bool get isOpaque => false;
|
|
||||||
|
|
||||||
void popState([dynamic result]) {
|
|
||||||
assert(result == null);
|
|
||||||
if (callback != null)
|
|
||||||
callback(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get hasContent => false;
|
|
||||||
Duration get transitionDuration => const Duration();
|
|
||||||
Widget build(Key key, NavigatorState navigator, WatchableAnimationPerformance performance) => null;
|
|
||||||
}
|
|
||||||
|
|
||||||
class Navigator extends StatefulComponent {
|
class Navigator extends StatefulComponent {
|
||||||
Navigator({ this.routes, Key key }) : super(key: key) {
|
Navigator({ this.routes, Key key }) : super(key: key) {
|
||||||
// To use a navigator, you must at a minimum define the route with the name '/'.
|
// To use a navigator, you must at a minimum define the route with the name '/'.
|
||||||
@ -128,6 +34,7 @@ class NavigatorState extends State<Navigator> {
|
|||||||
super.initState(context);
|
super.initState(context);
|
||||||
PageRoute route = new PageRoute(config.routes['/']);
|
PageRoute route = new PageRoute(config.routes['/']);
|
||||||
assert(route != null);
|
assert(route != null);
|
||||||
|
assert(!route.ephemeral);
|
||||||
_history.add(route);
|
_history.add(route);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -147,53 +54,223 @@ class NavigatorState extends State<Navigator> {
|
|||||||
|
|
||||||
void push(Route route) {
|
void push(Route route) {
|
||||||
assert(!_debugCurrentlyHaveRoute(route));
|
assert(!_debugCurrentlyHaveRoute(route));
|
||||||
_history.insert(_currentPosition + 1, route);
|
|
||||||
setState(() {
|
setState(() {
|
||||||
|
while (currentRoute.ephemeral) {
|
||||||
|
assert(currentRoute.ephemeral);
|
||||||
|
currentRoute.didPop(null);
|
||||||
|
_currentPosition -= 1;
|
||||||
|
}
|
||||||
|
_history.insert(_currentPosition + 1, route);
|
||||||
_currentPosition += 1;
|
_currentPosition += 1;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void pop([dynamic result]) {
|
void popRoute(Route route, [dynamic result]) {
|
||||||
if (_currentPosition > 0) {
|
assert(_debugCurrentlyHaveRoute(route));
|
||||||
Route route = _history[_currentPosition];
|
assert(_currentPosition > 0);
|
||||||
route.popState(result);
|
setState(() {
|
||||||
setState(() {
|
while (currentRoute != route) {
|
||||||
|
assert(currentRoute.ephemeral);
|
||||||
|
currentRoute.didPop(null);
|
||||||
_currentPosition -= 1;
|
_currentPosition -= 1;
|
||||||
});
|
}
|
||||||
}
|
assert(_currentPosition > 0);
|
||||||
|
currentRoute.didPop(result);
|
||||||
|
_currentPosition -= 1;
|
||||||
|
});
|
||||||
|
assert(!_debugCurrentlyHaveRoute(route));
|
||||||
|
}
|
||||||
|
|
||||||
|
void pop([dynamic result]) {
|
||||||
|
setState(() {
|
||||||
|
while (currentRoute.ephemeral) {
|
||||||
|
currentRoute.didPop(null);
|
||||||
|
_currentPosition -= 1;
|
||||||
|
}
|
||||||
|
assert(_currentPosition > 0);
|
||||||
|
currentRoute.didPop(result);
|
||||||
|
_currentPosition -= 1;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _debugCurrentlyHaveRoute(Route route) {
|
bool _debugCurrentlyHaveRoute(Route route) {
|
||||||
return _history.any((candidate) => candidate == route);
|
int index = _history.indexOf(route);
|
||||||
|
return index >= 0 && index <= _currentPosition;
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
List<Widget> visibleRoutes = new List<Widget>();
|
List<Widget> visibleRoutes = new List<Widget>();
|
||||||
|
bool alreadyInsertModalBarrier = false;
|
||||||
for (int i = _history.length-1; i >= 0; i -= 1) {
|
for (int i = _history.length-1; i >= 0; i -= 1) {
|
||||||
Route route = _history[i];
|
Route route = _history[i];
|
||||||
if (!route.hasContent)
|
if (!route.hasContent) {
|
||||||
|
assert(!route.modal);
|
||||||
continue;
|
continue;
|
||||||
WatchableAnimationPerformance performance = route.ensurePerformance(
|
}
|
||||||
|
route.ensurePerformance(
|
||||||
direction: (i <= _currentPosition) ? Direction.forward : Direction.reverse
|
direction: (i <= _currentPosition) ? Direction.forward : Direction.reverse
|
||||||
);
|
);
|
||||||
route.onDismissed = () {
|
route._onDismissed = () {
|
||||||
setState(() {
|
setState(() {
|
||||||
assert(_history.contains(route));
|
assert(_history.contains(route));
|
||||||
_history.remove(route);
|
_history.remove(route);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
Key key = new ObjectKey(route);
|
Key key = new ObjectKey(route);
|
||||||
Widget widget = route.build(key, this, performance);
|
Widget widget = route.build(key, this);
|
||||||
visibleRoutes.add(widget);
|
visibleRoutes.add(widget);
|
||||||
if (route.isActuallyOpaque)
|
if (route.isActuallyOpaque)
|
||||||
break;
|
break;
|
||||||
}
|
assert(route.modal || route.ephemeral);
|
||||||
if (visibleRoutes.length > 1) {
|
if (route.modal && i > 0 && !alreadyInsertModalBarrier) {
|
||||||
visibleRoutes.insert(1, new Listener(
|
visibleRoutes.add(new Listener(
|
||||||
onPointerDown: (_) { pop(); },
|
onPointerDown: (_) { pop(); },
|
||||||
child: new Container()
|
child: new Container()
|
||||||
));
|
));
|
||||||
|
alreadyInsertModalBarrier = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return new Focus(child: new Stack(visibleRoutes.reversed.toList()));
|
return new Focus(child: new Stack(visibleRoutes.reversed.toList()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
abstract class Route {
|
||||||
|
|
||||||
|
WatchableAnimationPerformance get performance => _performance?.view;
|
||||||
|
AnimationPerformance _performance;
|
||||||
|
NotificationCallback _onDismissed;
|
||||||
|
|
||||||
|
AnimationPerformance createPerformance() {
|
||||||
|
Duration duration = transitionDuration;
|
||||||
|
if (duration > Duration.ZERO) {
|
||||||
|
return new AnimationPerformance(duration: duration)
|
||||||
|
..addStatusListener((AnimationStatus status) {
|
||||||
|
if (status == AnimationStatus.dismissed && _onDismissed != null)
|
||||||
|
_onDismissed();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ensurePerformance({ Direction direction }) {
|
||||||
|
assert(direction != null);
|
||||||
|
if (_performance == null)
|
||||||
|
_performance = createPerformance();
|
||||||
|
if (_performance != null) {
|
||||||
|
AnimationStatus desiredStatus = direction == Direction.forward ? AnimationStatus.forward : AnimationStatus.reverse;
|
||||||
|
if (_performance.status != desiredStatus)
|
||||||
|
_performance.play(direction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If hasContent is true, then the route represents some on-screen state.
|
||||||
|
///
|
||||||
|
/// If hasContent is false, then no performance will be created, and the values of
|
||||||
|
/// ephemeral, modal, and opaque are ignored. This is useful if the route
|
||||||
|
/// represents some state handled by another widget. See
|
||||||
|
/// NavigatorState.pushState().
|
||||||
|
///
|
||||||
|
/// Set hasContent to false if you have nothing useful to return from build().
|
||||||
|
bool get hasContent => true;
|
||||||
|
|
||||||
|
/// If ephemeral is true, then to explicitly pop the route you have to use
|
||||||
|
/// navigator.popRoute() with a reference to this route. navigator.pop()
|
||||||
|
/// automatically pops all ephemeral routes before popping the current
|
||||||
|
/// top-most non-ephemeral route.
|
||||||
|
///
|
||||||
|
/// If ephemeral is false, then the route can be popped with navigator.pop().
|
||||||
|
///
|
||||||
|
/// Set ephemeral to true if you want to be automatically popped when another
|
||||||
|
/// route is pushed or popped.
|
||||||
|
///
|
||||||
|
/// modal must be true if ephemeral is false.
|
||||||
|
bool get ephemeral => false;
|
||||||
|
|
||||||
|
/// If modal is true, a hidden layer is inserted in the widget tree that
|
||||||
|
/// catches all touches to widgets created by routes below this one, even if
|
||||||
|
/// this one is transparent.
|
||||||
|
///
|
||||||
|
/// If modal is false, then earlier routes can be interacted with, including
|
||||||
|
/// causing new routes to be pushed and/or this route (and maybe others) to be
|
||||||
|
/// popped.
|
||||||
|
///
|
||||||
|
/// ephemeral must be true if modal is false.
|
||||||
|
bool get modal => true;
|
||||||
|
|
||||||
|
/// If opaque is true, then routes below this one will not be built or painted
|
||||||
|
/// when the transition to this route is complete.
|
||||||
|
///
|
||||||
|
/// If opaque is false, then the previous route will always be painted even if
|
||||||
|
/// this route's transition is complete.
|
||||||
|
///
|
||||||
|
/// Set this to true if there's no reason to build and paint the route behind
|
||||||
|
/// you when your transition is finished, and set it to false if you do not
|
||||||
|
/// cover the entire application surface or are in any way semi-transparent.
|
||||||
|
bool get opaque => false;
|
||||||
|
|
||||||
|
/// If this is set to a non-zero [Duration], then an [AnimationPerformance]
|
||||||
|
/// object, available via the performance field, will be created when the
|
||||||
|
/// route is first built, using the duration described here.
|
||||||
|
Duration get transitionDuration => Duration.ZERO;
|
||||||
|
|
||||||
|
bool get isActuallyOpaque => (performance == null || _performance.isCompleted) && opaque;
|
||||||
|
|
||||||
|
Widget build(Key key, NavigatorState navigator);
|
||||||
|
void didPop([dynamic result]) {
|
||||||
|
if (performance == null && _onDismissed != null)
|
||||||
|
_onDismissed();
|
||||||
|
}
|
||||||
|
|
||||||
|
String toString() => '$runtimeType()';
|
||||||
|
}
|
||||||
|
|
||||||
|
const Duration _kTransitionDuration = const Duration(milliseconds: 150);
|
||||||
|
const Point _kTransitionStartPoint = const Point(0.0, 75.0);
|
||||||
|
|
||||||
|
class PageRoute extends Route {
|
||||||
|
PageRoute(this.builder);
|
||||||
|
|
||||||
|
final RouteBuilder builder;
|
||||||
|
|
||||||
|
bool get opaque => true;
|
||||||
|
|
||||||
|
Duration get transitionDuration => _kTransitionDuration;
|
||||||
|
|
||||||
|
Widget build(Key key, NavigatorState navigator) {
|
||||||
|
// TODO(jackson): Hit testing should ignore transform
|
||||||
|
// TODO(jackson): Block input unless content is interactive
|
||||||
|
return new SlideTransition(
|
||||||
|
key: key,
|
||||||
|
performance: performance,
|
||||||
|
position: new AnimatedValue<Point>(_kTransitionStartPoint, end: Point.origin, curve: easeOut),
|
||||||
|
child: new FadeTransition(
|
||||||
|
performance: performance,
|
||||||
|
opacity: new AnimatedValue<double>(0.0, end: 1.0, curve: easeOut),
|
||||||
|
child: builder(navigator, this)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef void RouteStateCallback(RouteState route);
|
||||||
|
|
||||||
|
class RouteState extends Route {
|
||||||
|
RouteState({ this.route, this.owner, this.callback });
|
||||||
|
|
||||||
|
Route route;
|
||||||
|
State owner;
|
||||||
|
RouteStateCallback callback;
|
||||||
|
|
||||||
|
bool get opaque => false;
|
||||||
|
|
||||||
|
void didPop([dynamic result]) {
|
||||||
|
assert(result == null);
|
||||||
|
if (callback != null)
|
||||||
|
callback(this);
|
||||||
|
super.didPop(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get hasContent => false;
|
||||||
|
Widget build(Key key, NavigatorState navigator) => null;
|
||||||
|
}
|
||||||
|
@ -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 {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void popState([dynamic result]) {
|
void didPop([dynamic result]) {
|
||||||
completer.complete(result);
|
completer.complete(result);
|
||||||
|
super.didPop(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
71
packages/unit/test/widget/draggable_test.dart
Normal file
71
packages/unit/test/widget/draggable_test.dart
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
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);
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user