Commit 395184f7 authored by Adam Barth's avatar Adam Barth

Add hero transition support to Navigator2

In this approach, the hero support is layered on top of the basic navigator
functionality.
parent 2e301d4d
...@@ -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,7 +9,7 @@ import 'package:flutter/services.dart'; ...@@ -9,7 +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/page.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';
...@@ -87,13 +87,16 @@ class _MaterialAppState extends State<MaterialApp> { ...@@ -87,13 +87,16 @@ class _MaterialAppState extends State<MaterialApp> {
void _metricHandler(Size size) => setState(() { _size = size; }); void _metricHandler(Size size) => setState(() { _size = size; });
n2.Route _generateRoute(n2.RouteArguments args) { final n2.HeroController _heroController = new n2.HeroController();
return new n2.PageRoute(
n2.Route _generateRoute(n2.NamedRouteSettings settings) {
return new n2.HeroPageRoute(
builder: (BuildContext context) { builder: (BuildContext context) {
RouteArguments routeArgs = new RouteArguments(context: context); RouteBuilder builder = config.routes[settings.name] ?? config.onGenerateRoute(settings.name);
return config.routes[args.name](routeArgs); return builder(new RouteArguments(context: context));
}, },
args: args settings: settings,
heroController: _heroController
); );
} }
......
// 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();
} }
} }
......
...@@ -13,7 +13,7 @@ abstract class Route { ...@@ -13,7 +13,7 @@ abstract class Route {
final List<OverlayEntry> _entries = new List<OverlayEntry>(); final List<OverlayEntry> _entries = new List<OverlayEntry>();
void add(OverlayState overlay, OverlayEntry insertionPoint) { void didPush(OverlayState overlay, OverlayEntry insertionPoint) {
List<Widget> widgets = createWidgets(); List<Widget> widgets = createWidgets();
for (Widget widget in widgets) { for (Widget widget in widgets) {
_entries.add(new OverlayEntry(child: widget)); _entries.add(new OverlayEntry(child: widget));
...@@ -22,20 +22,22 @@ abstract class Route { ...@@ -22,20 +22,22 @@ abstract class Route {
} }
} }
void remove(dynamic result) { void didMakeCurrent() { }
void didPop(dynamic result) {
for (OverlayEntry entry in _entries) for (OverlayEntry entry in _entries)
entry.remove(); entry.remove();
} }
} }
class RouteArguments { class NamedRouteSettings {
const RouteArguments({ this.name: '<anonymous>', this.mostValuableKeys }); const NamedRouteSettings({ this.name: '<anonymous>', this.mostValuableKeys });
final String name; final String name;
final Set<Key> mostValuableKeys; final Set<Key> mostValuableKeys;
} }
typedef Route RouteFactory(RouteArguments args); typedef Route RouteFactory(NamedRouteSettings settings);
class Navigator extends StatefulComponent { class Navigator extends StatefulComponent {
Navigator({ Navigator({
...@@ -67,18 +69,19 @@ class Navigator extends StatefulComponent { ...@@ -67,18 +69,19 @@ class Navigator extends StatefulComponent {
} }
class NavigatorState extends State<Navigator> { class NavigatorState extends State<Navigator> {
final GlobalKey<OverlayState> _overlay = new GlobalKey<OverlayState>(); final GlobalKey<OverlayState> _overlayKey = new GlobalKey<OverlayState>();
final List<Route> _ephemeral = new List<Route>(); final List<Route> _ephemeral = new List<Route>();
final List<Route> _modal = new List<Route>(); final List<Route> _modal = new List<Route>();
void initState() { void initState() {
super.initState(); super.initState();
push(config.onGenerateRoute(new RouteArguments(name: Navigator.defaultRouteName))); push(config.onGenerateRoute(new NamedRouteSettings(name: Navigator.defaultRouteName)));
} }
bool get hasPreviousRoute => _modal.length > 1; bool get hasPreviousRoute => _modal.length > 1;
OverlayState get overlay => _overlayKey.currentState;
OverlayEntry get _topRouteOverlay { OverlayEntry get _currentOverlay {
for (Route route in _ephemeral.reversed) { for (Route route in _ephemeral.reversed) {
if (route.topEntry != null) if (route.topEntry != null)
return route.topEntry; return route.topEntry;
...@@ -90,41 +93,49 @@ class NavigatorState extends State<Navigator> { ...@@ -90,41 +93,49 @@ class NavigatorState extends State<Navigator> {
return null; 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 }) { void pushNamed(String name, { Set<Key> mostValuableKeys }) {
RouteArguments args = new RouteArguments(name: name, mostValuableKeys: mostValuableKeys); NamedRouteSettings settings = new NamedRouteSettings(
push(config.onGenerateRoute(args) ?? config.onUnknownRoute(args)); name: name,
mostValuableKeys: mostValuableKeys
);
push(config.onGenerateRoute(settings) ?? config.onUnknownRoute(settings));
} }
void push(Route route) { void push(Route route) {
_popAllEphemeralRoutes(); _popAllEphemeralRoutes();
route.add(_overlay.currentState, _topRouteOverlay); route.didPush(overlay, _currentOverlay);
_modal.add(route); _modal.add(route);
route.didMakeCurrent();
} }
void pushEphemeral(Route route) { void pushEphemeral(Route route) {
route.add(_overlay.currentState, _topRouteOverlay); route.didPush(overlay, _currentOverlay);
_ephemeral.add(route); _ephemeral.add(route);
route.didMakeCurrent();
} }
void _popAllEphemeralRoutes() { void _popAllEphemeralRoutes() {
List<Route> localEphemeral = new List<Route>.from(_ephemeral); List<Route> localEphemeral = new List<Route>.from(_ephemeral);
_ephemeral.clear(); _ephemeral.clear();
for (Route route in localEphemeral) for (Route route in localEphemeral)
route.remove(null); route.didPop(null);
assert(_ephemeral.isEmpty); assert(_ephemeral.isEmpty);
} }
void pop([dynamic result]) { void pop([dynamic result]) {
if (_ephemeral.isNotEmpty) { _removeCurrentRoute().didPop(result);
_ephemeral.removeLast().remove(result); _currentRoute.didMakeCurrent();
return;
}
_modal.removeLast().remove(result);
} }
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new Overlay( return new Overlay(
key: _overlay, key: _overlayKey,
initialEntries: _modal.first._entries initialEntries: _modal.first._entries
); );
} }
......
...@@ -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(() {
......
...@@ -10,6 +10,7 @@ import 'navigator2.dart'; ...@@ -10,6 +10,7 @@ import 'navigator2.dart';
import 'overlay.dart'; import 'overlay.dart';
import 'transitions.dart'; import 'transitions.dart';
// TODO(abarth): Should we add a type for the result?
abstract class TransitionRoute extends Route { abstract class TransitionRoute extends Route {
bool get opaque => true; bool get opaque => true;
...@@ -27,21 +28,28 @@ abstract class TransitionRoute extends Route { ...@@ -27,21 +28,28 @@ abstract class TransitionRoute extends Route {
dynamic _result; dynamic _result;
void _handleStatusChanged(PerformanceStatus status) { void _handleStatusChanged(PerformanceStatus status) {
if (status == PerformanceStatus.completed && opaque) { switch (status) {
bottomEntry.opaque = true; case PerformanceStatus.completed:
} else if (status == PerformanceStatus.dismissed) { bottomEntry.opaque = opaque;
super.remove(_result); break;
case PerformanceStatus.forward:
case PerformanceStatus.reverse:
bottomEntry.opaque = false;
break;
case PerformanceStatus.dismissed:
super.didPop(_result);
break;
} }
} }
void add(OverlayState overlayer, OverlayEntry insertionPoint) { void didPush(OverlayState overlay, OverlayEntry insertionPoint) {
_performance = createPerformance() _performance = createPerformance()
..addStatusListener(_handleStatusChanged) ..addStatusListener(_handleStatusChanged)
..forward(); ..forward();
super.add(overlayer, insertionPoint); super.didPush(overlay, insertionPoint);
} }
void remove(dynamic result) { void didPop(dynamic result) {
_result = result; _result = result;
_performance.reverse(); _performance.reverse();
} }
...@@ -52,8 +60,9 @@ abstract class TransitionRoute extends Route { ...@@ -52,8 +60,9 @@ abstract class TransitionRoute extends Route {
class _Page extends StatefulComponent { class _Page extends StatefulComponent {
_Page({ _Page({
PageRoute route Key key,
}) : route = route, super(key: new GlobalObjectKey(route)); this.route
}) : super(key: key);
final PageRoute route; final PageRoute route;
...@@ -67,15 +76,28 @@ class _PageState extends State<_Page> { ...@@ -67,15 +76,28 @@ class _PageState extends State<_Page> {
final AnimatedValue<double> _opacity = final AnimatedValue<double> _opacity =
new AnimatedValue<double>(0.0, end: 1.0, curve: Curves.easeOut); new AnimatedValue<double>(0.0, end: 1.0, curve: Curves.easeOut);
final GlobalKey _subtreeKey = new GlobalKey();
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (config.route._offstage) {
return new OffStage(
child: new KeyedSubtree(
key: _subtreeKey,
child: _invokeBuilder()
)
);
}
return new SlideTransition( return new SlideTransition(
performance: config.route.performance, performance: config.route.performance,
position: _position, position: _position,
child: new FadeTransition( child: new FadeTransition(
performance: config.route.performance, performance: config.route.performance,
opacity: _opacity, opacity: _opacity,
child: new KeyedSubtree(
key: _subtreeKey,
child: _invokeBuilder() child: _invokeBuilder()
) )
)
); );
} }
...@@ -94,19 +116,30 @@ class _PageState extends State<_Page> { ...@@ -94,19 +116,30 @@ class _PageState extends State<_Page> {
class PageRoute extends TransitionRoute { class PageRoute extends TransitionRoute {
PageRoute({ PageRoute({
this.builder, this.builder,
this.args: const RouteArguments() this.settings: const NamedRouteSettings()
}) { }) {
assert(builder != null); assert(builder != null);
assert(opaque); assert(opaque);
} }
final WidgetBuilder builder; final WidgetBuilder builder;
final RouteArguments args; final NamedRouteSettings settings;
String get name => args.name; final GlobalKey<_PageState> pageKey = new GlobalKey<_PageState>();
String get name => settings.name;
Duration get transitionDuration => const Duration(milliseconds: 150); Duration get transitionDuration => const Duration(milliseconds: 150);
List<Widget> createWidgets() => [ new _Page(route: this) ]; 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)'; 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