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
|
||||
// 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
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -9,102 +9,8 @@ import 'package:sky/src/fn3/framework.dart';
|
||||
import 'package:sky/src/fn3/transitions.dart';
|
||||
|
||||
typedef Widget RouteBuilder(NavigatorState navigator, Route route);
|
||||
|
||||
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 {
|
||||
Navigator({ this.routes, Key key }) : super(key: key) {
|
||||
// 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);
|
||||
PageRoute route = new PageRoute(config.routes['/']);
|
||||
assert(route != null);
|
||||
assert(!route.ephemeral);
|
||||
_history.add(route);
|
||||
}
|
||||
|
||||
@ -147,53 +54,223 @@ class NavigatorState extends State<Navigator> {
|
||||
|
||||
void push(Route route) {
|
||||
assert(!_debugCurrentlyHaveRoute(route));
|
||||
_history.insert(_currentPosition + 1, route);
|
||||
setState(() {
|
||||
while (currentRoute.ephemeral) {
|
||||
assert(currentRoute.ephemeral);
|
||||
currentRoute.didPop(null);
|
||||
_currentPosition -= 1;
|
||||
}
|
||||
_history.insert(_currentPosition + 1, route);
|
||||
_currentPosition += 1;
|
||||
});
|
||||
}
|
||||
|
||||
void pop([dynamic result]) {
|
||||
if (_currentPosition > 0) {
|
||||
Route route = _history[_currentPosition];
|
||||
route.popState(result);
|
||||
setState(() {
|
||||
void popRoute(Route route, [dynamic result]) {
|
||||
assert(_debugCurrentlyHaveRoute(route));
|
||||
assert(_currentPosition > 0);
|
||||
setState(() {
|
||||
while (currentRoute != route) {
|
||||
assert(currentRoute.ephemeral);
|
||||
currentRoute.didPop(null);
|
||||
_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) {
|
||||
return _history.any((candidate) => candidate == route);
|
||||
int index = _history.indexOf(route);
|
||||
return index >= 0 && index <= _currentPosition;
|
||||
}
|
||||
|
||||
Widget build(BuildContext context) {
|
||||
List<Widget> visibleRoutes = new List<Widget>();
|
||||
bool alreadyInsertModalBarrier = false;
|
||||
for (int i = _history.length-1; i >= 0; i -= 1) {
|
||||
Route route = _history[i];
|
||||
if (!route.hasContent)
|
||||
if (!route.hasContent) {
|
||||
assert(!route.modal);
|
||||
continue;
|
||||
WatchableAnimationPerformance performance = route.ensurePerformance(
|
||||
}
|
||||
route.ensurePerformance(
|
||||
direction: (i <= _currentPosition) ? Direction.forward : Direction.reverse
|
||||
);
|
||||
route.onDismissed = () {
|
||||
route._onDismissed = () {
|
||||
setState(() {
|
||||
assert(_history.contains(route));
|
||||
_history.remove(route);
|
||||
});
|
||||
};
|
||||
Key key = new ObjectKey(route);
|
||||
Widget widget = route.build(key, this, performance);
|
||||
Widget widget = route.build(key, this);
|
||||
visibleRoutes.add(widget);
|
||||
if (route.isActuallyOpaque)
|
||||
break;
|
||||
}
|
||||
if (visibleRoutes.length > 1) {
|
||||
visibleRoutes.insert(1, new Listener(
|
||||
onPointerDown: (_) { pop(); },
|
||||
child: new Container()
|
||||
));
|
||||
assert(route.modal || route.ephemeral);
|
||||
if (route.modal && i > 0 && !alreadyInsertModalBarrier) {
|
||||
visibleRoutes.add(new Listener(
|
||||
onPointerDown: (_) { pop(); },
|
||||
child: new Container()
|
||||
));
|
||||
alreadyInsertModalBarrier = true;
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
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