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 {
}
bool _haveScheduledVisualUpdate = false;
int _nextPrivateCallbackId = 0; // negative
int _nextCallbackId = 0; // positive
final List<SchedulerCallback> _persistentCallbacks = new List<SchedulerCallback>();
......@@ -55,7 +54,6 @@ class Scheduler {
Duration timeStamp = new Duration(
microseconds: (rawTimeStamp.inMicroseconds / timeDilation).round());
_haveScheduledVisualUpdate = false;
assert(_postFrameCallbacks.length == 0);
Map<int, SchedulerCallback> callbacks = _transientCallbacks;
_transientCallbacks = new Map<int, SchedulerCallback>();
......@@ -68,9 +66,11 @@ class Scheduler {
for (SchedulerCallback callback in _persistentCallbacks)
invokeCallback(callback, timeStamp);
for (SchedulerCallback callback in _postFrameCallbacks)
invokeCallback(callback, timeStamp);
List<SchedulerCallback> localPostFrameCallbacks =
new List<SchedulerCallback>.from(_postFrameCallbacks);
_postFrameCallbacks.clear();
for (SchedulerCallback callback in localPostFrameCallbacks)
invokeCallback(callback, timeStamp);
_inFrame = false;
}
......@@ -133,13 +133,7 @@ class Scheduler {
/// frame. In this case, the registration order is not preserved. Callbacks
/// are called in an arbitrary order.
void requestPostFrameCallback(SchedulerCallback callback) {
if (_inFrame) {
_postFrameCallbacks.add(callback);
} else {
_nextPrivateCallbackId -= 1;
_transientCallbacks[_nextPrivateCallbackId] = callback;
ensureVisualUpdate();
}
_postFrameCallbacks.add(callback);
}
/// Ensure that a frame will be produced after this function is called.
......
......@@ -9,7 +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 'package:flutter/src/widgets/hero_controller.dart' as n2;
import 'theme.dart';
import 'title.dart';
......@@ -87,13 +87,16 @@ class _MaterialAppState extends State<MaterialApp> {
void _metricHandler(Size size) => setState(() { _size = size; });
n2.Route _generateRoute(n2.RouteArguments args) {
return new n2.PageRoute(
final n2.HeroController _heroController = new n2.HeroController();
n2.Route _generateRoute(n2.NamedRouteSettings settings) {
return new n2.HeroPageRoute(
builder: (BuildContext context) {
RouteArguments routeArgs = new RouteArguments(context: context);
return config.routes[args.name](routeArgs);
RouteBuilder builder = config.routes[settings.name] ?? config.onGenerateRoute(settings.name);
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 {
hero.targetState._setChild(hero.key);
for (HeroState source in hero.sourceStates)
source._resetChild();
if (onQuestFinished != null)
onQuestFinished();
}
_heroes.clear();
_currentPerformance = null;
if (onQuestFinished != null)
onQuestFinished();
}
}
......
......@@ -13,7 +13,7 @@ abstract class Route {
final List<OverlayEntry> _entries = new List<OverlayEntry>();
void add(OverlayState overlay, OverlayEntry insertionPoint) {
void didPush(OverlayState overlay, OverlayEntry insertionPoint) {
List<Widget> widgets = createWidgets();
for (Widget widget in widgets) {
_entries.add(new OverlayEntry(child: widget));
......@@ -22,20 +22,22 @@ abstract class Route {
}
}
void remove(dynamic result) {
void didMakeCurrent() { }
void didPop(dynamic result) {
for (OverlayEntry entry in _entries)
entry.remove();
}
}
class RouteArguments {
const RouteArguments({ this.name: '<anonymous>', this.mostValuableKeys });
class NamedRouteSettings {
const NamedRouteSettings({ this.name: '<anonymous>', this.mostValuableKeys });
final String name;
final Set<Key> mostValuableKeys;
}
typedef Route RouteFactory(RouteArguments args);
typedef Route RouteFactory(NamedRouteSettings settings);
class Navigator extends StatefulComponent {
Navigator({
......@@ -67,18 +69,19 @@ class Navigator extends StatefulComponent {
}
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> _modal = new List<Route>();
void 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;
OverlayState get overlay => _overlayKey.currentState;
OverlayEntry get _topRouteOverlay {
OverlayEntry get _currentOverlay {
for (Route route in _ephemeral.reversed) {
if (route.topEntry != null)
return route.topEntry;
......@@ -90,41 +93,49 @@ class NavigatorState extends State<Navigator> {
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 }) {
RouteArguments args = new RouteArguments(name: name, mostValuableKeys: mostValuableKeys);
push(config.onGenerateRoute(args) ?? config.onUnknownRoute(args));
NamedRouteSettings settings = new NamedRouteSettings(
name: name,
mostValuableKeys: mostValuableKeys
);
push(config.onGenerateRoute(settings) ?? config.onUnknownRoute(settings));
}
void push(Route route) {
_popAllEphemeralRoutes();
route.add(_overlay.currentState, _topRouteOverlay);
route.didPush(overlay, _currentOverlay);
_modal.add(route);
route.didMakeCurrent();
}
void pushEphemeral(Route route) {
route.add(_overlay.currentState, _topRouteOverlay);
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.remove(null);
route.didPop(null);
assert(_ephemeral.isEmpty);
}
void pop([dynamic result]) {
if (_ephemeral.isNotEmpty) {
_ephemeral.removeLast().remove(result);
return;
}
_modal.removeLast().remove(result);
_removeCurrentRoute().didPop(result);
_currentRoute.didMakeCurrent();
}
Widget build(BuildContext context) {
return new Overlay(
key: _overlay,
key: _overlayKey,
initialEntries: _modal.first._entries
);
}
......
......@@ -16,6 +16,8 @@ class OverlayEntry {
bool get opaque => _opaque;
bool _opaque;
void set opaque(bool value) {
if (_opaque = value)
return;
_opaque = value;
_state?.setState(() {});
}
......@@ -51,10 +53,6 @@ class OverlayState extends State<Overlay> {
void insert(OverlayEntry entry, { OverlayEntry above }) {
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)));
entry._state = this;
setState(() {
......
......@@ -10,6 +10,7 @@ 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;
......@@ -27,21 +28,28 @@ abstract class TransitionRoute extends Route {
dynamic _result;
void _handleStatusChanged(PerformanceStatus status) {
if (status == PerformanceStatus.completed && opaque) {
bottomEntry.opaque = true;
} else if (status == PerformanceStatus.dismissed) {
super.remove(_result);
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 add(OverlayState overlayer, OverlayEntry insertionPoint) {
void didPush(OverlayState overlay, OverlayEntry insertionPoint) {
_performance = createPerformance()
..addStatusListener(_handleStatusChanged)
..forward();
super.add(overlayer, insertionPoint);
super.didPush(overlay, insertionPoint);
}
void remove(dynamic result) {
void didPop(dynamic result) {
_result = result;
_performance.reverse();
}
......@@ -52,8 +60,9 @@ abstract class TransitionRoute extends Route {
class _Page extends StatefulComponent {
_Page({
PageRoute route
}) : route = route, super(key: new GlobalObjectKey(route));
Key key,
this.route
}) : super(key: key);
final PageRoute route;
......@@ -67,14 +76,27 @@ class _PageState extends State<_Page> {
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: _invokeBuilder()
child: new KeyedSubtree(
key: _subtreeKey,
child: _invokeBuilder()
)
)
);
}
......@@ -94,19 +116,30 @@ class _PageState extends State<_Page> {
class PageRoute extends TransitionRoute {
PageRoute({
this.builder,
this.args: const RouteArguments()
this.settings: const NamedRouteSettings()
}) {
assert(builder != null);
assert(opaque);
}
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);
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)';
}
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