forked from firka/flutter
@@ -24,22 +24,20 @@ class StockHomeState extends State<StockHome> {
|
||||
String _searchQuery;
|
||||
|
||||
void _handleSearchBegin() {
|
||||
Navigator.of(context).pushState(this, (_) {
|
||||
setState(() {
|
||||
_isSearching = false;
|
||||
_searchQuery = null;
|
||||
});
|
||||
});
|
||||
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,47 +49,62 @@ 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,
|
||||
variables: <AnimatedValue<double>>[_layout.childTop],
|
||||
builder: (BuildContext context) {
|
||||
return new ClipRect(
|
||||
child: new CustomOneChildLayout(
|
||||
delegate: _layout,
|
||||
token: _layout.childTop.value,
|
||||
child: new GestureDetector(
|
||||
onVerticalDragStart: _handleDragStart,
|
||||
onVerticalDragUpdate: _handleDragUpdate,
|
||||
onVerticalDragEnd: _handleDragEnd,
|
||||
child: new Material(child: config.child)
|
||||
)
|
||||
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(
|
||||
child: new CustomOneChildLayout(
|
||||
delegate: _layout,
|
||||
token: _layout.childTop.value,
|
||||
child: new GestureDetector(
|
||||
onVerticalDragStart: _handleDragStart,
|
||||
onVerticalDragUpdate: _handleDragUpdate,
|
||||
onVerticalDragEnd: _handleDragEnd,
|
||||
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;
|
||||
|
||||
Performance createPerformance() {
|
||||
_performance = super.createPerformance();
|
||||
return _performance;
|
||||
}
|
||||
|
||||
void didPush(NavigatorState navigator) {
|
||||
super.didPush(navigator);
|
||||
_performance?.forward();
|
||||
}
|
||||
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,
|
||||
opacity: new AnimatedValue<double>(0.0, end: 1.0, curve: Curves.easeOut),
|
||||
child: builder(args)
|
||||
);
|
||||
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: 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(
|
||||
onHorizontalDragStart: (_) {
|
||||
if (interactive)
|
||||
route._takeControl();
|
||||
},
|
||||
onHorizontalDragUpdate: (double delta) {
|
||||
if (interactive)
|
||||
route._moveDrawer(delta);
|
||||
},
|
||||
onHorizontalDragEnd: (Offset velocity) {
|
||||
if (interactive)
|
||||
route._settle(velocity);
|
||||
},
|
||||
child: new Stack(<Widget>[
|
||||
// mask
|
||||
new GestureDetector(
|
||||
onTap: () {
|
||||
if (interactive)
|
||||
route._close();
|
||||
},
|
||||
child: new ColorTransition(
|
||||
performance: 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,
|
||||
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]),
|
||||
width: _kWidth,
|
||||
child: child
|
||||
return new Focus(
|
||||
key: new GlobalObjectKey(route),
|
||||
autofocus: true,
|
||||
child: new GestureDetector(
|
||||
onHorizontalDragStart: (_) {
|
||||
if (route.interactive)
|
||||
route._takeControl();
|
||||
},
|
||||
onHorizontalDragUpdate: (double delta) {
|
||||
if (route.interactive)
|
||||
route._moveDrawer(delta);
|
||||
},
|
||||
onHorizontalDragEnd: (Offset velocity) {
|
||||
if (route.interactive)
|
||||
route._settle(velocity);
|
||||
},
|
||||
child: new Stack(<Widget>[
|
||||
// mask
|
||||
new GestureDetector(
|
||||
onTap: () {
|
||||
if (route.interactive)
|
||||
route._close();
|
||||
},
|
||||
child: new ColorTransition(
|
||||
performance: route.performance,
|
||||
color: new AnimatedColorValue(Colors.transparent, end: Colors.black54),
|
||||
child: new Container()
|
||||
)
|
||||
),
|
||||
new Positioned(
|
||||
top: 0.0,
|
||||
left: 0.0,
|
||||
bottom: 0.0,
|
||||
child: new SlideTransition(
|
||||
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[route.level]),
|
||||
width: _kWidth,
|
||||
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;
|
||||
|
||||
Performance createPerformance() {
|
||||
_performance = super.createPerformance();
|
||||
return _performance;
|
||||
}
|
||||
|
||||
void didPush(NavigatorState navigator) {
|
||||
super.didPush(navigator);
|
||||
_performance.forward();
|
||||
}
|
||||
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
|
||||
});
|
||||
_interactive = false;
|
||||
}
|
||||
|
||||
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,32 +79,45 @@ 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,
|
||||
opacity: menuOpacity,
|
||||
child: new BuilderTransition(
|
||||
performance: performance,
|
||||
variables: <AnimatedValue<double>>[menuTop, menuBottom],
|
||||
builder: (BuildContext context) {
|
||||
RenderBox renderBox = context.findRenderObject();
|
||||
return new CustomPaint(
|
||||
child: new ScrollableViewport(child: new Container(child: new Column(children))),
|
||||
onPaint: (ui.Canvas canvas, Size size) {
|
||||
double top = renderBox.globalToLocal(new Point(0.0, menuTop.value)).y;
|
||||
double bottom = renderBox.globalToLocal(new Point(0.0, menuBottom.value)).y;
|
||||
menuPainter.paint(canvas, new Rect.fromLTRB(0.0, top, size.width, bottom));
|
||||
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: route.performance,
|
||||
variables: <AnimatedValue<double>>[menuTop, menuBottom],
|
||||
builder: (BuildContext context) {
|
||||
RenderBox renderBox = context.findRenderObject();
|
||||
return new CustomPaint(
|
||||
child: new ScrollableViewport(child: new Container(child: new Column(children))),
|
||||
onPaint: (ui.Canvas canvas, Size size) {
|
||||
double top = renderBox.globalToLocal(new Point(0.0, menuTop.value)).y;
|
||||
double bottom = renderBox.globalToLocal(new Point(0.0, menuBottom.value)).y;
|
||||
menuPainter.paint(canvas, new Rect.fromLTRB(0.0, top, size.width, bottom));
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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,80 +22,84 @@ 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,
|
||||
opacity: new AnimatedValue<double>(0.0, end: 1.0, curve: new Interval(0.0, 1.0 / 3.0)),
|
||||
child: new BuilderTransition(
|
||||
performance: performance,
|
||||
variables: <AnimatedValue<double>>[width, height],
|
||||
builder: (BuildContext context) {
|
||||
return new CustomPaint(
|
||||
onPaint: (ui.Canvas canvas, Size size) {
|
||||
double widthValue = width.value * size.width;
|
||||
double heightValue = height.value * size.height;
|
||||
painter.paint(canvas, new Rect.fromLTWH(size.width - widthValue, 0.0, widthValue, heightValue));
|
||||
},
|
||||
child: new ConstrainedBox(
|
||||
constraints: new BoxConstraints(
|
||||
minWidth: _kMenuMinWidth,
|
||||
maxWidth: _kMenuMaxWidth
|
||||
),
|
||||
child: new IntrinsicWidth(
|
||||
stepWidth: _kMenuWidthStep,
|
||||
child: new ScrollableViewport(
|
||||
child: new Container(
|
||||
padding: const EdgeDims.symmetric(
|
||||
horizontal: _kMenuHorizontalPadding,
|
||||
vertical: _kMenuVerticalPadding
|
||||
),
|
||||
child: new BlockBody(children)
|
||||
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: route.performance,
|
||||
variables: <AnimatedValue<double>>[width, height],
|
||||
builder: (BuildContext context) {
|
||||
return new CustomPaint(
|
||||
onPaint: (ui.Canvas canvas, Size size) {
|
||||
double widthValue = width.value * size.width;
|
||||
double heightValue = height.value * size.height;
|
||||
painter.paint(canvas, new Rect.fromLTWH(size.width - widthValue, 0.0, widthValue, heightValue));
|
||||
},
|
||||
child: new ConstrainedBox(
|
||||
constraints: new BoxConstraints(
|
||||
minWidth: _kMenuMinWidth,
|
||||
maxWidth: _kMenuMaxWidth
|
||||
),
|
||||
child: new IntrinsicWidth(
|
||||
stepWidth: _kMenuWidthStep,
|
||||
child: new ScrollableViewport(
|
||||
child: new Container(
|
||||
// TODO(abarth): Teach Block about padding.
|
||||
padding: const EdgeDims.symmetric(
|
||||
horizontal: _kMenuHorizontalPadding,
|
||||
vertical: _kMenuVerticalPadding
|
||||
),
|
||||
child: new BlockBody(children)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
20
packages/flutter/lib/src/widgets/modal_barrier.dart
Normal file
20
packages/flutter/lib/src/widgets/modal_barrier.dart
Normal file
@@ -0,0 +1,20 @@
|
||||
// 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> {
|
||||
|
||||
List<Route> _history = new List<Route>();
|
||||
int _currentPosition = 0; // which route is "current"
|
||||
|
||||
Route get currentRoute => _history[_currentPosition];
|
||||
bool get hasPreviousRoute => _history.length > 1;
|
||||
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();
|
||||
_activeHeroes = new HeroParty(onQuestFinished: _handleHeroQuestFinished);
|
||||
PageRoute route = new PageRoute(config.routes[kDefaultRouteName], name: kDefaultRouteName);
|
||||
assert(route.hasContent);
|
||||
assert(!route.ephemeral);
|
||||
_insertRoute(route);
|
||||
push(config.onGenerateRoute(new NamedRouteSettings(name: Navigator.defaultRouteName)));
|
||||
}
|
||||
|
||||
void pushState(State owner, StateRouteCallback onPop) {
|
||||
push(new StateRoute(
|
||||
route: currentRoute,
|
||||
owner: owner,
|
||||
onPop: onPop
|
||||
));
|
||||
bool get hasPreviousRoute => _modal.length > 1;
|
||||
OverlayState get overlay => _overlayKey.currentState;
|
||||
|
||||
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;
|
||||
}
|
||||
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 }) {
|
||||
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;
|
||||
NamedRouteSettings settings = new NamedRouteSettings(
|
||||
name: name,
|
||||
mostValuableKeys: mostValuableKeys
|
||||
);
|
||||
push(config.onGenerateRoute(settings) ?? config.onUnknownRoute(settings));
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
_popAllEphemeralRoutes();
|
||||
route.didPush(overlay, _currentOverlay);
|
||||
_modal.add(route);
|
||||
route.didMakeCurrent();
|
||||
}
|
||||
|
||||
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 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]) {
|
||||
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;
|
||||
});
|
||||
_removeCurrentRoute().didPop(result);
|
||||
currentRoute.didMakeCurrent();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
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;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
)));
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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
|
||||
return new Overlay(
|
||||
key: _overlayKey,
|
||||
initialEntries: _modal.first.overlayEntries
|
||||
);
|
||||
}
|
||||
|
||||
/// 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))
|
||||
)
|
||||
);
|
||||
}
|
||||
return super._internalBuild(context, nextRoute, buildTargetHeroes: buildTargetHeroes);
|
||||
}
|
||||
|
||||
void didPush(NavigatorState navigator) {
|
||||
super.didPush(navigator);
|
||||
_performance?.forward();
|
||||
}
|
||||
|
||||
void didPop([dynamic result]) {
|
||||
_performance?.reverse();
|
||||
super.didPop(result);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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 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;
|
||||
}
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
// 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);
|
||||
|
||||
98
packages/flutter/lib/src/widgets/routes.dart
Normal file
98
packages/flutter/lib/src/widgets/routes.dart
Normal file
@@ -0,0 +1,98 @@
|
||||
// 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';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
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(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
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;
|
||||
}
|
||||
));
|
||||
|
||||
@@ -105,7 +108,7 @@ void main() {
|
||||
expect(tester.findText('15'), isNotNull);
|
||||
expect(tester.findText('16'), isNull);
|
||||
expect(tester.findText('100'), isNull);
|
||||
|
||||
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user