Commit 309d25d4 authored by Hixie's avatar Hixie

Move Material page animations to Material layer.

PageRoute is now MaterialPageRoute.

This also changes the following:

- Now the HeroController is a Navigator observer, rather than a feature
  of HeroPageRoutes, which are gone. This means heroes can work between
  any kind of ModalRoute now.

- ModalPageRoute is moved from modal_barrier.dart to routes.dart.

- It allows routes to opt-out of their modal barrier being a shortcut to
  popping the route.

- Features of PageRoute that aren't Material-specific get promoted to
  ModalRoute features: storage, the subtree key, offstageness...

The AnimatedModalBarrier is still a ModalRoute feature.
parent 8bc9e1b8
......@@ -33,9 +33,11 @@ export 'src/material/material.dart';
export 'src/material/material_app.dart';
export 'src/material/material_button.dart';
export 'src/material/material_list.dart';
export 'src/material/popup_menu_item.dart';
export 'src/material/page.dart';
export 'src/material/popup_menu.dart';
export 'src/material/popup_menu_item.dart';
export 'src/material/progress_indicator.dart';
export 'src/material/radial_reaction.dart';
export 'src/material/radio.dart';
export 'src/material/raised_button.dart';
export 'src/material/scaffold.dart';
......@@ -44,11 +46,10 @@ export 'src/material/shadows.dart';
export 'src/material/snack_bar.dart';
export 'src/material/switch.dart';
export 'src/material/tabs.dart';
export 'src/material/theme_data.dart';
export 'src/material/theme.dart';
export 'src/material/theme_data.dart';
export 'src/material/title.dart';
export 'src/material/tool_bar.dart';
export 'src/material/typography.dart';
export 'src/material/radial_reaction.dart';
export 'widgets.dart';
......@@ -168,7 +168,7 @@ class Performance extends PerformanceView {
/// Returns a [PerformanceView] for this performance,
/// so that a pointer to this object can be passed around without
/// allowing users of that pointer to mutate the AnimationPerformance state.
/// allowing users of that pointer to mutate the Performance state.
PerformanceView get view => this;
/// The length of time this performance should last
......
......@@ -124,7 +124,9 @@ class _DialogRoute extends ModalRoute {
Duration get transitionDuration => const Duration(milliseconds: 150);
Color get barrierColor => Colors.black54;
Widget buildModalWidget(BuildContext context) {
Widget buildPage(BuildContext context) => child;
Widget buildTransition(BuildContext context, PerformanceView performance, Widget child) {
return new FadeTransition(
performance: performance,
opacity: new AnimatedValue<double>(0.0, end: 1.0, curve: Curves.easeOut),
......
......@@ -8,6 +8,7 @@ import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'page.dart';
import 'theme.dart';
import 'title.dart';
......@@ -95,13 +96,12 @@ class _MaterialAppState extends State<MaterialApp> {
final HeroController _heroController = new HeroController();
Route _generateRoute(NamedRouteSettings settings) {
return new HeroPageRoute(
return new MaterialPageRoute(
builder: (BuildContext context) {
RouteBuilder builder = config.routes[settings.name] ?? config.onGenerateRoute(settings.name);
return builder(new RouteArguments(context: context));
},
settings: settings,
heroController: _heroController
settings: settings
);
}
......@@ -118,7 +118,8 @@ class _MaterialAppState extends State<MaterialApp> {
title: config.title,
child: new Navigator(
key: _navigator,
onGenerateRoute: _generateRoute
onGenerateRoute: _generateRoute,
observer: _heroController
)
)
)
......
......@@ -3,16 +3,10 @@
// found in the LICENSE file.
import 'package:flutter/animation.dart';
import 'package:flutter/widgets.dart';
import 'basic.dart';
import 'framework.dart';
import 'modal_barrier.dart';
import 'navigator.dart';
import 'page_storage.dart';
import 'transitions.dart';
class _PageTransition extends TransitionWithChild {
_PageTransition({
class _MaterialPageTransition extends TransitionWithChild {
_MaterialPageTransition({
Key key,
PerformanceView performance,
Widget child
......@@ -33,6 +27,7 @@ class _PageTransition extends TransitionWithChild {
..translate(_position.value.x, _position.value.y);
return new Transform(
transform: transform,
// TODO(ianh): tell the transform to be un-transformed for hit testing
child: new Opacity(
opacity: _opacity.value,
child: child
......@@ -41,82 +36,38 @@ class _PageTransition extends TransitionWithChild {
}
}
class _Page extends StatefulComponent {
_Page({
Key key,
this.route
}) : super(key: key);
final PageRoute route;
class MaterialPageRoute extends ModalRoute {
MaterialPageRoute({
this.builder,
NamedRouteSettings settings: const NamedRouteSettings()
}) : super(settings: settings) {
assert(builder != null);
assert(opaque);
}
_PageState createState() => new _PageState();
}
final WidgetBuilder builder;
class _PageState extends State<_Page> {
final GlobalKey _subtreeKey = new GlobalKey();
Duration get transitionDuration => const Duration(milliseconds: 150);
Widget build(BuildContext context) {
if (config.route._offstage) {
return new OffStage(
child: new PageStorage(
key: _subtreeKey,
bucket: config.route._storageBucket,
child: _invokeBuilder()
)
);
}
return new _PageTransition(
performance: config.route.performance,
child: new PageStorage(
key: _subtreeKey,
bucket: config.route._storageBucket,
child: _invokeBuilder()
)
);
}
bool get opaque => true;
Widget _invokeBuilder() {
Widget result = config.route.builder(context);
Widget buildPage(BuildContext context) {
Widget result = builder(context);
assert(() {
if (result == null)
debugPrint('The builder for route \'${config.route.name}\' returned null. Route builders must never return null.');
debugPrint('The builder for route \'${settings.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 ModalRoute {
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>();
bool get opaque => true;
String get name => settings.name;
Duration get transitionDuration => const Duration(milliseconds: 150);
Widget buildModalWidget(BuildContext context) => new _Page(key: pageKey, route: this);
final PageStorageBucket _storageBucket = new PageStorageBucket();
bool get offstage => _offstage;
bool _offstage = false;
void set offstage (bool value) {
if (_offstage == value)
return;
_offstage = value;
pageKey.currentState?.setState(() { });
Widget buildTransition(BuildContext context, PerformanceView performance, Widget child) {
return new _MaterialPageTransition(
performance: performance,
child: child
);
}
String get debugLabel => '${super.debugLabel}($name)';
String get debugLabel => '${super.debugLabel}(${settings.name})';
}
......@@ -110,7 +110,7 @@ class _MenuRoute extends ModalRoute {
bool get opaque => false;
Duration get transitionDuration => _kMenuDuration;
Widget buildModalWidget(BuildContext context) => new _PopupMenu(route: this);
Widget buildPage(BuildContext context) => new _PopupMenu(route: this);
}
Future showMenu({ BuildContext context, ModalPosition position, List<PopupMenuItem> items, int level: 4 }) {
......
......@@ -10,68 +10,46 @@ import 'framework.dart';
import 'heroes.dart';
import 'navigator.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;
NavigatorState _navigator;
void didPush(OverlayState overlay, OverlayEntry insertionPoint) {
super.didPush(overlay, insertionPoint);
// TODO(abarth): Pass the NavigatorState explicitly.
if (overlay != null) {
_navigator = Navigator.of(overlay.context);
heroController?.didPush(_navigator, this);
}
}
void didPop(dynamic result) {
super.didPop(result);
if (_navigator != null) {
heroController?.didPop(_navigator, this);
_navigator = null;
}
}
}
import 'routes.dart';
class HeroController {
class HeroController extends NavigatorObserver {
HeroController() {
_party = new HeroParty(onQuestFinished: _handleQuestFinished);
}
HeroParty _party;
PerformanceView _performance;
HeroPageRoute _from;
HeroPageRoute _to;
ModalRoute _from;
ModalRoute _to;
final List<OverlayEntry> _overlayEntries = new List<OverlayEntry>();
void didPush(NavigatorState navigator, HeroPageRoute route) {
void didPushModal(Route route) {
assert(navigator != null);
assert(route != null);
assert(route.performance != null);
Route from = navigator.currentRoute;
if (from is HeroPageRoute)
_from = from;
_to = route;
_performance = route.performance;
_checkForHeroQuest();
if (route is ModalRoute) { // as opposed to StateRoute, say
assert(route.performance != null);
Route from = navigator.currentRoute;
if (from is ModalRoute) // as opposed to the many other types of routes, or null
_from = from;
_to = route;
_performance = route.performance;
_checkForHeroQuest();
}
}
void didPop(NavigatorState navigator, HeroPageRoute route) {
void didPopModal(Route route) {
assert(navigator != null);
assert(route != null);
assert(route.performance != null);
Route to = navigator.currentRoute;
if (to is HeroPageRoute) {
_to = to;
_from = route;
_performance = route.performance;
_checkForHeroQuest();
if (route is ModalRoute) { // as opposed to StateRoute, say
assert(route.performance != null);
Route to = navigator.currentRoute;
if (to is ModalRoute) { // as opposed to the many other types of routes
_to = to;
_from = route;
_performance = route.performance;
_checkForHeroQuest();
}
}
}
......@@ -123,10 +101,9 @@ class HeroController {
Set<Key> mostValuableKeys = _getMostValuableKeys();
Map<Object, HeroHandle> heroesFrom = _party.isEmpty ?
Hero.of(_from.pageKey.currentContext, mostValuableKeys) : _party.getHeroesToAnimate();
Hero.of(_from.subtreeContext, mostValuableKeys) : _party.getHeroesToAnimate();
BuildContext context = _to.pageKey.currentContext;
Map<Object, HeroHandle> heroesTo = Hero.of(context, mostValuableKeys);
Map<Object, HeroHandle> heroesTo = Hero.of(_to.subtreeContext, mostValuableKeys);
_to.offstage = false;
PerformanceView performance = _performance;
......@@ -136,7 +113,6 @@ class HeroController {
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);
......
......@@ -2,32 +2,30 @@
// 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 'focus.dart';
import 'framework.dart';
import 'navigator.dart';
import 'routes.dart';
import 'status_transitions.dart';
import 'transitions.dart';
const Color _kTransparent = const Color(0x00000000);
const Color kTransparent = const Color(0x00000000);
class ModalBarrier extends StatelessComponent {
ModalBarrier({
Key key,
this.color: _kTransparent
this.color: kTransparent,
this.dismissable: true
}) : super(key: key);
final Color color;
final bool dismissable;
Widget build(BuildContext context) {
return new Listener(
onPointerDown: (_) {
Navigator.of(context).pop();
if (dismissable)
Navigator.of(context).pop();
},
child: new ConstrainedBox(
constraints: const BoxConstraints.expand(),
......@@ -45,11 +43,13 @@ class AnimatedModalBarrier extends StatelessComponent {
AnimatedModalBarrier({
Key key,
this.color,
this.performance
this.performance,
this.dismissable: true
}) : super(key: key);
final AnimatedColorValue color;
final PerformanceView performance;
final bool dismissable;
Widget build(BuildContext context) {
return new BuilderTransition(
......@@ -58,69 +58,12 @@ class AnimatedModalBarrier extends StatelessComponent {
builder: (BuildContext context) {
return new IgnorePointer(
ignoring: performance.status == PerformanceStatus.reverse,
child: new ModalBarrier(color: color.value)
child: new ModalBarrier(
color: color.value,
dismissable: dismissable
)
);
}
);
}
}
class _ModalScope extends StatusTransitionComponent {
_ModalScope({
Key key,
ModalRoute route,
this.child
}) : route = route, super(key: key, performance: route.performance);
final ModalRoute route;
final Widget child;
Widget build(BuildContext context) {
Widget focus = new Focus(
key: new GlobalObjectKey(route),
child: new IgnorePointer(
ignoring: route.performance.status == PerformanceStatus.reverse,
child: child
)
);
ModalPosition position = route.position;
if (position == null)
return focus;
return new Positioned(
top: position.top,
right: position.right,
bottom: position.bottom,
left: position.left,
child: focus
);
}
}
class ModalPosition {
const ModalPosition({ this.top, this.right, this.bottom, this.left });
final double top;
final double right;
final double bottom;
final double left;
}
abstract class ModalRoute extends TransitionRoute {
ModalRoute({ Completer completer }) : super(completer: completer);
ModalPosition get position => null;
Color get barrierColor => _kTransparent;
Widget buildModalWidget(BuildContext context);
Widget _buildModalBarrier(BuildContext context) {
return new AnimatedModalBarrier(
color: new AnimatedColorValue(_kTransparent, end: barrierColor, curve: Curves.ease),
performance: performance
);
}
Widget _buildModalScope(BuildContext context) {
return new _ModalScope(route: this, child: buildModalWidget(context));
}
List<WidgetBuilder> get builders => <WidgetBuilder>[ _buildModalBarrier, _buildModalScope ];
}
......@@ -21,17 +21,26 @@ class NamedRouteSettings {
typedef Route RouteFactory(NamedRouteSettings settings);
class NavigatorObserver {
NavigatorState _navigator;
NavigatorState get navigator => _navigator;
void didPopModal(Route route) { }
void didPushModal(Route route) { }
}
class Navigator extends StatefulComponent {
Navigator({
Key key,
this.onGenerateRoute,
this.onUnknownRoute
this.onUnknownRoute,
this.observer
}) : super(key: key) {
assert(onGenerateRoute != null);
}
final RouteFactory onGenerateRoute;
final RouteFactory onUnknownRoute;
final NavigatorObserver observer;
static const String defaultRouteName = '/';
......@@ -57,9 +66,24 @@ class NavigatorState extends State<Navigator> {
void initState() {
super.initState();
assert(config.observer == null || config.observer.navigator == null);
config.observer?._navigator = this;
push(config.onGenerateRoute(new NamedRouteSettings(name: Navigator.defaultRouteName)));
}
void didUpdateConfig(Navigator oldConfig) {
if (oldConfig.observer != config.observer) {
oldConfig.observer?._navigator = null;
assert(config.observer == null || config.observer.navigator == null);
config.observer?._navigator = this;
}
}
void dispose() {
config.observer?._navigator = null;
super.dispose();
}
bool get hasPreviousRoute => _modal.length > 1;
OverlayState get overlay => _overlayKey.currentState;
......@@ -75,11 +99,7 @@ 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();
}
Route get currentRoute => _ephemeral.isNotEmpty ? _ephemeral.last : _modal.isNotEmpty ? _modal.last : null;
void pushNamed(String name, { Set<Key> mostValuableKeys }) {
assert(name != null);
......@@ -90,9 +110,10 @@ class NavigatorState extends State<Navigator> {
push(config.onGenerateRoute(settings) ?? config.onUnknownRoute(settings));
}
void push(Route route) {
void push(Route route, { Set<Key> mostValuableKeys }) {
_popAllEphemeralRoutes();
route.didPush(overlay, _currentOverlay);
config.observer?.didPushModal(route);
_modal.add(route);
}
......@@ -110,7 +131,13 @@ class NavigatorState extends State<Navigator> {
}
void pop([dynamic result]) {
_removeCurrentRoute().didPop(result);
if (_ephemeral.isNotEmpty) {
_ephemeral.removeLast().didPop(result);
} else {
Route route = _modal.removeLast();
route.didPop(result);
config.observer?.didPopModal(route);
}
}
Widget build(BuildContext context) {
......
......@@ -7,9 +7,13 @@ import 'dart:async';
import 'package:flutter/animation.dart';
import 'basic.dart';
import 'focus.dart';
import 'framework.dart';
import 'modal_barrier.dart';
import 'navigator.dart';
import 'overlay.dart';
import 'page_storage.dart';
import 'status_transitions.dart';
class StateRoute extends Route {
StateRoute({ this.onPop });
......@@ -29,7 +33,7 @@ class OverlayRoute extends Route {
List<WidgetBuilder> get builders => const <WidgetBuilder>[];
List<OverlayEntry> get overlayEntries => _overlayEntries;
final List<OverlayEntry> _overlayEntries = new List<OverlayEntry>();
final List<OverlayEntry> _overlayEntries = <OverlayEntry>[];
void didPush(OverlayState overlay, OverlayEntry insertionPoint) {
for (WidgetBuilder builder in builders) {
......@@ -99,3 +103,128 @@ abstract class TransitionRoute extends OverlayRoute {
String get debugLabel => '$runtimeType';
String toString() => '$runtimeType(performance: $_performance)';
}
class _ModalScope extends StatusTransitionComponent {
_ModalScope({
Key key,
this.subtreeKey,
this.storageBucket,
PerformanceView performance,
this.route
}) : super(key: key, performance: performance);
final GlobalKey subtreeKey;
final PageStorageBucket storageBucket;
final ModalRoute route;
Widget build(BuildContext context) {
Widget contents = new PageStorage(
key: subtreeKey,
bucket: storageBucket,
child: route.buildPage(context)
);
if (route.offstage) {
contents = new OffStage(child: contents);
} else {
contents = new Focus(
key: new GlobalObjectKey(route),
child: new IgnorePointer(
ignoring: performance.status == PerformanceStatus.reverse,
child: route.buildTransition(context, performance, contents)
)
);
}
ModalPosition position = route.position;
if (position == null)
return contents;
return new Positioned(
top: position.top,
right: position.right,
bottom: position.bottom,
left: position.left,
child: contents
);
}
}
class ModalPosition {
const ModalPosition({ this.top, this.right, this.bottom, this.left });
final double top;
final double right;
final double bottom;
final double left;
}
abstract class ModalRoute extends TransitionRoute {
ModalRoute({
Completer completer,
this.settings: const NamedRouteSettings()
}) : super(completer: completer);
final NamedRouteSettings settings;
// The API for subclasses to override - used by _ModalScope
ModalPosition get position => null;
Widget buildPage(BuildContext context);
Widget buildTransition(BuildContext context, PerformanceView performance, Widget child) {
return child;
}
// The API for subclasses to override - used by this class
Color get barrierColor => kTransparent;
// The API for _ModalScope and HeroController
bool get offstage => _offstage;
bool _offstage = false;
void set offstage (bool value) {
if (_offstage == value)
return;
_offstage = value;
_scopeKey.currentState?.setState(() {
// _offstage is the value we're setting, but since there might not be a
// state, we set it outside of this callback (which will only be called if
// there's a state currently built).
// _scopeKey is the key for the _ModalScope built in _buildModalScope().
// When we mark that state dirty, it'll rebuild itself, and use our
// offstage (via their config.route.offstage) when building.
});
}
BuildContext get subtreeContext => _subtreeKey.currentContext;
// Internals
final GlobalKey<StatusTransitionState> _scopeKey = new GlobalKey<StatusTransitionState>();
final GlobalKey _subtreeKey = new GlobalKey();
final PageStorageBucket _storageBucket = new PageStorageBucket();
Widget _buildModalBarrier(BuildContext context) {
return new AnimatedModalBarrier(
color: new AnimatedColorValue(kTransparent, end: barrierColor, curve: Curves.ease),
performance: performance,
dismissable: false
);
}
Widget _buildModalScope(BuildContext context) {
return new _ModalScope(
key: _scopeKey,
subtreeKey: _subtreeKey,
storageBucket: _storageBucket,
performance: performance,
route: this
);
}
List<WidgetBuilder> get builders => <WidgetBuilder>[
_buildModalBarrier,
_buildModalScope
];
}
......@@ -18,10 +18,10 @@ abstract class StatusTransitionComponent extends StatefulComponent {
Widget build(BuildContext context);
_StatusTransitionState createState() => new _StatusTransitionState();
StatusTransitionState createState() => new StatusTransitionState();
}
class _StatusTransitionState extends State<StatusTransitionComponent> {
class StatusTransitionState extends State<StatusTransitionComponent> {
void initState() {
super.initState();
config.performance.addStatusListener(_performanceStatusChanged);
......
......@@ -27,7 +27,6 @@ export 'src/widgets/modal_barrier.dart';
export 'src/widgets/navigator.dart';
export 'src/widgets/overlay.dart';
export 'src/widgets/page_storage.dart';
export 'src/widgets/page.dart';
export 'src/widgets/placeholder.dart';
export 'src/widgets/routes.dart';
export 'src/widgets/scrollable.dart';
......
......@@ -37,8 +37,8 @@ void main() {
// Tap on the the bottom sheet itself to dismiss it
tester.tap(tester.findText('BottomSheet'));
tester.pump(); // bottom sheet dismiss animation starts
tester.pump(new Duration(seconds: 1)); // animation done
tester.pump(new Duration(seconds: 1)); // rebuild frame
tester.pump(new Duration(seconds: 1)); // last frame of animation (sheet is entirely off-screen, but still present)
tester.pump(new Duration(seconds: 1)); // frame after the animation (sheet has been removed)
expect(showBottomSheetThenCalled, isTrue);
expect(tester.findText('BottomSheet'), isNull);
......
......@@ -3,7 +3,7 @@
// found in the LICENSE file.
import 'package:flutter/animation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/material.dart';
import 'package:test/test.dart';
import 'widget_tester.dart';
......@@ -36,9 +36,9 @@ void main() {
key: navigatorKey,
onGenerateRoute: (NamedRouteSettings settings) {
if (settings.name == '/')
return new PageRoute(builder: (_) => new Container(child: new ThePositiveNumbers()));
return new MaterialPageRoute(builder: (_) => new Container(child: new ThePositiveNumbers()));
else if (settings.name == '/second')
return new PageRoute(builder: (_) => new Container(child: new ThePositiveNumbers()));
return new MaterialPageRoute(builder: (_) => new Container(child: new ThePositiveNumbers()));
return null;
}
));
......
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