From bbc17ac9c3561e71ff2b6c3e7da4e8cffb2d39b2 Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Wed, 28 Oct 2015 23:18:14 -0700 Subject: [PATCH] Add support for modal, ephemeral, and contentless routes to Navigator2 Routes can now create a list of widgets, which can be empty (in the case of contentless routes) or have multiple entries (in the case of modal routes). Ephemeral routes are handled by keeping a separate list of modal and ephemeral routes. --- .../sky/lib/src/material/material_app.dart | 13 +- .../sky/lib/src/widgets/navigator2.dart | 211 ++++++------------ sky/packages/sky/lib/src/widgets/page.dart | 112 ++++++++++ 3 files changed, 192 insertions(+), 144 deletions(-) create mode 100644 sky/packages/sky/lib/src/widgets/page.dart diff --git a/sky/packages/sky/lib/src/material/material_app.dart b/sky/packages/sky/lib/src/material/material_app.dart index dc7bfb3f3a6..81df08aa24a 100644 --- a/sky/packages/sky/lib/src/material/material_app.dart +++ b/sky/packages/sky/lib/src/material/material_app.dart @@ -9,6 +9,7 @@ import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter/src/widgets/navigator2.dart' as n2; +import 'package:flutter/src/widgets/page.dart' as n2; import 'theme.dart'; import 'title.dart'; @@ -86,12 +87,22 @@ class _MaterialAppState extends State { void _metricHandler(Size size) => setState(() { _size = size; }); + n2.Route _generateRoute(n2.RouteArguments args) { + return new n2.PageRoute( + builder: (BuildContext context) { + RouteArguments routeArgs = new RouteArguments(context: context); + return config.routes[args.name](routeArgs); + }, + args: args + ); + } + Widget build(BuildContext context) { Widget navigator; if (_kUseNavigator2) { navigator = new n2.Navigator( key: _navigator, - routes: config.routes + onGenerateRoute: _generateRoute ); } else { navigator = new Navigator( diff --git a/sky/packages/sky/lib/src/widgets/navigator2.dart b/sky/packages/sky/lib/src/widgets/navigator2.dart index 6370862d496..debd883bf79 100644 --- a/sky/packages/sky/lib/src/widgets/navigator2.dart +++ b/sky/packages/sky/lib/src/widgets/navigator2.dart @@ -2,55 +2,54 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; - -import 'package:flutter/animation.dart'; - -import 'basic.dart'; import 'framework.dart'; import 'overlay.dart'; -import 'transitions.dart'; abstract class Route { - /// Override this function to return the widget that this route should display. - Widget createWidget(); + List createWidgets() => const []; - Widget _child; - OverlayEntry _entry; + OverlayEntry get topEntry => _entries.isNotEmpty ? _entries.last : null; + OverlayEntry get bottomEntry => _entries.isNotEmpty ? _entries.first : null; - void willPush() { - _child = createWidget(); + final List _entries = new List(); + + void add(OverlayState overlay, OverlayEntry insertionPoint) { + List widgets = createWidgets(); + for (Widget widget in widgets) { + _entries.add(new OverlayEntry(child: widget)); + overlay?.insert(_entries.last, above: insertionPoint); + insertionPoint = _entries.last; + } } - void didPop(dynamic result) { - _entry.remove(); + void remove(dynamic result) { + for (OverlayEntry entry in _entries) + entry.remove(); } } -typedef Widget RouteBuilder(args); -typedef RouteBuilder RouteGenerator(String name); +class RouteArguments { + const RouteArguments({ this.name: '', this.mostValuableKeys }); -const String _kDefaultPageName = '/'; + final String name; + final Set mostValuableKeys; +} + +typedef Route RouteFactory(RouteArguments args); class Navigator extends StatefulComponent { Navigator({ Key key, - this.routes, - this.onGeneratePage, - this.onUnknownPage + 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(_kDefaultPageName)); + assert(onGenerateRoute != null); } - final Map routes; + final RouteFactory onGenerateRoute; + final RouteFactory onUnknownRoute; - /// you need to implement this if you pushNamed() to names that might not be in routes. - final RouteGenerator onGeneratePage; - - /// 404 generator. You only need to implement this if you have a way to navigate to arbitrary names. - final RouteBuilder onUnknownPage; + static const String defaultRouteName = '/'; static NavigatorState of(BuildContext context) { NavigatorState result; @@ -68,139 +67,65 @@ class Navigator extends StatefulComponent { } class NavigatorState extends State { - GlobalKey _overlay = new GlobalKey(); - List _history = new List(); + final GlobalKey _overlay = new GlobalKey(); + final List _ephemeral = new List(); + final List _modal = new List(); void initState() { super.initState(); - _addRouteToHistory(new PageRoute( - builder: config.routes[_kDefaultPageName], - name: _kDefaultPageName - )); + push(config.onGenerateRoute(new RouteArguments(name: Navigator.defaultRouteName))); } - RouteBuilder _generatePage(String name) { - assert(config.onGeneratePage != null); - return config.onGeneratePage(name); - } + bool get hasPreviousRoute => _modal.length > 1; - bool get hasPreviousRoute => _history.length > 1; + OverlayEntry get _topRouteOverlay { + 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; + } void pushNamed(String name, { Set mostValuableKeys }) { - final RouteBuilder builder = config.routes[name] ?? _generatePage(name) ?? config.onUnknownPage; - assert(builder != null); // 404 getting your 404! - push(new PageRoute( - builder: builder, - name: name, - mostValuableKeys: mostValuableKeys - )); - } - - void _addRouteToHistory(Route route) { - route.willPush(); - route._entry = new OverlayEntry(child: route._child); - _history.add(route); + RouteArguments args = new RouteArguments(name: name, mostValuableKeys: mostValuableKeys); + push(config.onGenerateRoute(args) ?? config.onUnknownRoute(args)); } void push(Route route) { - OverlayEntry reference = _history.last._entry; - _addRouteToHistory(route); - _overlay.currentState.insert(route._entry, above: reference); + _popAllEphemeralRoutes(); + route.add(_overlay.currentState, _topRouteOverlay); + _modal.add(route); + } + + void pushEphemeral(Route route) { + route.add(_overlay.currentState, _topRouteOverlay); + _ephemeral.add(route); + } + + void _popAllEphemeralRoutes() { + List localEphemeral = new List.from(_ephemeral); + _ephemeral.clear(); + for (Route route in localEphemeral) + route.remove(null); + assert(_ephemeral.isEmpty); } void pop([dynamic result]) { - _history.removeLast().didPop(result); + if (_ephemeral.isNotEmpty) { + _ephemeral.removeLast().remove(result); + return; + } + _modal.removeLast().remove(result); } Widget build(BuildContext context) { return new Overlay( key: _overlay, - initialEntries: [ _history.first._entry ] + initialEntries: _modal.first._entries ); } } - -abstract class TransitionRoute extends Route { - 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); - } - - void willPush() { - _performance = createPerformance(); - _performance.forward(); - super.willPush(); - } - - Future didPop(dynamic result) async { - await _performance.reverse(); - super.didPop(result); - } - - String get debugLabel => '$runtimeType'; - String toString() => '$runtimeType(performance: $_performance)'; -} - -class _Page extends StatefulComponent { - _Page({ Key key, this.route }) : super(key: key); - - final PageRoute route; - - _PageState createState() => new _PageState(); -} - -class _PageState extends State<_Page> { - final AnimatedValue _position = - new AnimatedValue(const Point(0.0, 75.0), end: Point.origin, curve: Curves.easeOut); - - final AnimatedValue _opacity = - new AnimatedValue(0.0, end: 1.0, curve: Curves.easeOut); - - Widget build(BuildContext context) { - return new SlideTransition( - performance: config.route.performance, - position: _position, - child: new FadeTransition( - performance: config.route.performance, - opacity: _opacity, - child: _invokeBuilder() - ) - ); - } - - Widget _invokeBuilder() { - Widget result = config.route.builder(null); - assert(() { - if (result == null) - debugPrint('The builder for route \'${config.route.name}\' returned null. Route builders must never return null.'); - assert(result != null && 'A route builder returned null. See the previous log message for details.' is String); - return true; - }); - return result; - } -} - -class PageRoute extends TransitionRoute { - PageRoute({ - this.builder, - this.name: '', - this.mostValuableKeys - }) { - assert(builder != null); - } - - final RouteBuilder builder; - final String name; - final Set mostValuableKeys; - - Duration get transitionDuration => const Duration(milliseconds: 150); - Widget createWidget() => new _Page(route: this); - - String get debugLabel => '${super.debugLabel}($name)'; -} diff --git a/sky/packages/sky/lib/src/widgets/page.dart b/sky/packages/sky/lib/src/widgets/page.dart new file mode 100644 index 00000000000..64ca2fdda5e --- /dev/null +++ b/sky/packages/sky/lib/src/widgets/page.dart @@ -0,0 +1,112 @@ +// 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 'navigator2.dart'; +import 'overlay.dart'; +import 'transitions.dart'; + +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) { + if (status == PerformanceStatus.completed && opaque) { + bottomEntry.opaque = true; + } else if (status == PerformanceStatus.dismissed) { + super.remove(_result); + } + } + + void add(OverlayState overlayer, OverlayEntry insertionPoint) { + _performance = createPerformance() + ..addStatusListener(_handleStatusChanged) + ..forward(); + super.add(overlayer, insertionPoint); + } + + void remove(dynamic result) { + _result = result; + _performance.reverse(); + } + + String get debugLabel => '$runtimeType'; + String toString() => '$runtimeType(performance: $_performance)'; +} + +class _Page extends StatefulComponent { + _Page({ + PageRoute route + }) : route = route, super(key: new GlobalObjectKey(route)); + + final PageRoute route; + + _PageState createState() => new _PageState(); +} + +class _PageState extends State<_Page> { + final AnimatedValue _position = + new AnimatedValue(const Point(0.0, 75.0), end: Point.origin, curve: Curves.easeOut); + + final AnimatedValue _opacity = + new AnimatedValue(0.0, end: 1.0, curve: Curves.easeOut); + + Widget build(BuildContext context) { + return new SlideTransition( + performance: config.route.performance, + position: _position, + child: new FadeTransition( + performance: config.route.performance, + opacity: _opacity, + child: _invokeBuilder() + ) + ); + } + + Widget _invokeBuilder() { + Widget result = config.route.builder(context); + assert(() { + if (result == null) + debugPrint('The builder for route \'${config.route.name}\' returned null. Route builders must never return null.'); + assert(result != null && 'A route builder returned null. See the previous log message for details.' is String); + return true; + }); + return result; + } +} + +class PageRoute extends TransitionRoute { + PageRoute({ + this.builder, + this.args: const RouteArguments() + }) { + assert(builder != null); + assert(opaque); + } + + final WidgetBuilder builder; + final RouteArguments args; + + String get name => args.name; + + Duration get transitionDuration => const Duration(milliseconds: 150); + List createWidgets() => [ new _Page(route: this) ]; + + String get debugLabel => '${super.debugLabel}($name)'; +}