diff --git a/packages/flutter/lib/src/animation/scheduler.dart b/packages/flutter/lib/src/animation/scheduler.dart index 2d374f83575..8b1d891b465 100644 --- a/packages/flutter/lib/src/animation/scheduler.dart +++ b/packages/flutter/lib/src/animation/scheduler.dart @@ -31,7 +31,6 @@ class Scheduler { } bool _haveScheduledVisualUpdate = false; - int _nextPrivateCallbackId = 0; // negative int _nextCallbackId = 0; // positive final List _persistentCallbacks = new List(); @@ -55,7 +54,6 @@ class Scheduler { Duration timeStamp = new Duration( microseconds: (rawTimeStamp.inMicroseconds / timeDilation).round()); _haveScheduledVisualUpdate = false; - assert(_postFrameCallbacks.length == 0); Map callbacks = _transientCallbacks; _transientCallbacks = new Map(); @@ -68,9 +66,11 @@ class Scheduler { for (SchedulerCallback callback in _persistentCallbacks) invokeCallback(callback, timeStamp); - for (SchedulerCallback callback in _postFrameCallbacks) - invokeCallback(callback, timeStamp); + List localPostFrameCallbacks = + new List.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. diff --git a/packages/flutter/lib/src/material/material_app.dart b/packages/flutter/lib/src/material/material_app.dart index dc7bfb3f3a6..117c6d5c038 100644 --- a/packages/flutter/lib/src/material/material_app.dart +++ b/packages/flutter/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/hero_controller.dart' as n2; import 'theme.dart'; import 'title.dart'; @@ -86,12 +87,25 @@ class _MaterialAppState extends State { 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 navigator; if (_kUseNavigator2) { navigator = new n2.Navigator( key: _navigator, - routes: config.routes + onGenerateRoute: _generateRoute ); } else { navigator = new Navigator( diff --git a/packages/flutter/lib/src/widgets/hero_controller.dart b/packages/flutter/lib/src/widgets/hero_controller.dart new file mode 100644 index 00000000000..3e0852bbce1 --- /dev/null +++ b/packages/flutter/lib/src/widgets/hero_controller.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 '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 _overlayEntries = new List(); + + 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 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 _getMostValuableKeys() { + Set result = new Set(); + 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 mostValuableKeys = _getMostValuableKeys(); + + Map heroesFrom = _party.isEmpty ? + Hero.of(_from.pageKey.currentContext, mostValuableKeys) : _party.getHeroesToAnimate(); + + BuildContext context = _to.pageKey.currentContext; + Map 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 heroes = _party.getWidgets(navigator.context, performance); + _addHeroesToOverlay(heroes, navigator.overlay); + } +} diff --git a/packages/flutter/lib/src/widgets/heroes.dart b/packages/flutter/lib/src/widgets/heroes.dart index 83f794d3b27..0dee9abd08a 100644 --- a/packages/flutter/lib/src/widgets/heroes.dart +++ b/packages/flutter/lib/src/widgets/heroes.dart @@ -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(); } } diff --git a/packages/flutter/lib/src/widgets/navigator2.dart b/packages/flutter/lib/src/widgets/navigator2.dart index 6370862d496..354bb95cb5b 100644 --- a/packages/flutter/lib/src/widgets/navigator2.dart +++ b/packages/flutter/lib/src/widgets/navigator2.dart @@ -2,55 +2,56 @@ // 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 didPush(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 didMakeCurrent() { } + void didPop(dynamic result) { - _entry.remove(); + for (OverlayEntry entry in _entries) + entry.remove(); } } -typedef Widget RouteBuilder(args); -typedef RouteBuilder RouteGenerator(String name); +class NamedRouteSettings { + const NamedRouteSettings({ this.name: '', this.mostValuableKeys }); -const String _kDefaultPageName = '/'; + final String name; + final Set mostValuableKeys; +} + +typedef Route RouteFactory(NamedRouteSettings settings); 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 +69,74 @@ class Navigator extends StatefulComponent { } class NavigatorState extends State { - GlobalKey _overlay = new GlobalKey(); - List _history = new List(); + final GlobalKey _overlayKey = 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 NamedRouteSettings(name: Navigator.defaultRouteName))); } - RouteBuilder _generatePage(String name) { - assert(config.onGeneratePage != null); - return config.onGeneratePage(name); + 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; } - bool get hasPreviousRoute => _history.length > 1; + Route get _currentRoute => _ephemeral.isNotEmpty ? _ephemeral.last : _modal.last; + + Route _removeCurrentRoute() { + return _ephemeral.isNotEmpty ? _ephemeral.removeLast() : _modal.removeLast(); + } 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, + NamedRouteSettings settings = new NamedRouteSettings( name: name, mostValuableKeys: mostValuableKeys - )); - } - - void _addRouteToHistory(Route route) { - route.willPush(); - route._entry = new OverlayEntry(child: route._child); - _history.add(route); + ); + push(config.onGenerateRoute(settings) ?? config.onUnknownRoute(settings)); } void push(Route route) { - OverlayEntry reference = _history.last._entry; - _addRouteToHistory(route); - _overlay.currentState.insert(route._entry, above: reference); + _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 localEphemeral = new List.from(_ephemeral); + _ephemeral.clear(); + for (Route route in localEphemeral) + route.didPop(null); + assert(_ephemeral.isEmpty); } void pop([dynamic result]) { - _history.removeLast().didPop(result); + _removeCurrentRoute().didPop(result); + _currentRoute.didMakeCurrent(); } Widget build(BuildContext context) { return new Overlay( - key: _overlay, - initialEntries: [ _history.first._entry ] + key: _overlayKey, + 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/packages/flutter/lib/src/widgets/overlay.dart b/packages/flutter/lib/src/widgets/overlay.dart index 8e8ec6f570f..fc289fbb397 100644 --- a/packages/flutter/lib/src/widgets/overlay.dart +++ b/packages/flutter/lib/src/widgets/overlay.dart @@ -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 { 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(() { diff --git a/packages/flutter/lib/src/widgets/page.dart b/packages/flutter/lib/src/widgets/page.dart new file mode 100644 index 00000000000..4e1341a886a --- /dev/null +++ b/packages/flutter/lib/src/widgets/page.dart @@ -0,0 +1,145 @@ +// 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 _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); + + 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 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)'; +}