Commit 05f19cfd authored by Adam Barth's avatar Adam Barth

Merge pull request #1881 from abarth/navigator2

Switch to Navigator2
parents a7cd12fc 50a177b7
......@@ -24,22 +24,20 @@ class StockHomeState extends State<StockHome> {
String _searchQuery;
void _handleSearchBegin() {
Navigator.of(context).pushState(this, (_) {
Navigator.of(context).push(new StateRoute(
onPop: () {
setState(() {
_isSearching = false;
_searchQuery = null;
});
});
}
));
setState(() {
_isSearching = true;
});
}
void _handleSearchEnd() {
assert(() {
final StateRoute currentRoute = Navigator.of(context).currentRoute;
return currentRoute.owner == this;
});
Navigator.of(context).pop();
}
......
......@@ -4,8 +4,6 @@
import 'package:flutter/material.dart';
import 'package:flutter/src/widgets/navigator2.dart' as n2;
class Home extends StatelessComponent {
Widget build(BuildContext context) {
return new Container(
......@@ -15,11 +13,11 @@ class Home extends StatelessComponent {
new Text("You are at home"),
new RaisedButton(
child: new Text('GO SHOPPING'),
onPressed: () => n2.Navigator.of(context).pushNamed('/shopping')
onPressed: () => Navigator.of(context).pushNamed('/shopping')
),
new RaisedButton(
child: new Text('START ADVENTURE'),
onPressed: () => n2.Navigator.of(context).pushNamed('/adventure')
onPressed: () => Navigator.of(context).pushNamed('/adventure')
)],
justifyContent: FlexJustifyContent.center
)
......@@ -36,11 +34,11 @@ class Shopping extends StatelessComponent {
new Text("Village Shop"),
new RaisedButton(
child: new Text('RETURN HOME'),
onPressed: () => n2.Navigator.of(context).pop()
onPressed: () => Navigator.of(context).pop()
),
new RaisedButton(
child: new Text('GO TO DUNGEON'),
onPressed: () => n2.Navigator.of(context).pushNamed('/adventure')
onPressed: () => Navigator.of(context).pushNamed('/adventure')
)],
justifyContent: FlexJustifyContent.center
)
......@@ -57,7 +55,7 @@ class Adventure extends StatelessComponent {
new Text("Monster's Lair"),
new RaisedButton(
child: new Text('RUN!!!'),
onPressed: () => n2.Navigator.of(context).pop()
onPressed: () => Navigator.of(context).pop()
)],
justifyContent: FlexJustifyContent.center
)
......
......@@ -17,14 +17,9 @@ const double _kMinFlingVelocity = 700.0;
const double _kFlingVelocityScale = 1.0 / 300.0;
class _BottomSheet extends StatefulComponent {
_BottomSheet({
Key key,
this.child,
this.performance
}) : super(key: key);
_BottomSheet({ Key key, this.route }) : super(key: key);
final Widget child;
final Performance performance;
final _ModalBottomSheetRoute route;
_BottomSheetState createState() => new _BottomSheetState();
}
......@@ -54,27 +49,39 @@ class _BottomSheetState extends State<_BottomSheet> {
bool _dragEnabled = false;
void _handleDragStart(Point position) {
_dragEnabled = !config.performance.isAnimating;
_dragEnabled = !config.route._performance.isAnimating;
}
void _handleDragUpdate(double delta) {
if (!_dragEnabled)
return;
config.performance.progress -= delta / _layout.childTop.end;
config.route._performance.progress -= delta / _layout.childTop.end;
}
void _handleDragEnd(Offset velocity) {
if (!_dragEnabled)
return;
if (velocity.dy > _kMinFlingVelocity)
config.performance.fling(velocity: -velocity.dy * _kFlingVelocityScale);
config.route._performance.fling(velocity: -velocity.dy * _kFlingVelocityScale);
else
config.performance.forward();
config.route._performance.forward();
}
Widget build(BuildContext context) {
return new BuilderTransition(
performance: config.performance,
return new Focus(
key: new GlobalObjectKey(config.route),
autofocus: true,
child: new GestureDetector(
onTap: () { Navigator.of(context).pop(); },
child: new Stack(<Widget>[
// mask
new ColorTransition(
performance: config.route._performance,
color: new AnimatedColorValue(Colors.transparent, end: Colors.black54),
child: new Container()
),
new BuilderTransition(
performance: config.route._performance,
variables: <AnimatedValue<double>>[_layout.childTop],
builder: (BuildContext context) {
return new ClipRect(
......@@ -85,16 +92,19 @@ class _BottomSheetState extends State<_BottomSheet> {
onVerticalDragStart: _handleDragStart,
onVerticalDragUpdate: _handleDragUpdate,
onVerticalDragEnd: _handleDragEnd,
child: new Material(child: config.child)
child: new Material(child: config.route.child)
)
)
);
}
)
])
)
);
}
}
class _ModalBottomSheetRoute extends Route {
class _ModalBottomSheetRoute extends TransitionRoute {
_ModalBottomSheetRoute({ this.completer, this.child }) {
_performance = new Performance(duration: transitionDuration, debugLabel: 'ModalBottomSheet');
}
......@@ -102,53 +112,27 @@ class _ModalBottomSheetRoute extends Route {
final Completer completer;
final Widget child;
PerformanceView get performance => _performance?.view;
Performance _performance;
bool get ephemeral => true;
bool get modal => true;
bool get opaque => false;
Duration get transitionDuration => _kBottomSheetDuration;
Widget build(RouteArguments args) {
return new Focus(
key: new GlobalObjectKey(this),
autofocus: true,
child: new GestureDetector(
onTap: () { navigator.pop(); },
child: new Stack(<Widget>[
// mask
new ColorTransition(
performance: performance,
color: new AnimatedColorValue(Colors.transparent, end: Colors.black54),
child: new Container()
),
// sheet
new _BottomSheet(
performance: _performance,
child: child
)
])
)
);
}
Performance _performance;
void didPush(NavigatorState navigator) {
super.didPush(navigator);
_performance?.forward();
Performance createPerformance() {
_performance = super.createPerformance();
return _performance;
}
List<Widget> createWidgets() => [ new _BottomSheet(route: this) ];
void didPop([dynamic result]) {
completer.complete(result);
super.didPop(result);
if (_performance.status != PerformanceStatus.dismissed)
_performance?.reverse();
}
}
Future showModalBottomSheet({ BuildContext context, Widget child }) {
final Completer completer = new Completer();
Navigator.of(context).push(new _ModalBottomSheetRoute(
Navigator.of(context).pushEphemeral(new _ModalBottomSheetRoute(
completer: completer,
child: child
));
......
......@@ -100,6 +100,7 @@ class Dialog extends StatelessComponent {
));
}
// TODO(abarth): We should return the backdrop as a separate entry from createWidgets.
return new Stack(<Widget>[
new GestureDetector(
onTap: onDismiss,
......@@ -130,21 +131,27 @@ class Dialog extends StatelessComponent {
}
}
class _DialogRoute extends PerformanceRoute {
_DialogRoute({ this.completer, this.builder });
class _DialogRoute extends TransitionRoute {
_DialogRoute({ this.completer, this.child });
final Completer completer;
final RouteBuilder builder;
final Widget child;
bool get opaque => false;
Duration get transitionDuration => const Duration(milliseconds: 150);
Widget build(RouteArguments args) {
return new FadeTransition(
performance: args.previousPerformance,
List<Widget> createWidgets() {
return [
new Focus(
key: new GlobalObjectKey(this),
autofocus: true,
child: new FadeTransition(
performance: performance,
opacity: new AnimatedValue<double>(0.0, end: 1.0, curve: Curves.easeOut),
child: builder(args)
);
child: child
)
)
];
}
void didPop([dynamic result]) {
......@@ -155,15 +162,6 @@ class _DialogRoute extends PerformanceRoute {
Future showDialog({ BuildContext context, Widget child }) {
Completer completer = new Completer();
Navigator.of(context).push(new _DialogRoute(
completer: completer,
builder: (RouteArguments args) {
return new Focus(
key: new GlobalObjectKey(completer),
autofocus: true,
child: child
);
}
));
Navigator.of(context).push(new _DialogRoute(completer: completer, child: child));
return completer.future;
}
......@@ -31,115 +31,90 @@ const Point _kOpenPosition = Point.origin;
const Point _kClosedPosition = const Point(-_kWidth, 0.0);
class _Drawer extends StatelessComponent {
_Drawer({ Key key, this.route }) : super(key: key);
_Drawer({
Key key,
this.child,
this.level: 3,
this.performance,
this.interactive,
this.route
}) : super(key: key);
final Widget child;
final int level;
final PerformanceView performance;
final bool interactive;
final _DrawerRoute route;
Widget build(BuildContext context) {
return new GestureDetector(
return new Focus(
key: new GlobalObjectKey(route),
autofocus: true,
child: new GestureDetector(
onHorizontalDragStart: (_) {
if (interactive)
if (route.interactive)
route._takeControl();
},
onHorizontalDragUpdate: (double delta) {
if (interactive)
if (route.interactive)
route._moveDrawer(delta);
},
onHorizontalDragEnd: (Offset velocity) {
if (interactive)
if (route.interactive)
route._settle(velocity);
},
child: new Stack(<Widget>[
// mask
new GestureDetector(
onTap: () {
if (interactive)
if (route.interactive)
route._close();
},
child: new ColorTransition(
performance: performance,
performance: route.performance,
color: new AnimatedColorValue(Colors.transparent, end: Colors.black54),
child: new Container()
)
),
// drawer
new Positioned(
top: 0.0,
left: 0.0,
bottom: 0.0,
child: new SlideTransition(
performance: performance,
performance: route.performance,
position: new AnimatedValue<Point>(_kClosedPosition, end: _kOpenPosition),
child: new AnimatedContainer(
curve: Curves.ease,
duration: _kThemeChangeDuration,
decoration: new BoxDecoration(
backgroundColor: Theme.of(context).canvasColor,
boxShadow: shadows[level]),
boxShadow: shadows[route.level]),
width: _kWidth,
child: child
child: route.child
)
)
)
])
)
);
}
}
class _DrawerRoute extends Route {
class _DrawerRoute extends TransitionRoute {
_DrawerRoute({ this.child, this.level });
final Widget child;
final int level;
PerformanceView get performance => _performance?.view;
Performance _performance = new Performance(duration: _kBaseSettleDuration, debugLabel: 'Drawer');
Duration get transitionDuration => _kBaseSettleDuration;
bool get opaque => false;
bool get interactive => _interactive;
bool _interactive = true;
Widget build(RouteArguments args) {
return new Focus(
key: new GlobalObjectKey(this),
autofocus: true,
child: new _Drawer(
child: child,
level: level,
performance: performance,
interactive: _interactive,
route: this
)
);
}
Performance _performance;
void didPush(NavigatorState navigator) {
super.didPush(navigator);
_performance.forward();
Performance createPerformance() {
_performance = super.createPerformance();
return _performance;
}
List<Widget> createWidgets() => [ new _Drawer(route: this) ];
void didPop([dynamic result]) {
assert(result == null); // because we don't do anything with it, so otherwise it'd be lost
super.didPop(result);
if (_performance.status != PerformanceStatus.dismissed)
_performance.reverse();
setState(() {
_interactive = false;
// TODO(ianh): https://github.com/flutter/engine/issues/1539
});
}
void _takeControl() {
......
......@@ -22,23 +22,9 @@ const double _kBaselineOffsetFromBottom = 20.0;
const Border _kDropdownUnderline = const Border(bottom: const BorderSide(color: const Color(0xFFBDBDBD), width: 2.0));
class _DropdownMenu extends StatelessComponent {
_DropdownMenu({
Key key,
this.items,
this.rect,
this.performance,
this.selectedIndex,
this.level: 4
}) : super(key: key) {
assert(items != null);
assert(performance != null);
}
_DropdownMenu({ Key key, this.route }) : super(key: key);
final List<DropdownMenuItem> items;
final Rect rect;
final PerformanceView performance;
final int selectedIndex;
final int level;
final _MenuRoute route;
Widget build(BuildContext context) {
// The menu is shown in three stages (unit timing in brackets):
......@@ -50,11 +36,11 @@ class _DropdownMenu extends StatelessComponent {
// When the menu is dismissed we just fade the entire thing out
// in the first 0.25.
final double unit = 0.5 / (items.length + 1.5);
final double unit = 0.5 / (route.items.length + 1.5);
final List<Widget> children = <Widget>[];
for (int itemIndex = 0; itemIndex < items.length; ++itemIndex) {
for (int itemIndex = 0; itemIndex < route.items.length; ++itemIndex) {
AnimatedValue<double> opacity;
if (itemIndex == selectedIndex) {
if (itemIndex == route.selectedIndex) {
opacity = new AnimatedValue<double>(0.0, end: 1.0, curve: const Interval(0.0, 0.001), reverseCurve: const Interval(0.75, 1.0));
} else {
final double start = (0.5 + (itemIndex + 1) * unit).clamp(0.0, 1.0);
......@@ -62,12 +48,12 @@ class _DropdownMenu extends StatelessComponent {
opacity = new AnimatedValue<double>(0.0, end: 1.0, curve: new Interval(start, end), reverseCurve: const Interval(0.75, 1.0));
}
children.add(new FadeTransition(
performance: performance,
performance: route.performance,
opacity: opacity,
child: new InkWell(
child: items[itemIndex],
child: route.items[itemIndex],
onTap: () {
Navigator.of(context).pop(items[itemIndex].value);
Navigator.of(context).pop(route.items[itemIndex].value);
}
)
));
......@@ -79,13 +65,13 @@ class _DropdownMenu extends StatelessComponent {
reverseCurve: new Interval(0.75, 1.0)
);
final AnimatedValue<double> menuTop = new AnimatedValue<double>(rect.top,
end: rect.top - selectedIndex * rect.height,
final AnimatedValue<double> menuTop = new AnimatedValue<double>(route.rect.top,
end: route.rect.top - route.selectedIndex * route.rect.height,
curve: new Interval(0.25, 0.5),
reverseCurve: const Interval(0.0, 0.001)
);
final AnimatedValue<double> menuBottom = new AnimatedValue<double>(rect.bottom,
end: menuTop.end + items.length * rect.height,
final AnimatedValue<double> menuBottom = new AnimatedValue<double>(route.rect.bottom,
end: menuTop.end + route.items.length * route.rect.height,
curve: new Interval(0.25, 0.5),
reverseCurve: const Interval(0.0, 0.001)
);
......@@ -93,14 +79,25 @@ class _DropdownMenu extends StatelessComponent {
final BoxPainter menuPainter = new BoxPainter(new BoxDecoration(
backgroundColor: Theme.of(context).canvasColor,
borderRadius: 2.0,
boxShadow: shadows[level]
boxShadow: shadows[route.level]
));
return new FadeTransition(
performance: performance,
final RenderBox renderBox = Navigator.of(context).context.findRenderObject();
final Size navigatorSize = renderBox.size;
final RelativeRect menuRect = new RelativeRect.fromSize(route.rect, navigatorSize);
return new Positioned(
top: menuRect.top - (route.selectedIndex * route.rect.height),
right: menuRect.right - _kMenuHorizontalPadding,
left: menuRect.left - _kMenuHorizontalPadding,
child: new Focus(
key: new GlobalObjectKey(route),
autofocus: true,
child: new FadeTransition(
performance: route.performance,
opacity: menuOpacity,
child: new BuilderTransition(
performance: performance,
performance: route.performance,
variables: <AnimatedValue<double>>[menuTop, menuBottom],
builder: (BuildContext context) {
RenderBox renderBox = context.findRenderObject();
......@@ -114,11 +111,13 @@ class _DropdownMenu extends StatelessComponent {
);
}
)
)
)
);
}
}
class _MenuRoute extends PerformanceRoute {
class _MenuRoute extends TransitionRoute {
_MenuRoute({
this.completer,
this.items,
......@@ -133,33 +132,13 @@ class _MenuRoute extends PerformanceRoute {
final int level;
final int selectedIndex;
bool get ephemeral => true;
bool get modal => true;
bool get opaque => false;
Duration get transitionDuration => _kMenuDuration;
Widget build(RouteArguments args) {
final RenderBox renderBox = navigator.context.findRenderObject();
final Size navigatorSize = renderBox.size;
final RelativeRect menuRect = new RelativeRect.fromSize(rect, navigatorSize);
return new Positioned(
top: menuRect.top - (selectedIndex * rect.height),
right: menuRect.right - _kMenuHorizontalPadding,
left: menuRect.left - _kMenuHorizontalPadding,
child: new Focus(
key: new GlobalObjectKey(this),
autofocus: true,
child: new _DropdownMenu(
items: items,
selectedIndex: selectedIndex,
rect: rect,
level: level,
performance: performance
)
)
);
}
List<Widget> createWidgets() => [
new ModalBarrier(),
new _DropdownMenu(route: this)
];
void didPop([dynamic result]) {
completer.complete(result);
......@@ -210,7 +189,7 @@ class DropdownButton<T> extends StatelessComponent {
final RenderBox renderBox = indexedStackKey.currentContext.findRenderObject();
final Rect rect = renderBox.localToGlobal(Point.origin) & renderBox.size;
final Completer completer = new Completer();
Navigator.of(context).push(new _MenuRoute(
Navigator.of(context).pushEphemeral(new _MenuRoute(
completer: completer,
items: items,
selectedIndex: selectedIndex,
......
......@@ -8,9 +8,6 @@ import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/src/widgets/navigator2.dart' as n2;
import 'package:flutter/src/widgets/hero_controller.dart' as n2;
import 'theme.dart';
import 'title.dart';
......@@ -34,8 +31,6 @@ AssetBundle _initDefaultBundle() {
final AssetBundle _defaultBundle = _initDefaultBundle();
const bool _kUseNavigator2 = false;
class MaterialApp extends StatefulComponent {
MaterialApp({
Key key,
......@@ -87,10 +82,10 @@ class _MaterialAppState extends State<MaterialApp> {
void _metricHandler(Size size) => setState(() { _size = size; });
final n2.HeroController _heroController = new n2.HeroController();
final HeroController _heroController = new HeroController();
n2.Route _generateRoute(n2.NamedRouteSettings settings) {
return new n2.HeroPageRoute(
Route _generateRoute(NamedRouteSettings settings) {
return new HeroPageRoute(
builder: (BuildContext context) {
RouteBuilder builder = config.routes[settings.name] ?? config.onGenerateRoute(settings.name);
return builder(new RouteArguments(context: context));
......@@ -101,19 +96,6 @@ class _MaterialAppState extends State<MaterialApp> {
}
Widget build(BuildContext context) {
Widget navigator;
if (_kUseNavigator2) {
navigator = new n2.Navigator(
key: _navigator,
onGenerateRoute: _generateRoute
);
} else {
navigator = new Navigator(
key: _navigator,
routes: config.routes,
onGenerateRoute: config.onGenerateRoute
);
}
return new MediaQuery(
data: new MediaQueryData(size: _size),
child: new Theme(
......@@ -124,7 +106,10 @@ class _MaterialAppState extends State<MaterialApp> {
bundle: _defaultBundle,
child: new Title(
title: config.title,
child: navigator
child: new Navigator(
key: _navigator,
onGenerateRoute: _generateRoute
)
)
)
)
......
......@@ -22,52 +22,53 @@ const double _kMenuMaxWidth = 5.0 * _kMenuWidthStep;
const double _kMenuHorizontalPadding = 16.0;
const double _kMenuVerticalPadding = 8.0;
class PopupMenu extends StatelessComponent {
PopupMenu({
class _PopupMenu extends StatelessComponent {
_PopupMenu({
Key key,
this.items,
this.level: 4,
this.performance
}) : super(key: key) {
assert(items != null);
assert(performance != null);
}
this.route
}) : super(key: key);
final List<PopupMenuItem> items;
final int level;
final PerformanceView performance;
final _MenuRoute route;
Widget build(BuildContext context) {
final BoxPainter painter = new BoxPainter(new BoxDecoration(
backgroundColor: Theme.of(context).canvasColor,
borderRadius: 2.0,
boxShadow: shadows[level]
boxShadow: shadows[route.level]
));
double unit = 1.0 / (items.length + 1.5); // 1.0 for the width and 0.5 for the last item's fade.
double unit = 1.0 / (route.items.length + 1.5); // 1.0 for the width and 0.5 for the last item's fade.
List<Widget> children = <Widget>[];
for (int i = 0; i < items.length; ++i) {
for (int i = 0; i < route.items.length; ++i) {
double start = (i + 1) * unit;
double end = (start + 1.5 * unit).clamp(0.0, 1.0);
children.add(new FadeTransition(
performance: performance,
performance: route.performance,
opacity: new AnimatedValue<double>(0.0, end: 1.0, curve: new Interval(start, end)),
child: new InkWell(
onTap: () { Navigator.of(context).pop(items[i].value); },
child: items[i]
onTap: () { Navigator.of(context).pop(route.items[i].value); },
child: route.items[i]
))
);
}
final AnimatedValue<double> width = new AnimatedValue<double>(0.0, end: 1.0, curve: new Interval(0.0, unit));
final AnimatedValue<double> height = new AnimatedValue<double>(0.0, end: 1.0, curve: new Interval(0.0, unit * items.length));
final AnimatedValue<double> height = new AnimatedValue<double>(0.0, end: 1.0, curve: new Interval(0.0, unit * route.items.length));
return new FadeTransition(
performance: performance,
return new Positioned(
top: route.position?.top,
right: route.position?.right,
bottom: route.position?.bottom,
left: route.position?.left,
child: new Focus(
key: new GlobalObjectKey(route),
autofocus: true,
child: new FadeTransition(
performance: route.performance,
opacity: new AnimatedValue<double>(0.0, end: 1.0, curve: new Interval(0.0, 1.0 / 3.0)),
child: new BuilderTransition(
performance: performance,
performance: route.performance,
variables: <AnimatedValue<double>>[width, height],
builder: (BuildContext context) {
return new CustomPaint(
......@@ -85,6 +86,7 @@ class PopupMenu extends StatelessComponent {
stepWidth: _kMenuWidthStep,
child: new ScrollableViewport(
child: new Container(
// TODO(abarth): Teach Block about padding.
padding: const EdgeDims.symmetric(
horizontal: _kMenuHorizontalPadding,
vertical: _kMenuVerticalPadding
......@@ -97,6 +99,8 @@ class PopupMenu extends StatelessComponent {
);
}
)
)
)
);
}
}
......@@ -109,7 +113,7 @@ class MenuPosition {
final double left;
}
class _MenuRoute extends PerformanceRoute {
class _MenuRoute extends TransitionRoute {
_MenuRoute({ this.completer, this.position, this.items, this.level });
final Completer completer;
......@@ -125,28 +129,13 @@ class _MenuRoute extends PerformanceRoute {
return result;
}
bool get ephemeral => true;
bool get modal => true;
bool get opaque => false;
Duration get transitionDuration => _kMenuDuration;
Widget build(RouteArguments args) {
return new Positioned(
top: position?.top,
right: position?.right,
bottom: position?.bottom,
left: position?.left,
child: new Focus(
key: new GlobalObjectKey(this),
autofocus: true,
child: new PopupMenu(
items: items,
level: level,
performance: performance
)
)
);
}
List<Widget> createWidgets() => [
new ModalBarrier(),
new _PopupMenu(route: this),
];
void didPop([dynamic result]) {
completer.complete(result);
......@@ -156,7 +145,7 @@ class _MenuRoute extends PerformanceRoute {
Future showMenu({ BuildContext context, MenuPosition position, List<PopupMenuItem> items, int level: 4 }) {
Completer completer = new Completer();
Navigator.of(context).push(new _MenuRoute(
Navigator.of(context).pushEphemeral(new _MenuRoute(
completer: completer,
position: position,
items: items,
......
......@@ -34,19 +34,19 @@ class SnackBarAction extends StatelessComponent {
}
}
class SnackBar extends StatelessComponent {
SnackBar({
class _SnackBar extends StatelessComponent {
_SnackBar({
Key key,
this.content,
this.actions,
this.performance
this.route
}) : super(key: key) {
assert(content != null);
}
final Widget content;
final List<SnackBarAction> actions;
final PerformanceView performance;
final _SnackBarRoute route;
Widget build(BuildContext context) {
List<Widget> children = <Widget>[
......@@ -63,7 +63,7 @@ class SnackBar extends StatelessComponent {
if (actions != null)
children.addAll(actions);
return new SquashTransition(
performance: performance,
performance: route.performance,
height: new AnimatedValue<double>(
0.0,
end: kSnackBarHeight,
......@@ -91,27 +91,18 @@ class SnackBar extends StatelessComponent {
}
}
class _SnackBarRoute extends PerformanceRoute {
_SnackBarRoute({ this.content, this.actions });
final Widget content;
final List<SnackBarAction> actions;
bool get hasContent => false;
bool get ephemeral => true;
bool get modal => false;
class _SnackBarRoute extends TransitionRoute {
bool get opaque => false;
Duration get transitionDuration => const Duration(milliseconds: 200);
Widget build(RouteArguments args) => null;
}
void showSnackBar({ BuildContext context, GlobalKey<PlaceholderState> placeholderKey, Widget content, List<SnackBarAction> actions }) {
Route route = new _SnackBarRoute();
SnackBar snackBar = new SnackBar(
_SnackBarRoute route = new _SnackBarRoute();
_SnackBar snackBar = new _SnackBar(
route: route,
content: content,
actions: actions,
performance: route.performance
actions: actions
);
placeholderKey.currentState.child = snackBar;
Navigator.of(context).push(route);
Navigator.of(context).pushEphemeral(route);
}
......@@ -10,6 +10,7 @@ import 'basic.dart';
import 'binding.dart';
import 'framework.dart';
import 'navigator.dart';
import 'overlay.dart';
typedef bool DragTargetWillAccept<T>(T data);
typedef void DragTargetAccept<T>(T data);
......@@ -61,11 +62,11 @@ class Draggable extends StatefulComponent {
}
class _DraggableState extends State<Draggable> {
DragRoute _route;
_DragAvatar _avatar;
void _startDrag(PointerInputEvent 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.
if (_avatar != null)
return; // TODO(ianh): once we switch to using gestures, just hand the gesture to the avatar so it can do everything itself. then we can have multiple drags at the same time.
final Point point = new Point(event.x, event.y);
Point dragStartPoint;
switch (config.dragAnchor) {
......@@ -78,39 +79,38 @@ class _DraggableState extends State<Draggable> {
break;
}
assert(dragStartPoint != null);
_route = new DragRoute(
_avatar = new _DragAvatar(
data: config.data,
dragStartPoint: dragStartPoint,
feedback: config.feedback,
feedbackOffset: config.feedbackOffset,
onDragFinished: () {
_route = null;
_avatar = null;
}
);
_route.update(point);
Navigator.of(context).push(_route);
_avatar.update(point);
_avatar.rebuild(context);
}
void _updateDrag(PointerInputEvent event) {
if (_route != null) {
Navigator.of(context).setState(() {
_route.update(new Point(event.x, event.y));
});
if (_avatar != null) {
_avatar.update(new Point(event.x, event.y));
_avatar.rebuild(context);
}
}
void _cancelDrag(PointerInputEvent event) {
if (_route != null) {
Navigator.of(context).popRoute(_route, DragEndKind.canceled);
assert(_route == null);
if (_avatar != null) {
_avatar.finish(_DragEndKind.canceled);
assert(_avatar == null);
}
}
void _drop(PointerInputEvent event) {
if (_route != null) {
_route.update(new Point(event.x, event.y));
Navigator.of(context).popRoute(_route, DragEndKind.dropped);
assert(_route == null);
if (_avatar != null) {
_avatar.update(new Point(event.x, event.y));
_avatar.finish(_DragEndKind.dropped);
assert(_avatar == null);
}
}
......@@ -187,10 +187,10 @@ class DragTargetState<T> extends State<DragTarget<T>> {
}
enum DragEndKind { dropped, canceled }
enum _DragEndKind { dropped, canceled }
class DragRoute extends Route {
DragRoute({
class _DragAvatar {
_DragAvatar({
this.data,
this.dragStartPoint: Point.origin,
this.feedback,
......@@ -209,6 +209,7 @@ class DragRoute extends Route {
DragTargetState _activeTarget;
bool _activeTargetWillAcceptDrop = false;
Offset _lastOffset;
OverlayEntry _entry;
void update(Point globalPosition) {
_lastOffset = globalPosition - dragStartPoint;
......@@ -222,6 +223,12 @@ class DragRoute extends Route {
_activeTargetWillAcceptDrop = _activeTarget != null && _activeTarget.didEnter(data);
}
void rebuild(BuildContext context) {
_entry?.remove();
_entry = new OverlayEntry(child: _build(context));
Navigator.of(context).overlay.insert(_entry);
}
DragTargetState _getDragTarget(List<HitTestEntry> path) {
// TODO(abarth): Why do we reverse the path here?
for (HitTestEntry entry in path.reversed) {
......@@ -234,25 +241,21 @@ class DragRoute extends Route {
return null;
}
void didPop([DragEndKind endKind]) {
void finish(_DragEndKind endKind) {
if (_activeTarget != null) {
if (endKind == DragEndKind.dropped && _activeTargetWillAcceptDrop)
if (endKind == _DragEndKind.dropped && _activeTargetWillAcceptDrop)
_activeTarget.didDrop(data);
else
_activeTarget.didLeave(data);
}
_activeTarget = null;
_activeTargetWillAcceptDrop = false;
_entry.remove();
if (onDragFinished != null)
onDragFinished();
super.didPop(endKind);
}
bool get ephemeral => true;
bool get modal => false;
bool get opaque => false;
Widget build(RouteArguments args) {
Widget _build(BuildContext context) {
return new Positioned(
left: _lastOffset.dx,
top: _lastOffset.dy,
......
......@@ -8,7 +8,7 @@ import 'package:flutter/rendering.dart';
import 'basic.dart';
import 'framework.dart';
import 'heroes.dart';
import 'navigator2.dart';
import 'navigator.dart';
import 'overlay.dart';
import 'page.dart';
......@@ -69,10 +69,9 @@ class HeroController {
}
void _addHeroesToOverlay(Iterable<Widget> heroes, OverlayState overlay) {
OverlayEntry insertionPoint = _to.topEntry;
for (Widget hero in heroes) {
OverlayEntry entry = new OverlayEntry(child: hero);
overlay.insert(entry, above: insertionPoint);
overlay.insert(entry);
_overlayEntries.add(entry);
}
}
......
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'basic.dart';
import 'framework.dart';
import 'navigator.dart';
class ModalBarrier extends StatelessComponent {
ModalBarrier({ Key key }) : super(key: key);
Widget build(BuildContext context) {
return new Listener(
onPointerDown: (_) {
Navigator.of(context).pop();
},
child: new Container()
);
}
}
......@@ -2,50 +2,48 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/animation.dart';
import 'package:flutter/rendering.dart';
import 'basic.dart';
import 'focus.dart';
import 'framework.dart';
import 'heroes.dart';
import 'transitions.dart';
import 'gridpaper.dart';
/// Set this to true to overlay a pixel grid on the screen, with every 100
/// pixels marked with a thick maroon line and every 10 pixels marked with a
/// thin maroon line. This can help with verifying widget positions.
bool debugShowGrid = false;
Color debugGridColor = const Color(0x7F7F2020);
const String kDefaultRouteName = '/';
import 'overlay.dart';
// ---------------- Begin scaffolding for Navigator1 to Navigator2 transition
class RouteArguments {
const RouteArguments({ this.context, this.previousPerformance, this.nextPerformance });
const RouteArguments({ this.context });
final BuildContext context;
final PerformanceView previousPerformance;
final PerformanceView nextPerformance;
}
typedef Widget RouteBuilder(RouteArguments args);
typedef RouteBuilder RouteGenerator(String name);
typedef void StateRouteCallback(StateRoute route);
// ---------------- End scaffolding for Navigator1 to Navigator2 transition
abstract class Route {
List<OverlayEntry> get overlayEntries;
void didPush(OverlayState overlay, OverlayEntry insertionPoint);
void didMakeCurrent();
void didPop(dynamic result);
}
class NamedRouteSettings {
const NamedRouteSettings({ this.name: '<anonymous>', this.mostValuableKeys });
final String name;
final Set<Key> mostValuableKeys;
}
typedef Route RouteFactory(NamedRouteSettings settings);
class Navigator extends StatefulComponent {
Navigator({
Key key,
this.routes,
this.onGenerateRoute, // you need to implement this if you pushNamed() to names that might not be in routes.
this.onUnknownRoute // 404 generator. You only need to implement this if you have a way to navigate to arbitrary names.
this.onGenerateRoute,
this.onUnknownRoute
}) : super(key: key) {
// To use a navigator, you must at a minimum define the route with the name '/'.
assert(routes != null);
assert(routes.containsKey(kDefaultRouteName));
assert(onGenerateRoute != null);
}
final Map<String, RouteBuilder> routes;
final RouteGenerator onGenerateRoute;
final RouteBuilder onUnknownRoute;
final RouteFactory onGenerateRoute;
final RouteFactory onUnknownRoute;
static const String defaultRouteName = '/';
static NavigatorState of(BuildContext context) {
NavigatorState result;
......@@ -62,656 +60,75 @@ class Navigator extends StatefulComponent {
NavigatorState createState() => new NavigatorState();
}
// The navigator tracks which "page" we are on.
// It also animates between these pages.
// Pages can have "heroes", which are UI elements that animate from point to point.
// These animations are called journeys.
//
// Journeys can start in two conditions:
// - Everything is calm, and we have no heroes in flight. In this case, we will
// have to collect the heroes from the route we're starting at and the route
// we're going to, and try to transition from one set to the other.
// - We already have heroes in flight. In that case, we just want to look at
// the heroes of our destination, and then try to transition to them from the
// in-flight heroes.
class _HeroTransitionInstruction {
Route from;
Route to;
void update(Route newFrom, Route newTo) {
assert(newFrom != null);
assert(newTo != null);
if (!newFrom.canHaveHeroes || !newTo.canHaveHeroes)
return;
assert(newFrom.performance != null);
assert(newTo.performance != null);
if (from == null)
from = newFrom;
to = newTo;
if (from == to)
reset();
}
void reset() {
assert(hasInstructions);
from = null;
to = null;
}
bool get hasInstructions => from != null || to != null;
}
class NavigatorState extends State<Navigator> {
final GlobalKey<OverlayState> _overlayKey = new GlobalKey<OverlayState>();
final List<Route> _ephemeral = new List<Route>();
final List<Route> _modal = new List<Route>();
List<Route> _history = new List<Route>();
int _currentPosition = 0; // which route is "current"
Route get currentRoute => _history[_currentPosition];
bool get hasPreviousRoute => _history.length > 1;
void initState() {
super.initState();
_activeHeroes = new HeroParty(onQuestFinished: _handleHeroQuestFinished);
PageRoute route = new PageRoute(config.routes[kDefaultRouteName], name: kDefaultRouteName);
assert(route.hasContent);
assert(!route.ephemeral);
_insertRoute(route);
}
void pushState(State owner, StateRouteCallback onPop) {
push(new StateRoute(
route: currentRoute,
owner: owner,
onPop: onPop
));
}
void pushNamed(String name, { Set<Key> mostValuableKeys }) {
RouteBuilder generateRoute() {
assert(config.onGenerateRoute != null);
return config.onGenerateRoute(name);
}
final RouteBuilder builder = config.routes[name] ?? generateRoute() ?? config.onUnknownRoute;
assert(builder != null); // 404 getting your 404!
push(new PageRoute(builder, name: name, mostValuableKeys: mostValuableKeys));
}
final _HeroTransitionInstruction _desiredHeroes = new _HeroTransitionInstruction();
HeroParty _activeHeroes;
void _handleHeroQuestFinished() {
for (Route route in _history)
route._hasActiveHeroes = false;
}
void push(Route route) {
assert(!_debugCurrentlyHaveRoute(route));
setState(() {
// pop ephemeral routes (like popup menus)
while (currentRoute.ephemeral) {
currentRoute.didPop(null);
_currentPosition -= 1;
}
// find the most recent active route that might have heroes
if (route.hasContent) {
int index = _currentPosition;
while (index > 0 && !_history[index].hasContent)
index -= 1;
assert(_history[index].hasContent);
_desiredHeroes.update(_history[index], route);
}
// add the new route
_currentPosition += 1;
_insertRoute(route);
});
}
void popRoute(Route route, [dynamic result]) {
assert(_debugCurrentlyHaveRoute(route));
assert(_currentPosition > 0);
setState(() {
// pop any routes above this one (they must be ephemeral, otherwise there's an error)
while (currentRoute != route) {
assert(currentRoute.ephemeral);
currentRoute.didPop(null);
_currentPosition -= 1;
}
});
pop(result);
assert(!_debugCurrentlyHaveRoute(route));
}
void pop([dynamic result]) {
setState(() {
assert(_currentPosition > 0);
// find the most recent previous route that might have heroes
if (currentRoute.hasContent) {
int index = _currentPosition - 1;
while (index > 0 && !_history[index].hasContent)
index -= 1;
assert(_history[index].hasContent);
_desiredHeroes.update(currentRoute, _history[index]);
}
// pop the route
currentRoute.didPop(result);
_currentPosition -= 1;
});
}
bool _debugCurrentlyHaveRoute(Route route) {
int index = _history.indexOf(route);
return index >= 0 && index <= _currentPosition;
}
void _didCompleteRoute(Route route) {
assert(_history.contains(route));
if (route.isActuallyOpaque) {
setState(() {
// we need to rebuild because our build function depends on
// whether the route is opaque or not.
});
}
}
void _didDismissRoute(Route route) {
assert(_history.contains(route));
if (_history.lastIndexOf(route) <= _currentPosition)
popRoute(route);
}
void _insertRoute(Route route) {
_history.insert(_currentPosition, route);
route.didPush(this);
}
void _removeRoute(Route route) {
assert(_history.contains(route));
setState(() {
if (_desiredHeroes.hasInstructions) {
if (_desiredHeroes.from == route || _desiredHeroes.to == route)
_desiredHeroes.reset();
}
_history.remove(route);
});
}
PerformanceView _currentHeroPerformance;
Widget build(BuildContext context) {
List<Widget> visibleRoutes = <Widget>[];
assert(() {
if (debugShowGrid)
visibleRoutes.add(new GridPaper(color: debugGridColor));
return true;
});
bool alreadyInsertedHeroes = false;
bool alreadyInsertedModalBarrier = false;
Route nextContentRoute;
PerformanceView nextHeroPerformance;
for (int i = _history.length-1; i >= 0; i -= 1) {
final Route route = _history[i];
if (!route.hasContent) {
assert(!route.modal);
assert(!_desiredHeroes.hasInstructions || (_desiredHeroes.from != route && _desiredHeroes.to != route));
assert(!route._hasActiveHeroes);
continue;
}
if (route._hasActiveHeroes && !alreadyInsertedHeroes) {
visibleRoutes.addAll(_activeHeroes.getWidgets(context, _currentHeroPerformance));
alreadyInsertedHeroes = true;
}
if (_desiredHeroes.hasInstructions) {
if ((_desiredHeroes.to == route || _desiredHeroes.from == route) && nextHeroPerformance == null)
nextHeroPerformance = route.performance;
visibleRoutes.add(new _RouteWidget(route: route, nextRoute: nextContentRoute, buildTargetHeroes: _desiredHeroes.to == route));
} else {
visibleRoutes.add(new _RouteWidget(route: route, nextRoute: nextContentRoute));
}
if (route.isActuallyOpaque) {
assert(!_desiredHeroes.hasInstructions ||
(_history.indexOf(_desiredHeroes.from) >= i && _history.indexOf(_desiredHeroes.to) >= i));
break;
}
assert(route.modal || route.ephemeral);
if (route.modal && i > 0 && !alreadyInsertedModalBarrier) {
visibleRoutes.add(new Listener(
onPointerDown: (_) { pop(); },
child: new Container()
));
alreadyInsertedModalBarrier = true;
}
nextContentRoute = route;
}
if (_desiredHeroes.hasInstructions) {
assert(nextHeroPerformance != null);
scheduler.requestPostFrameCallback((Duration timestamp) {
Map<Object, HeroHandle> heroesFrom;
Map<Object, HeroHandle> heroesTo;
Set<Key> mostValuableKeys = new Set<Key>();
if (_desiredHeroes.from.mostValuableKeys != null)
mostValuableKeys.addAll(_desiredHeroes.from.mostValuableKeys);
if (_desiredHeroes.to.mostValuableKeys != null)
mostValuableKeys.addAll(_desiredHeroes.to.mostValuableKeys);
if (_activeHeroes.isEmpty) {
assert(!_desiredHeroes.from._hasActiveHeroes);
heroesFrom = _desiredHeroes.from.getHeroesToAnimate(mostValuableKeys);
_desiredHeroes.from._hasActiveHeroes = heroesFrom.length > 0;
} else {
assert(_desiredHeroes.from._hasActiveHeroes);
heroesFrom = _activeHeroes.getHeroesToAnimate();
}
heroesTo = _desiredHeroes.to.getHeroesToAnimate(mostValuableKeys);
_desiredHeroes.to._hasActiveHeroes = heroesTo.length > 0;
_desiredHeroes.reset();
setState(() {
final RenderBox renderObject = context.findRenderObject();
final Point animationTopLeft = renderObject.localToGlobal(Point.origin);
final Point animationBottomRight = renderObject.localToGlobal(renderObject.size.bottomRight(Point.origin));
final Rect animationArea = new Rect.fromLTRB(animationTopLeft.x, animationTopLeft.y, animationBottomRight.x, animationBottomRight.y);
Curve curve = Curves.ease;
if (nextHeroPerformance.status == PerformanceStatus.reverse) {
nextHeroPerformance = new ReversePerformance(nextHeroPerformance);
curve = new Interval(nextHeroPerformance.progress, 1.0, curve: curve);
}
_activeHeroes.animate(heroesFrom, heroesTo, animationArea, curve);
_currentHeroPerformance = nextHeroPerformance;
});
});
}
return new Focus(child: new Stack(visibleRoutes.reversed.toList()));
}
}
class _RouteWidget extends StatefulComponent {
_RouteWidget({
Route route,
this.nextRoute,
this.buildTargetHeroes: false
}) : route = route,
super(key: new ObjectKey(route)) {
assert(route != null);
}
final Route route;
final Route nextRoute;
final bool buildTargetHeroes;
_RouteWidgetState createState() => new _RouteWidgetState();
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
if (route.performance != null)
description.add('${route.performance}');
else
description.add('${route.debugLabel}');
if (buildTargetHeroes)
description.add('building target heroes this frame');
}
}
class _RouteWidgetState extends State<_RouteWidget> {
void initState() {
super.initState();
config.route._widgetState = this;
push(config.onGenerateRoute(new NamedRouteSettings(name: Navigator.defaultRouteName)));
}
void dispose() {
config.route._widgetState = null;
super.dispose();
}
Widget build(BuildContext context) {
return config.route._internalBuild(context, config.nextRoute, buildTargetHeroes: config.buildTargetHeroes);
}
}
class _StorageEntryIdentifier {
Type clientType;
List<Key> keys;
void addKey(Key key) {
assert(key != null);
assert(key is! GlobalKey);
keys ??= <Key>[];
keys.add(key);
}
GlobalKey scopeKey;
bool operator ==(dynamic other) {
if (other is! _StorageEntryIdentifier)
return false;
final _StorageEntryIdentifier typedOther = other;
if (clientType != typedOther.clientType ||
scopeKey != typedOther.scopeKey ||
keys?.length != typedOther.keys?.length)
return false;
if (keys != null) {
for (int index = 0; index < keys.length; index += 1) {
if (keys[index] != typedOther.keys[index])
return false;
}
}
return true;
}
int get hashCode {
int value = 373;
value = 37 * value + clientType.hashCode;
value = 37 * value + scopeKey.hashCode;
if (keys != null) {
for (Key key in keys)
value = 37 * value + key.hashCode;
}
return value;
}
}
abstract class Route {
Route() {
_subtreeKey = new GlobalKey(label: debugLabel);
}
/// 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().
///
/// modal must be false if hasContent is false, since otherwise any
/// interaction with the system at all would imply that the current route is
/// popped, which would be pointless.
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.
/// hasContent must be true if modal is true.
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;
PerformanceView get performance => null;
bool get isActuallyOpaque => (performance == null || performance.isCompleted) && opaque;
bool get hasPreviousRoute => _modal.length > 1;
OverlayState get overlay => _overlayKey.currentState;
NavigatorState get navigator => _navigator;
NavigatorState _navigator;
_RouteWidgetState _widgetState;
void setState(void fn()) {
if (_widgetState != null)
_widgetState.setState(fn);
else
fn();
}
void didPush(NavigatorState navigator) {
assert(_navigator == null);
_navigator = navigator;
assert(_navigator != null);
performance?.addStatusListener(_handlePerformanceStatusChanged);
}
void didPop([dynamic result]) {
assert(navigator != null);
if (performance == null)
navigator._removeRoute(this);
}
void _handlePerformanceStatusChanged(PerformanceStatus status) {
if (status == PerformanceStatus.completed) {
navigator._didCompleteRoute(this);
} else if (status == PerformanceStatus.dismissed) {
navigator._didDismissRoute(this);
navigator._removeRoute(this);
_navigator = null;
OverlayEntry get _currentOverlay {
for (Route route in _ephemeral.reversed) {
if (route.overlayEntries.isNotEmpty)
return route.overlayEntries.last;
}
for (Route route in _modal.reversed) {
if (route.overlayEntries.isNotEmpty)
return route.overlayEntries.last;
}
/// Called (indirectly, via a RouteWidget) by the navigator.build()
/// function if hasContent is true, to get the subtree for this
/// route.
///
/// If buildTargetHeroes is true, then getHeroesToAnimate() will be called
/// after this build, before the next build, and this build should render the
/// route off-screen, at the end of its animation. Next frame, the argument
/// will be false, and the tree should be built at the first frame of the
/// transition animation, whatever that is.
Widget _internalBuild(BuildContext context, Route nextRoute, { bool buildTargetHeroes: false }) {
assert(navigator != null);
assert(_widgetState != null);
assert(hasContent);
return keySubtree(build(new RouteArguments(
context: context,
previousPerformance: performance,
nextPerformance: nextRoute?.performance
)));
return null;
}
bool get canHaveHeroes => hasContent && modal && opaque;
Set<Key> get mostValuableKeys => null;
/// Return a party of heroes (one per tag) to animate. This is called by the
/// navigator when hasContent is true just after this route, the previous
/// route, or the next route, has been pushed or popped, to figure out which
/// heroes it should be trying to animate.
Map<Object, HeroHandle> getHeroesToAnimate([Set<Key> mostValuableKeys]) => const <Object, HeroHandle>{};
bool _hasActiveHeroes = false;
Route get currentRoute => _ephemeral.isNotEmpty ? _ephemeral.last : _modal.last;
GlobalKey _subtreeKey;
/// Returns the BuildContext for the root of the subtree built for this route,
/// assuming that internalBuild used keySubtree to build that subtree.
/// This is only valid after a build phase.
BuildContext get subtreeContext => _subtreeKey.currentContext;
/// Wraps the given subtree in a route-specific GlobalKey.
Widget keySubtree(Widget child) {
return new KeyedSubtree(
key: _subtreeKey,
child: child
);
Route _removeCurrentRoute() {
return _ephemeral.isNotEmpty ? _ephemeral.removeLast() : _modal.removeLast();
}
/// Called by internalBuild. This is the method to override if you want to
/// change what subtree is built for this route.
Widget build(RouteArguments args);
static Route of(BuildContext context) {
Route result;
context.visitAncestorElements((Element element) {
if (element is StatefulComponentElement && element.state is _RouteWidgetState) {
result = element.widget.route;
return false;
}
return true;
});
return result;
}
_StorageEntryIdentifier _computeStorageIdentifier(BuildContext context) {
_StorageEntryIdentifier result = new _StorageEntryIdentifier();
result.clientType = context.widget.runtimeType;
Key lastKey = context.widget.key;
if (lastKey is! GlobalKey) {
context.visitAncestorElements((Element element) {
if (element.widget.key is GlobalKey) {
lastKey = element.widget.key;
return false;
} else if (element.widget is Navigator) {
// Not quite everyone who is in a Navigator actually is in a Route.
// For example, the modal barrier.
StatefulComponentElement statefulElement = element;
lastKey = new GlobalObjectKey(statefulElement.state);
return false;
} else if (element.widget.key != null) {
result.addKey(element.widget.key);
}
return true;
});
return result;
}
assert(lastKey is GlobalKey);
result.scopeKey = lastKey;
return result;
}
Map<_StorageEntryIdentifier, dynamic> _storage;
void writeState(BuildContext context, dynamic data) {
_storage ??= <_StorageEntryIdentifier, dynamic>{};
_storage[_computeStorageIdentifier(context)] = data;
}
dynamic readState(BuildContext context) => _storage != null ? _storage[_computeStorageIdentifier(context)] : null;
String get debugLabel => '$runtimeType';
String toString() => '$runtimeType(performance: $performance; key: $_subtreeKey)';
}
abstract class PerformanceRoute extends Route {
PerformanceRoute() {
_performance = createPerformance();
}
PerformanceView get performance => _performance?.view;
Performance _performance;
Performance createPerformance() {
Duration duration = transitionDuration;
assert(duration != null && duration >= Duration.ZERO);
return new Performance(duration: duration, debugLabel: debugLabel);
}
Duration get transitionDuration;
Widget _internalBuild(BuildContext context, Route nextRoute, { bool buildTargetHeroes: false }) {
assert(hasContent);
assert(transitionDuration > Duration.ZERO);
if (buildTargetHeroes && performance.progress != 1.0) {
Performance fakePerformance = createPerformance();
assert(fakePerformance != null);
fakePerformance.progress = 1.0;
return new OffStage(
child: keySubtree(
build(new RouteArguments(context: context, previousPerformance: fakePerformance))
)
void pushNamed(String name, { Set<Key> mostValuableKeys }) {
NamedRouteSettings settings = new NamedRouteSettings(
name: name,
mostValuableKeys: mostValuableKeys
);
}
return super._internalBuild(context, nextRoute, buildTargetHeroes: buildTargetHeroes);
push(config.onGenerateRoute(settings) ?? config.onUnknownRoute(settings));
}
void didPush(NavigatorState navigator) {
super.didPush(navigator);
_performance?.forward();
void push(Route route) {
_popAllEphemeralRoutes();
route.didPush(overlay, _currentOverlay);
_modal.add(route);
route.didMakeCurrent();
}
void didPop([dynamic result]) {
_performance?.reverse();
super.didPop(result);
void pushEphemeral(Route route) {
route.didPush(overlay, _currentOverlay);
_ephemeral.add(route);
route.didMakeCurrent();
}
}
const Duration _kTransitionDuration = const Duration(milliseconds: 150);
const Point _kTransitionStartPoint = const Point(0.0, 75.0);
/// A route that represents a page in an application.
///
/// PageRoutes try to animate between themselves in a fashion that is aware of
/// any Heroes.
class PageRoute extends PerformanceRoute {
PageRoute(this._builder, {
this.name: '<anonymous>',
Set<Key> mostValuableKeys
}) : _mostValuableKeys = mostValuableKeys {
assert(_builder != null);
void _popAllEphemeralRoutes() {
List<Route> localEphemeral = new List<Route>.from(_ephemeral);
_ephemeral.clear();
for (Route route in localEphemeral)
route.didPop(null);
assert(_ephemeral.isEmpty);
}
final RouteBuilder _builder;
final String name;
final Set<Key> _mostValuableKeys;
Set<Key> get mostValuableKeys => _mostValuableKeys;
bool get opaque => true;
Duration get transitionDuration => _kTransitionDuration;
Map<Object, HeroHandle> getHeroesToAnimate([Set<Key> mostValuableKeys]) {
return Hero.of(subtreeContext, mostValuableKeys);
void pop([dynamic result]) {
_removeCurrentRoute().didPop(result);
currentRoute.didMakeCurrent();
}
Widget build(RouteArguments args) {
// TODO(jackson): Hit testing should ignore transform
// TODO(jackson): Block input unless content is interactive
// TODO(ianh): Support having different transitions, e.g. when heroes are around.
return new SlideTransition(
performance: args.previousPerformance,
position: new AnimatedValue<Point>(_kTransitionStartPoint, end: Point.origin, curve: Curves.easeOut),
child: new FadeTransition(
performance: args.previousPerformance,
opacity: new AnimatedValue<double>(0.0, end: 1.0, curve: Curves.easeOut),
child: invokeBuilder(args)
)
Widget build(BuildContext context) {
return new Overlay(
key: _overlayKey,
initialEntries: _modal.first.overlayEntries
);
}
Widget invokeBuilder(RouteArguments args) {
Widget result = _builder(args);
assert(() {
if (result == null)
debugPrint('The builder for route \'$name\' returned null. RouteBuilders must never return null.');
assert(result != null && 'A RouteBuilder returned null. See the previous log message for details.' is String);
return true;
});
return result;
}
String get debugLabel => '${super.debugLabel}($name)';
}
class StateRoute extends Route {
StateRoute({ this.route, this.owner, this.onPop });
Route route;
State owner;
StateRouteCallback onPop;
bool get hasContent => false;
bool get modal => false;
bool get opaque => false;
void didPop([dynamic result]) {
assert(result == null);
if (onPop != null)
onPop(this);
super.didPop(result);
}
Widget build(RouteArguments args) => null;
}
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'framework.dart';
import 'overlay.dart';
abstract class Route {
List<Widget> createWidgets() => const <Widget>[];
OverlayEntry get topEntry => _entries.isNotEmpty ? _entries.last : null;
OverlayEntry get bottomEntry => _entries.isNotEmpty ? _entries.first : null;
final List<OverlayEntry> _entries = new List<OverlayEntry>();
void didPush(OverlayState overlay, OverlayEntry insertionPoint) {
List<Widget> widgets = createWidgets();
for (Widget widget in widgets) {
_entries.add(new OverlayEntry(child: widget));
overlay?.insert(_entries.last, above: insertionPoint);
insertionPoint = _entries.last;
}
}
void didMakeCurrent() { }
void didPop(dynamic result) {
for (OverlayEntry entry in _entries)
entry.remove();
}
}
class NamedRouteSettings {
const NamedRouteSettings({ this.name: '<anonymous>', this.mostValuableKeys });
final String name;
final Set<Key> mostValuableKeys;
}
typedef Route RouteFactory(NamedRouteSettings settings);
class Navigator extends StatefulComponent {
Navigator({
Key key,
this.onGenerateRoute,
this.onUnknownRoute
}) : super(key: key) {
assert(onGenerateRoute != null);
}
final RouteFactory onGenerateRoute;
final RouteFactory onUnknownRoute;
static const String defaultRouteName = '/';
static NavigatorState of(BuildContext context) {
NavigatorState result;
context.visitAncestorElements((Element element) {
if (element is StatefulComponentElement && element.state is NavigatorState) {
result = element.state;
return false;
}
return true;
});
return result;
}
NavigatorState createState() => new NavigatorState();
}
class NavigatorState extends State<Navigator> {
final GlobalKey<OverlayState> _overlayKey = new GlobalKey<OverlayState>();
final List<Route> _ephemeral = new List<Route>();
final List<Route> _modal = new List<Route>();
void initState() {
super.initState();
push(config.onGenerateRoute(new NamedRouteSettings(name: Navigator.defaultRouteName)));
}
bool get hasPreviousRoute => _modal.length > 1;
OverlayState get overlay => _overlayKey.currentState;
OverlayEntry get _currentOverlay {
for (Route route in _ephemeral.reversed) {
if (route.topEntry != null)
return route.topEntry;
}
for (Route route in _modal.reversed) {
if (route.topEntry != null)
return route.topEntry;
}
return null;
}
Route get _currentRoute => _ephemeral.isNotEmpty ? _ephemeral.last : _modal.last;
Route _removeCurrentRoute() {
return _ephemeral.isNotEmpty ? _ephemeral.removeLast() : _modal.removeLast();
}
void pushNamed(String name, { Set<Key> mostValuableKeys }) {
NamedRouteSettings settings = new NamedRouteSettings(
name: name,
mostValuableKeys: mostValuableKeys
);
push(config.onGenerateRoute(settings) ?? config.onUnknownRoute(settings));
}
void push(Route route) {
_popAllEphemeralRoutes();
route.didPush(overlay, _currentOverlay);
_modal.add(route);
route.didMakeCurrent();
}
void pushEphemeral(Route route) {
route.didPush(overlay, _currentOverlay);
_ephemeral.add(route);
route.didMakeCurrent();
}
void _popAllEphemeralRoutes() {
List<Route> localEphemeral = new List<Route>.from(_ephemeral);
_ephemeral.clear();
for (Route route in localEphemeral)
route.didPop(null);
assert(_ephemeral.isEmpty);
}
void pop([dynamic result]) {
_removeCurrentRoute().didPop(result);
_currentRoute.didMakeCurrent();
}
Widget build(BuildContext context) {
return new Overlay(
key: _overlayKey,
initialEntries: _modal.first._entries
);
}
}
......@@ -16,7 +16,7 @@ class OverlayEntry {
bool get opaque => _opaque;
bool _opaque;
void set opaque(bool value) {
if (_opaque = value)
if (_opaque == value)
return;
_opaque = value;
_state?.setState(() {});
......
......@@ -6,59 +6,11 @@ import 'package:flutter/animation.dart';
import 'basic.dart';
import 'framework.dart';
import 'navigator2.dart';
import 'overlay.dart';
import 'navigator.dart';
import 'page_storage.dart';
import 'routes.dart';
import 'transitions.dart';
// TODO(abarth): Should we add a type for the result?
abstract class TransitionRoute extends Route {
bool get opaque => true;
PerformanceView get performance => _performance?.view;
Performance _performance;
Duration get transitionDuration;
Performance createPerformance() {
Duration duration = transitionDuration;
assert(duration != null && duration >= Duration.ZERO);
return new Performance(duration: duration, debugLabel: debugLabel);
}
dynamic _result;
void _handleStatusChanged(PerformanceStatus status) {
switch (status) {
case PerformanceStatus.completed:
bottomEntry.opaque = opaque;
break;
case PerformanceStatus.forward:
case PerformanceStatus.reverse:
bottomEntry.opaque = false;
break;
case PerformanceStatus.dismissed:
super.didPop(_result);
break;
}
}
void didPush(OverlayState overlay, OverlayEntry insertionPoint) {
_performance = createPerformance()
..addStatusListener(_handleStatusChanged)
..forward();
super.didPush(overlay, insertionPoint);
}
void didPop(dynamic result) {
_result = result;
_performance.reverse();
}
String get debugLabel => '$runtimeType';
String toString() => '$runtimeType(performance: $_performance)';
}
class _Page extends StatefulComponent {
_Page({
Key key,
......@@ -130,6 +82,8 @@ class PageRoute extends TransitionRoute {
final GlobalKey<_PageState> pageKey = new GlobalKey<_PageState>();
bool get opaque => true;
String get name => settings.name;
Duration get transitionDuration => const Duration(milliseconds: 150);
......
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/animation.dart';
import 'basic.dart';
import 'framework.dart';
import 'navigator.dart';
import 'overlay.dart';
class StateRoute extends Route {
StateRoute({ this.onPop });
final VoidCallback onPop;
List<OverlayEntry> get overlayEntries => const <OverlayEntry>[];
void didPush(OverlayState overlay, OverlayEntry insertionPoint) { }
void didMakeCurrent() { }
void didPop(dynamic result) {
if (onPop != null)
onPop();
}
}
class OverlayRoute extends Route {
List<Widget> createWidgets() => const <Widget>[];
List<OverlayEntry> get overlayEntries => _overlayEntries;
final List<OverlayEntry> _overlayEntries = new List<OverlayEntry>();
void didPush(OverlayState overlay, OverlayEntry insertionPoint) {
List<Widget> widgets = createWidgets();
for (Widget widget in widgets) {
_overlayEntries.add(new OverlayEntry(child: widget));
overlay?.insert(_overlayEntries.last, above: insertionPoint);
insertionPoint = _overlayEntries.last;
}
}
void didMakeCurrent() { }
void didPop(dynamic result) {
for (OverlayEntry entry in _overlayEntries)
entry.remove();
_overlayEntries.clear();
}
}
// TODO(abarth): Should we add a type for the result?
abstract class TransitionRoute extends OverlayRoute {
Duration get transitionDuration;
bool get opaque;
PerformanceView get performance => _performance?.view;
Performance _performance;
Performance createPerformance() {
Duration duration = transitionDuration;
assert(duration != null && duration >= Duration.ZERO);
return new Performance(duration: duration, debugLabel: debugLabel);
}
dynamic _result;
void _handleStatusChanged(PerformanceStatus status) {
switch (status) {
case PerformanceStatus.completed:
if (overlayEntries.isNotEmpty)
overlayEntries.first.opaque = opaque;
break;
case PerformanceStatus.forward:
case PerformanceStatus.reverse:
if (overlayEntries.isNotEmpty)
overlayEntries.first.opaque = false;
break;
case PerformanceStatus.dismissed:
super.didPop(_result);
break;
}
}
void didPush(OverlayState overlay, OverlayEntry insertionPoint) {
_performance = createPerformance()
..addStatusListener(_handleStatusChanged)
..forward();
super.didPush(overlay, insertionPoint);
}
void didPop(dynamic result) {
_result = result;
_performance.reverse();
}
String get debugLabel => '$runtimeType';
String toString() => '$runtimeType(performance: $_performance)';
}
......@@ -16,7 +16,7 @@ import 'framework.dart';
import 'gesture_detector.dart';
import 'homogeneous_viewport.dart';
import 'mixed_viewport.dart';
import 'navigator.dart';
import 'page_storage.dart';
// The gesture velocity properties are pixels/second, config min,max limits are pixels/ms
const double _kMillisecondsPerSecond = 1000.0;
......@@ -56,7 +56,7 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
void initState() {
super.initState();
_animation = new SimulationStepper(_setScrollOffset);
_scrollOffset = Route.of(context)?.readState(context) ?? config.initialScrollOffset ?? 0.0;
_scrollOffset = PageStorage.of(context)?.readState(context) ?? config.initialScrollOffset ?? 0.0;
}
SimulationStepper _animation;
......@@ -178,7 +178,7 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
setState(() {
_scrollOffset = newScrollOffset;
});
Route.of(context)?.writeState(context, _scrollOffset);
PageStorage.of(context)?.writeState(context, _scrollOffset);
dispatchOnScroll();
}
......
......@@ -16,13 +16,18 @@ export 'src/widgets/focus.dart';
export 'src/widgets/framework.dart';
export 'src/widgets/gesture_detector.dart';
export 'src/widgets/gridpaper.dart';
export 'src/widgets/hero_controller.dart';
export 'src/widgets/heroes.dart';
export 'src/widgets/homogeneous_viewport.dart';
export 'src/widgets/media_query.dart';
export 'src/widgets/mimic.dart';
export 'src/widgets/mixed_viewport.dart';
export 'src/widgets/modal_barrier.dart';
export 'src/widgets/navigator.dart';
export 'src/widgets/page_storage.dart';
export 'src/widgets/page.dart';
export 'src/widgets/placeholder.dart';
export 'src/widgets/routes.dart';
export 'src/widgets/scrollable.dart';
export 'src/widgets/statistics_overlay.dart';
export 'src/widgets/transitions.dart';
......
import 'package:flutter/widgets.dart';
import 'package:flutter/material.dart';
import 'package:test/test.dart';
import '../engine/mock_events.dart';
......@@ -11,7 +11,7 @@ void main() {
List accepted = [];
tester.pumpWidget(new Navigator(
tester.pumpWidget(new MaterialApp(
routes: <String, RouteBuilder>{
'/': (RouteArguments args) { return new Column(<Widget>[
new Draggable(
......
import 'package:flutter/widgets.dart';
import 'package:flutter/material.dart';
import 'package:test/test.dart';
import 'widget_tester.dart';
......@@ -45,7 +45,7 @@ void main() {
'/second': (RouteArguments args) => new SecondComponent(),
};
tester.pumpWidget(new Navigator(routes: routes));
tester.pumpWidget(new MaterialApp(routes: routes));
expect(tester.findText('X'), isNotNull);
expect(tester.findText('Y'), isNull);
......
......@@ -34,9 +34,12 @@ void main() {
GlobalKey<NavigatorState> navigatorKey = new GlobalKey<NavigatorState>();
tester.pumpWidget(new Navigator(
key: navigatorKey,
routes: <String, RouteBuilder>{
'/': (RouteArguments args) => new Container(child: new ThePositiveNumbers()),
'/second': (RouteArguments args) => new Container(child: new ThePositiveNumbers()),
onGenerateRoute: (NamedRouteSettings settings) {
if (settings.name == '/')
return new PageRoute(builder: (_) => new Container(child: new ThePositiveNumbers()));
else if (settings.name == '/second')
return new PageRoute(builder: (_) => new Container(child: new ThePositiveNumbers()));
return null;
}
));
......
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