Commit 9548634b authored by Adam Barth's avatar Adam Barth

Merge pull request #1866 from abarth/route_types

Add support for modal, ephemeral, and contentless routes to Navigator2
parents 0a2bfc31 395184f7
...@@ -31,7 +31,6 @@ class Scheduler { ...@@ -31,7 +31,6 @@ class Scheduler {
} }
bool _haveScheduledVisualUpdate = false; bool _haveScheduledVisualUpdate = false;
int _nextPrivateCallbackId = 0; // negative
int _nextCallbackId = 0; // positive int _nextCallbackId = 0; // positive
final List<SchedulerCallback> _persistentCallbacks = new List<SchedulerCallback>(); final List<SchedulerCallback> _persistentCallbacks = new List<SchedulerCallback>();
...@@ -55,7 +54,6 @@ class Scheduler { ...@@ -55,7 +54,6 @@ class Scheduler {
Duration timeStamp = new Duration( Duration timeStamp = new Duration(
microseconds: (rawTimeStamp.inMicroseconds / timeDilation).round()); microseconds: (rawTimeStamp.inMicroseconds / timeDilation).round());
_haveScheduledVisualUpdate = false; _haveScheduledVisualUpdate = false;
assert(_postFrameCallbacks.length == 0);
Map<int, SchedulerCallback> callbacks = _transientCallbacks; Map<int, SchedulerCallback> callbacks = _transientCallbacks;
_transientCallbacks = new Map<int, SchedulerCallback>(); _transientCallbacks = new Map<int, SchedulerCallback>();
...@@ -68,9 +66,11 @@ class Scheduler { ...@@ -68,9 +66,11 @@ class Scheduler {
for (SchedulerCallback callback in _persistentCallbacks) for (SchedulerCallback callback in _persistentCallbacks)
invokeCallback(callback, timeStamp); invokeCallback(callback, timeStamp);
for (SchedulerCallback callback in _postFrameCallbacks) List<SchedulerCallback> localPostFrameCallbacks =
invokeCallback(callback, timeStamp); new List<SchedulerCallback>.from(_postFrameCallbacks);
_postFrameCallbacks.clear(); _postFrameCallbacks.clear();
for (SchedulerCallback callback in localPostFrameCallbacks)
invokeCallback(callback, timeStamp);
_inFrame = false; _inFrame = false;
} }
...@@ -133,13 +133,7 @@ class Scheduler { ...@@ -133,13 +133,7 @@ class Scheduler {
/// frame. In this case, the registration order is not preserved. Callbacks /// frame. In this case, the registration order is not preserved. Callbacks
/// are called in an arbitrary order. /// are called in an arbitrary order.
void requestPostFrameCallback(SchedulerCallback callback) { void requestPostFrameCallback(SchedulerCallback callback) {
if (_inFrame) {
_postFrameCallbacks.add(callback); _postFrameCallbacks.add(callback);
} else {
_nextPrivateCallbackId -= 1;
_transientCallbacks[_nextPrivateCallbackId] = callback;
ensureVisualUpdate();
}
} }
/// Ensure that a frame will be produced after this function is called. /// Ensure that a frame will be produced after this function is called.
......
...@@ -9,6 +9,7 @@ import 'package:flutter/services.dart'; ...@@ -9,6 +9,7 @@ import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter/src/widgets/navigator2.dart' as n2; import 'package:flutter/src/widgets/navigator2.dart' as n2;
import 'package:flutter/src/widgets/hero_controller.dart' as n2;
import 'theme.dart'; import 'theme.dart';
import 'title.dart'; import 'title.dart';
...@@ -86,12 +87,25 @@ class _MaterialAppState extends State<MaterialApp> { ...@@ -86,12 +87,25 @@ class _MaterialAppState extends State<MaterialApp> {
void _metricHandler(Size size) => setState(() { _size = size; }); void _metricHandler(Size size) => setState(() { _size = size; });
final n2.HeroController _heroController = new n2.HeroController();
n2.Route _generateRoute(n2.NamedRouteSettings settings) {
return new n2.HeroPageRoute(
builder: (BuildContext context) {
RouteBuilder builder = config.routes[settings.name] ?? config.onGenerateRoute(settings.name);
return builder(new RouteArguments(context: context));
},
settings: settings,
heroController: _heroController
);
}
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget navigator; Widget navigator;
if (_kUseNavigator2) { if (_kUseNavigator2) {
navigator = new n2.Navigator( navigator = new n2.Navigator(
key: _navigator, key: _navigator,
routes: config.routes onGenerateRoute: _generateRoute
); );
} else { } else {
navigator = new Navigator( navigator = new Navigator(
......
// 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 'package:flutter/rendering.dart';
import 'basic.dart';
import 'framework.dart';
import 'heroes.dart';
import 'navigator2.dart';
import 'overlay.dart';
import 'page.dart';
class HeroPageRoute extends PageRoute {
HeroPageRoute({
WidgetBuilder builder,
NamedRouteSettings settings: const NamedRouteSettings(),
this.heroController
}) : super(builder: builder, settings: settings);
final HeroController heroController;
void didMakeCurrent() {
heroController?.didMakeCurrent(this);
}
}
class HeroController {
HeroController() {
_party = new HeroParty(onQuestFinished: _handleQuestFinished);
}
HeroParty _party;
HeroPageRoute _from;
HeroPageRoute _to;
final List<OverlayEntry> _overlayEntries = new List<OverlayEntry>();
void didMakeCurrent(PageRoute current) {
assert(current != null);
assert(current.performance != null);
if (_from == null) {
_from = current;
return;
}
_to = current;
current.offstage = true;
scheduler.requestPostFrameCallback(_updateQuest);
}
void _handleQuestFinished() {
_removeHeroesFromOverlay();
_from = _to;
_to = null;
}
Rect _getAnimationArea(BuildContext context) {
RenderBox box = context.findRenderObject();
Point topLeft = box.localToGlobal(Point.origin);
Point bottomRight = box.localToGlobal(box.size.bottomRight(Point.origin));
return new Rect.fromLTRB(topLeft.x, topLeft.y, bottomRight.x, bottomRight.y);
}
void _removeHeroesFromOverlay() {
for (OverlayEntry entry in _overlayEntries)
entry.remove();
_overlayEntries.clear();
}
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);
_overlayEntries.add(entry);
}
}
Set<Key> _getMostValuableKeys() {
Set<Key> result = new Set<Key>();
if (_from.settings.mostValuableKeys != null)
result.addAll(_from.settings.mostValuableKeys);
if (_to.settings.mostValuableKeys != null)
result.addAll(_to.settings.mostValuableKeys);
return result;
}
void _updateQuest(Duration timeStamp) {
Set<Key> mostValuableKeys = _getMostValuableKeys();
Map<Object, HeroHandle> heroesFrom = _party.isEmpty ?
Hero.of(_from.pageKey.currentContext, mostValuableKeys) : _party.getHeroesToAnimate();
BuildContext context = _to.pageKey.currentContext;
Map<Object, HeroHandle> heroesTo = Hero.of(context, mostValuableKeys);
_to.offstage = false;
PerformanceView performance = _to.performance;
Curve curve = Curves.ease;
if (performance.status == PerformanceStatus.reverse) {
performance = new ReversePerformance(performance);
curve = new Interval(performance.progress, 1.0, curve: curve);
}
NavigatorState navigator = Navigator.of(context);
_party.animate(heroesFrom, heroesTo, _getAnimationArea(navigator.context), curve);
_removeHeroesFromOverlay();
Iterable<Widget> heroes = _party.getWidgets(navigator.context, performance);
_addHeroesToOverlay(heroes, navigator.overlay);
}
}
...@@ -387,11 +387,11 @@ class HeroParty { ...@@ -387,11 +387,11 @@ class HeroParty {
hero.targetState._setChild(hero.key); hero.targetState._setChild(hero.key);
for (HeroState source in hero.sourceStates) for (HeroState source in hero.sourceStates)
source._resetChild(); source._resetChild();
if (onQuestFinished != null)
onQuestFinished();
} }
_heroes.clear(); _heroes.clear();
_currentPerformance = null; _currentPerformance = null;
if (onQuestFinished != null)
onQuestFinished();
} }
} }
......
...@@ -2,55 +2,56 @@ ...@@ -2,55 +2,56 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async';
import 'package:flutter/animation.dart';
import 'basic.dart';
import 'framework.dart'; import 'framework.dart';
import 'overlay.dart'; import 'overlay.dart';
import 'transitions.dart';
abstract class Route { abstract class Route {
/// Override this function to return the widget that this route should display. List<Widget> createWidgets() => const <Widget>[];
Widget createWidget();
Widget _child; OverlayEntry get topEntry => _entries.isNotEmpty ? _entries.last : null;
OverlayEntry _entry; OverlayEntry get bottomEntry => _entries.isNotEmpty ? _entries.first : null;
void willPush() { final List<OverlayEntry> _entries = new List<OverlayEntry>();
_child = createWidget();
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) { void didPop(dynamic result) {
_entry.remove(); for (OverlayEntry entry in _entries)
entry.remove();
} }
} }
typedef Widget RouteBuilder(args); class NamedRouteSettings {
typedef RouteBuilder RouteGenerator(String name); const NamedRouteSettings({ this.name: '<anonymous>', this.mostValuableKeys });
const String _kDefaultPageName = '/'; final String name;
final Set<Key> mostValuableKeys;
}
typedef Route RouteFactory(NamedRouteSettings settings);
class Navigator extends StatefulComponent { class Navigator extends StatefulComponent {
Navigator({ Navigator({
Key key, Key key,
this.routes, this.onGenerateRoute,
this.onGeneratePage, this.onUnknownRoute
this.onUnknownPage
}) : super(key: key) { }) : super(key: key) {
// To use a navigator, you must at a minimum define the route with the name '/'. assert(onGenerateRoute != null);
assert(routes != null);
assert(routes.containsKey(_kDefaultPageName));
} }
final Map<String, RouteBuilder> 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. static const String defaultRouteName = '/';
final RouteBuilder onUnknownPage;
static NavigatorState of(BuildContext context) { static NavigatorState of(BuildContext context) {
NavigatorState result; NavigatorState result;
...@@ -68,139 +69,74 @@ class Navigator extends StatefulComponent { ...@@ -68,139 +69,74 @@ class Navigator extends StatefulComponent {
} }
class NavigatorState extends State<Navigator> { class NavigatorState extends State<Navigator> {
GlobalKey<OverlayState> _overlay = new GlobalKey<OverlayState>(); final GlobalKey<OverlayState> _overlayKey = new GlobalKey<OverlayState>();
List<Route> _history = new List<Route>(); final List<Route> _ephemeral = new List<Route>();
final List<Route> _modal = new List<Route>();
void initState() { void initState() {
super.initState(); super.initState();
_addRouteToHistory(new PageRoute( push(config.onGenerateRoute(new NamedRouteSettings(name: Navigator.defaultRouteName)));
builder: config.routes[_kDefaultPageName],
name: _kDefaultPageName
));
} }
RouteBuilder _generatePage(String name) { bool get hasPreviousRoute => _modal.length > 1;
assert(config.onGeneratePage != null); OverlayState get overlay => _overlayKey.currentState;
return config.onGeneratePage(name);
}
bool get hasPreviousRoute => _history.length > 1;
void pushNamed(String name, { Set<Key> mostValuableKeys }) { OverlayEntry get _currentOverlay {
final RouteBuilder builder = config.routes[name] ?? _generatePage(name) ?? config.onUnknownPage; for (Route route in _ephemeral.reversed) {
assert(builder != null); // 404 getting your 404! if (route.topEntry != null)
push(new PageRoute( return route.topEntry;
builder: builder,
name: name,
mostValuableKeys: mostValuableKeys
));
} }
for (Route route in _modal.reversed) {
void _addRouteToHistory(Route route) { if (route.topEntry != null)
route.willPush(); return route.topEntry;
route._entry = new OverlayEntry(child: route._child);
_history.add(route);
} }
return null;
void push(Route route) {
OverlayEntry reference = _history.last._entry;
_addRouteToHistory(route);
_overlay.currentState.insert(route._entry, above: reference);
} }
void pop([dynamic result]) { Route get _currentRoute => _ephemeral.isNotEmpty ? _ephemeral.last : _modal.last;
_history.removeLast().didPop(result);
Route _removeCurrentRoute() {
return _ephemeral.isNotEmpty ? _ephemeral.removeLast() : _modal.removeLast();
} }
Widget build(BuildContext context) { void pushNamed(String name, { Set<Key> mostValuableKeys }) {
return new Overlay( NamedRouteSettings settings = new NamedRouteSettings(
key: _overlay, name: name,
initialEntries: <OverlayEntry>[ _history.first._entry ] mostValuableKeys: mostValuableKeys
); );
push(config.onGenerateRoute(settings) ?? config.onUnknownRoute(settings));
} }
}
abstract class TransitionRoute extends Route { void push(Route route) {
PerformanceView get performance => _performance?.view; _popAllEphemeralRoutes();
Performance _performance; route.didPush(overlay, _currentOverlay);
_modal.add(route);
Duration get transitionDuration; route.didMakeCurrent();
Performance createPerformance() {
Duration duration = transitionDuration;
assert(duration != null && duration >= Duration.ZERO);
return new Performance(duration: duration, debugLabel: debugLabel);
} }
void willPush() { void pushEphemeral(Route route) {
_performance = createPerformance(); route.didPush(overlay, _currentOverlay);
_performance.forward(); _ephemeral.add(route);
super.willPush(); route.didMakeCurrent();
} }
Future didPop(dynamic result) async { void _popAllEphemeralRoutes() {
await _performance.reverse(); List<Route> localEphemeral = new List<Route>.from(_ephemeral);
super.didPop(result); _ephemeral.clear();
for (Route route in localEphemeral)
route.didPop(null);
assert(_ephemeral.isEmpty);
} }
String get debugLabel => '$runtimeType'; void pop([dynamic result]) {
String toString() => '$runtimeType(performance: $_performance)'; _removeCurrentRoute().didPop(result);
} _currentRoute.didMakeCurrent();
}
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<Point> _position =
new AnimatedValue<Point>(const Point(0.0, 75.0), end: Point.origin, curve: Curves.easeOut);
final AnimatedValue<double> _opacity =
new AnimatedValue<double>(0.0, end: 1.0, curve: Curves.easeOut);
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new SlideTransition( return new Overlay(
performance: config.route.performance, key: _overlayKey,
position: _position, initialEntries: _modal.first._entries
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: '<anonymous>',
this.mostValuableKeys
}) {
assert(builder != null);
}
final RouteBuilder builder;
final String name;
final Set<Key> mostValuableKeys;
Duration get transitionDuration => const Duration(milliseconds: 150);
Widget createWidget() => new _Page(route: this);
String get debugLabel => '${super.debugLabel}($name)';
} }
...@@ -16,6 +16,8 @@ class OverlayEntry { ...@@ -16,6 +16,8 @@ class OverlayEntry {
bool get opaque => _opaque; bool get opaque => _opaque;
bool _opaque; bool _opaque;
void set opaque(bool value) { void set opaque(bool value) {
if (_opaque = value)
return;
_opaque = value; _opaque = value;
_state?.setState(() {}); _state?.setState(() {});
} }
...@@ -51,10 +53,6 @@ class OverlayState extends State<Overlay> { ...@@ -51,10 +53,6 @@ class OverlayState extends State<Overlay> {
void insert(OverlayEntry entry, { OverlayEntry above }) { void insert(OverlayEntry entry, { OverlayEntry above }) {
assert(entry._state == null); assert(entry._state == null);
if (above != null) {
print('above._state ${above._state} --- ${above._state == this}');
print('_entries.contains ${_entries.contains(above)}');
}
assert(above == null || (above._state == this && _entries.contains(above))); assert(above == null || (above._state == this && _entries.contains(above)));
entry._state = this; entry._state = this;
setState(() { setState(() {
......
// 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';
// 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,
this.route
}) : super(key: key);
final PageRoute route;
_PageState createState() => new _PageState();
}
class _PageState extends State<_Page> {
final AnimatedValue<Point> _position =
new AnimatedValue<Point>(const Point(0.0, 75.0), end: Point.origin, curve: Curves.easeOut);
final AnimatedValue<double> _opacity =
new AnimatedValue<double>(0.0, end: 1.0, curve: Curves.easeOut);
final GlobalKey _subtreeKey = new GlobalKey();
Widget build(BuildContext context) {
if (config.route._offstage) {
return new OffStage(
child: new KeyedSubtree(
key: _subtreeKey,
child: _invokeBuilder()
)
);
}
return new SlideTransition(
performance: config.route.performance,
position: _position,
child: new FadeTransition(
performance: config.route.performance,
opacity: _opacity,
child: new KeyedSubtree(
key: _subtreeKey,
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.settings: const NamedRouteSettings()
}) {
assert(builder != null);
assert(opaque);
}
final WidgetBuilder builder;
final NamedRouteSettings settings;
final GlobalKey<_PageState> pageKey = new GlobalKey<_PageState>();
String get name => settings.name;
Duration get transitionDuration => const Duration(milliseconds: 150);
List<Widget> createWidgets() => [ new _Page(key: pageKey, route: this) ];
bool get offstage => _offstage;
bool _offstage = false;
void set offstage (bool value) {
if (_offstage == value)
return;
_offstage = value;
pageKey.currentState?.setState(() { });
}
String get debugLabel => '${super.debugLabel}($name)';
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment