Commit 6e371875 authored by Hixie's avatar Hixie

Route refactor

- Removed the concept of ephemeral routes.
- Renamed the two _MenuRoutes to _PopupMenuRoute and _DropDownRoute.
- Added type arguments in various places:
  - DropDownMenu
  - _DropDownRoute
  - _ModalBottomSheetRoute
  - PopupMenuItem
  - _PopupMenu
  - _PopupMenuRoute
- Made _ModalBottomSheetRoute, the two ex _MenuRoutes, and _DialogRoute
  all inherit from ModalRoute, via PopupRoute.
- Change "Dropdown" and "DropDown" to "DropDown" consistently.
- Made MaterialPageRoute inherit from PageRoute.
- Made ModalBarrier not create a box if it's always transparent.
- Exposed the Futures on TransitionRoutes.
- Fixed that menus were no longer dismissable by tapping the modal
  barrier.
parent de772647
......@@ -4,24 +4,22 @@
import 'package:flutter/material.dart';
class DropdownDemo extends StatefulComponent {
DropdownDemo();
DropdownDemoState createState() => new DropdownDemoState();
class DropDownDemo extends StatefulComponent {
DropDownDemoState createState() => new DropDownDemoState();
}
class DropdownDemoState extends State<DropdownDemo> {
class DropDownDemoState extends State<DropDownDemo> {
String _value = "Free";
List<DropdownMenuItem> _buildItems() {
List<DropDownMenuItem<String>> _buildItems() {
return ["One", "Two", "Free", "Four"].map((String value) {
return new DropdownMenuItem<String>(value: value, child: new Text(value));
return new DropDownMenuItem<String>(value: value, child: new Text(value));
})
.toList();
}
Widget build(BuildContext context) {
Widget dropdown = new DropdownButton<String>(
Widget dropdown = new DropDownButton<String>(
items: _buildItems(),
value: _value,
onChanged: (String newValue) {
......@@ -33,7 +31,7 @@ class DropdownDemoState extends State<DropdownDemo> {
);
return new Scaffold(
toolBar: new ToolBar(center: new Text('DropdownDemo Demo')),
toolBar: new ToolBar(center: new Text('DropDownDemo Demo')),
body: new Container(
decoration: new BoxDecoration(backgroundColor: Theme.of(context).primarySwatch[50]),
child: new Center(child: dropdown)
......@@ -44,14 +42,14 @@ class DropdownDemoState extends State<DropdownDemo> {
void main() {
runApp(new MaterialApp(
title: 'DropdownDemo',
title: 'DropDownDemo',
theme: new ThemeData(
brightness: ThemeBrightness.light,
primarySwatch: Colors.blue,
accentColor: Colors.redAccent[200]
),
routes: <String, RouteBuilder>{
'/': (RouteArguments args) => new DropdownDemo(),
'/': (RouteArguments args) => new DropDownDemo(),
}
));
}
......@@ -140,55 +140,32 @@ class _ModalBottomSheetState extends State<_ModalBottomSheet> {
}
}
class _ModalBottomSheetRoute extends OverlayRoute {
_ModalBottomSheetRoute({ this.completer, this.builder });
class _ModalBottomSheetRoute<T> extends PopupRoute<T> {
_ModalBottomSheetRoute({
Completer<T> completer,
this.builder
}) : super(completer: completer);
final Completer completer;
final WidgetBuilder builder;
Performance performance;
void didPush(OverlayState overlay, OverlayEntry insertionPoint) {
performance = BottomSheet.createPerformance()
..forward();
super.didPush(overlay, insertionPoint);
}
void didPop(dynamic result) {
completer.complete(result);
if (performance.isDismissed)
finished();
else
performance.reverse().then((_) { finished(); });
}
Duration get transitionDuration => _kBottomSheetDuration;
bool get barrierDismissable => true;
Color get barrierColor => Colors.black54;
Widget _buildModalBarrier(BuildContext context) {
return new AnimatedModalBarrier(
color: new AnimatedColorValue(_kTransparent, end: _kBarrierColor, curve: Curves.ease),
performance: performance
);
Performance createPerformance() {
return BottomSheet.createPerformance();
}
Widget _buildBottomSheet(BuildContext context) {
return new Focus(
key: new GlobalObjectKey(this),
child: new _ModalBottomSheet(route: this)
);
Widget buildPage(BuildContext context) {
return new _ModalBottomSheet(route: this);
}
List<WidgetBuilder> get builders => <WidgetBuilder>[
_buildModalBarrier,
_buildBottomSheet,
];
String get debugLabel => '$runtimeType';
String toString() => '$runtimeType(performance: $performance)';
}
Future showModalBottomSheet({ BuildContext context, WidgetBuilder builder }) {
assert(context != null);
assert(builder != null);
final Completer completer = new Completer();
Navigator.of(context).pushEphemeral(new _ModalBottomSheetRoute(
Navigator.of(context).push(new _ModalBottomSheetRoute(
completer: completer,
builder: builder
));
......
......@@ -115,13 +115,16 @@ class Dialog extends StatelessComponent {
}
}
class _DialogRoute<T> extends ModalRoute<T> {
_DialogRoute({ Completer<T> completer, this.child }) : super(completer: completer);
class _DialogRoute<T> extends PopupRoute<T> {
_DialogRoute({
Completer<T> completer,
this.child
}) : super(completer: completer);
final Widget child;
bool get opaque => false;
Duration get transitionDuration => const Duration(milliseconds: 150);
bool get barrierDismissable => true;
Color get barrierColor => Colors.black54;
Widget buildPage(BuildContext context) => child;
......
......@@ -14,14 +14,14 @@ import 'ink_well.dart';
import 'shadows.dart';
import 'theme.dart';
const Duration _kMenuDuration = const Duration(milliseconds: 300);
const Duration _kDropDownMenuDuration = const Duration(milliseconds: 300);
const double _kMenuItemHeight = 48.0;
const EdgeDims _kMenuHorizontalPadding = const EdgeDims.only(left: 36.0, right: 36.0);
const double _kBaselineOffsetFromBottom = 20.0;
const Border _kDropdownUnderline = const Border(bottom: const BorderSide(color: const Color(0xFFBDBDBD), width: 2.0));
const Border _kDropDownUnderline = const Border(bottom: const BorderSide(color: const Color(0xFFBDBDBD), width: 2.0));
class _DropdownMenuPainter extends CustomPainter {
const _DropdownMenuPainter({
class _DropDownMenuPainter extends CustomPainter {
const _DropDownMenuPainter({
this.color,
this.elevation,
this.menuTop,
......@@ -47,7 +47,7 @@ class _DropdownMenuPainter extends CustomPainter {
painter.paint(canvas, new Rect.fromLTRB(0.0, top, size.width, bottom));
}
bool shouldRepaint(_DropdownMenuPainter oldPainter) {
bool shouldRepaint(_DropDownMenuPainter oldPainter) {
return oldPainter.color != color
|| oldPainter.elevation != elevation
|| oldPainter.menuTop != menuTop
......@@ -56,23 +56,23 @@ class _DropdownMenuPainter extends CustomPainter {
}
}
class _DropdownMenu extends StatusTransitionComponent {
_DropdownMenu({
class _DropDownMenu<T> extends StatusTransitionComponent {
_DropDownMenu({
Key key,
_MenuRoute route
_DropDownRoute<T> route
}) : route = route, super(key: key, performance: route.performance);
final _MenuRoute route;
final _DropDownRoute<T> route;
Widget build(BuildContext context) {
// The menu is shown in three stages (unit timing in brackets):
// [0 - 0.25] - Fade in a rect-sized menu container with the selected item.
// [0.25 - 0.5] - Grow the otherwise empty menu container from the center
// [0s - 0.25s] - Fade in a rect-sized menu container with the selected item.
// [0.25s - 0.5s] - Grow the otherwise empty menu container from the center
// until it's big enough for as many items as we're going to show.
// [0.5 - 1.0] Fade in the remaining visible items from top to bottom.
// [0.5s - 1.0s] Fade in the remaining visible items from top to bottom.
//
// When the menu is dismissed we just fade the entire thing out
// in the first 0.25.
// in the first 0.25s.
final double unit = 0.5 / (route.items.length + 1.5);
final List<Widget> children = <Widget>[];
......@@ -135,7 +135,7 @@ class _DropdownMenu extends StatusTransitionComponent {
variables: <AnimatedValue<double>>[menuTop, menuBottom],
builder: (BuildContext context) {
return new CustomPaint(
painter: new _DropdownMenuPainter(
painter: new _DropDownMenuPainter(
color: Theme.of(context).canvasColor,
elevation: route.elevation,
menuTop: menuTop.value,
......@@ -152,38 +152,29 @@ class _DropdownMenu extends StatusTransitionComponent {
}
}
// TODO(abarth): This should use ModalRoute.
class _MenuRoute extends TransitionRoute {
_MenuRoute({
this.completer,
class _DropDownRoute<T> extends PopupRoute<T> {
_DropDownRoute({
Completer<T> completer,
this.items,
this.selectedIndex,
this.rect,
this.elevation: 8
});
}) : super(completer: completer);
final Completer completer;
final List<DropDownMenuItem<T>> items;
final int selectedIndex;
final Rect rect;
final List<DropdownMenuItem> items;
final int elevation;
final int selectedIndex;
bool get opaque => false;
Duration get transitionDuration => _kMenuDuration;
Widget _buildModalBarrier(BuildContext context) => new ModalBarrier();
Widget _buildDropDownMenu(BuildContext context) => new _DropdownMenu(route: this);
Duration get transitionDuration => _kDropDownMenuDuration;
bool get barrierDismissable => true;
Color get barrierColor => null;
List<WidgetBuilder> get builders => <WidgetBuilder>[ _buildModalBarrier, _buildDropDownMenu ];
void didPop([dynamic result]) {
completer.complete(result);
super.didPop(result);
}
Widget buildPage(BuildContext context) => new _DropDownMenu(route: this);
}
class DropdownMenuItem<T> extends StatelessComponent {
DropdownMenuItem({
class DropDownMenuItem<T> extends StatelessComponent {
DropDownMenuItem({
Key key,
this.value,
this.child
......@@ -207,8 +198,8 @@ class DropdownMenuItem<T> extends StatelessComponent {
}
}
class DropdownButton<T> extends StatelessComponent {
DropdownButton({
class DropDownButton<T> extends StatelessComponent {
DropDownButton({
Key key,
this.items,
this.value,
......@@ -216,16 +207,16 @@ class DropdownButton<T> extends StatelessComponent {
this.elevation: 8
}) : super(key: key);
final List<DropdownMenuItem<T>> items;
final List<DropDownMenuItem<T>> items;
final T value;
final ValueChanged<T> onChanged;
final int elevation;
void _showDropdown(BuildContext context, int selectedIndex, GlobalKey indexedStackKey) {
void _showDropDown(BuildContext context, int selectedIndex, GlobalKey indexedStackKey) {
final RenderBox renderBox = indexedStackKey.currentContext.findRenderObject();
final Rect rect = renderBox.localToGlobal(Point.origin) & renderBox.size;
final Completer completer = new Completer();
Navigator.of(context).pushEphemeral(new _MenuRoute(
final Completer completer = new Completer<T>();
Navigator.of(context).push(new _DropDownRoute<T>(
completer: completer,
items: items,
selectedIndex: selectedIndex,
......@@ -239,7 +230,7 @@ class DropdownButton<T> extends StatelessComponent {
}
Widget build(BuildContext context) {
GlobalKey indexedStackKey = new GlobalKey(debugLabel: 'DropdownButton.IndexedStack');
GlobalKey indexedStackKey = new GlobalKey(debugLabel: 'DropDownButton.IndexedStack');
int selectedIndex = 0;
for (int itemIndex = 0; itemIndex < items.length; itemIndex++) {
if (items[itemIndex].value == value) {
......@@ -247,12 +238,12 @@ class DropdownButton<T> extends StatelessComponent {
break;
}
}
return new GestureDetector(
child: new Container(
decoration: new BoxDecoration(border: _kDropdownUnderline),
decoration: new BoxDecoration(border: _kDropDownUnderline),
child: new Row(<Widget>[
new IndexedStack(items,
new IndexedStack(
items,
key: indexedStackKey,
index: selectedIndex,
alignment: const FractionalOffset(0.5, 0.0)
......@@ -266,7 +257,7 @@ class DropdownButton<T> extends StatelessComponent {
)
),
onTap: () {
_showDropdown(context, selectedIndex, indexedStackKey);
_showDropDown(context, selectedIndex, indexedStackKey);
}
);
}
......
......@@ -5,6 +5,8 @@
import 'package:flutter/animation.dart';
import 'package:flutter/widgets.dart';
import 'colors.dart';
class _MaterialPageTransition extends TransitionWithChild {
_MaterialPageTransition({
Key key,
......@@ -36,7 +38,7 @@ class _MaterialPageTransition extends TransitionWithChild {
}
}
class MaterialPageRoute<T> extends ModalRoute<T> {
class MaterialPageRoute<T> extends PageRoute<T> {
MaterialPageRoute({
this.builder,
NamedRouteSettings settings: const NamedRouteSettings()
......@@ -48,8 +50,8 @@ class MaterialPageRoute<T> extends ModalRoute<T> {
final WidgetBuilder builder;
Duration get transitionDuration => const Duration(milliseconds: 150);
bool get opaque => true;
bool get barrierDismissable => false;
Color get barrierColor => Colors.black54;
Widget buildPage(BuildContext context) {
Widget result = builder(context);
......
......@@ -53,13 +53,13 @@ class _PopupMenuPainter extends CustomPainter {
}
}
class _PopupMenu extends StatelessComponent {
class _PopupMenu<T> extends StatelessComponent {
_PopupMenu({
Key key,
this.route
}) : super(key: key);
final _MenuRoute route;
final _PopupMenuRoute<T> route;
Widget build(BuildContext context) {
double unit = 1.0 / (route.items.length + 1.5); // 1.0 for the width and 0.5 for the last item's fade.
......@@ -118,11 +118,16 @@ class _PopupMenu extends StatelessComponent {
}
}
class _MenuRoute extends ModalRoute {
_MenuRoute({ Completer completer, this.position, this.items, this.elevation }) : super(completer: completer);
class _PopupMenuRoute<T> extends PopupRoute<T> {
_PopupMenuRoute({
Completer<T> completer,
this.position,
this.items,
this.elevation
}) : super(completer: completer);
final ModalPosition position;
final List<PopupMenuItem> items;
final List<PopupMenuItem<T>> items;
final int elevation;
Performance createPerformance() {
......@@ -133,15 +138,16 @@ class _MenuRoute extends ModalRoute {
return result;
}
bool get opaque => false;
Duration get transitionDuration => _kMenuDuration;
bool get barrierDismissable => true;
Color get barrierColor => null;
Widget buildPage(BuildContext context) => new _PopupMenu(route: this);
}
Future showMenu({ BuildContext context, ModalPosition position, List<PopupMenuItem> items, int elevation: 8 }) {
Completer completer = new Completer();
Navigator.of(context).pushEphemeral(new _MenuRoute(
Navigator.of(context).push(new _PopupMenuRoute(
completer: completer,
position: position,
items: items,
......
......@@ -9,7 +9,7 @@ import 'theme.dart';
const double _kMenuItemHeight = 48.0;
const double _kBaselineOffsetFromBottom = 20.0;
class PopupMenuItem extends StatelessComponent {
class PopupMenuItem<T> extends StatelessComponent {
PopupMenuItem({
Key key,
this.value,
......@@ -17,7 +17,7 @@ class PopupMenuItem extends StatelessComponent {
}) : super(key: key);
final Widget child;
final dynamic value;
final T value;
Widget build(BuildContext context) {
return new Container(
......
......@@ -12,7 +12,7 @@ import 'transitions.dart';
// Heroes are the parts of an application's screen-to-screen transitions where a
// component from one screen shifts to a position on the other. For example,
// album art from a list of albums growing to become the centerpiece of the
// album's details view. In this context, a screen is a navigator Route.
// album's details view. In this context, a screen is a navigator ModalRoute.
// To get this effect, all you have to do is wrap each hero on each route with a
// Hero widget, and give each hero a tag. Tag must either be unique within the
......@@ -50,7 +50,7 @@ import 'transitions.dart';
// TODO(ianh): If the widgets use Inherited properties, they are taken from the
// Navigator's position in the widget hierarchy, not the source or target. We
// should interpolate the inherited properties from their value at the source to
// their value at the target. See: https://github.com/flutter/engine/issues/1698
// their value at the target. See: https://github.com/flutter/flutter/issues/213
final Object centerOfAttentionHeroTag = new Object();
......
......@@ -9,12 +9,10 @@ import 'framework.dart';
import 'navigator.dart';
import 'transitions.dart';
const Color kTransparent = const Color(0x00000000);
class ModalBarrier extends StatelessComponent {
ModalBarrier({
Key key,
this.color: kTransparent,
this.color,
this.dismissable: true
}) : super(key: key);
......@@ -27,9 +25,10 @@ class ModalBarrier extends StatelessComponent {
if (dismissable)
Navigator.of(context).pop();
},
behavior: HitTestBehavior.opaque,
child: new ConstrainedBox(
constraints: const BoxConstraints.expand(),
child: new DecoratedBox(
child: color == null ? null : new DecoratedBox(
decoration: new BoxDecoration(
backgroundColor: color
)
......
......@@ -61,7 +61,7 @@ class Navigator extends StatefulComponent {
class NavigatorState extends State<Navigator> {
final GlobalKey<OverlayState> _overlayKey = new GlobalKey<OverlayState>();
final List<Route> _ephemeral = new List<Route>();
// TODO(ianh): Rename _modal to _history or some such
final List<Route> _modal = new List<Route>();
void initState() {
......@@ -88,10 +88,6 @@ class NavigatorState extends State<Navigator> {
OverlayState get overlay => _overlayKey.currentState;
OverlayEntry get _currentOverlay {
for (Route route in _ephemeral.reversed) {
if (route.overlayEntries.isNotEmpty)
return route.overlayEntries.last;
}
for (Route route in _modal.reversed) {
if (route.overlayEntries.isNotEmpty)
return route.overlayEntries.last;
......@@ -110,7 +106,6 @@ class NavigatorState extends State<Navigator> {
void push(Route route, { Set<Key> mostValuableKeys }) {
setState(() {
_popAllEphemeralRoutes();
int index = _modal.length-1;
while (index >= 0 && _modal[index].willPushNext(route))
index -= 1;
......@@ -120,19 +115,6 @@ class NavigatorState extends State<Navigator> {
});
}
void pushEphemeral(Route route) {
route.didPush(overlay, _currentOverlay);
_ephemeral.add(route);
}
void _popAllEphemeralRoutes() {
List<Route> localEphemeral = new List<Route>.from(_ephemeral);
_ephemeral.clear();
for (Route route in localEphemeral)
route.didPop(null);
assert(_ephemeral.isEmpty);
}
/// Pops the given route, if it's the current route. If it's not the current
/// route, removes it from the list of active routes without notifying any
/// observers or adjacent routes.
......@@ -167,21 +149,18 @@ class NavigatorState extends State<Navigator> {
// We use setState to guarantee that we'll rebuild, since the routes can't
// do that for themselves, even if they have changed their own state (e.g.
// ModalScope.isCurrent).
if (_ephemeral.isNotEmpty) {
_ephemeral.removeLast().didPop(result);
} else {
assert(_modal.length > 1);
Route route = _modal.removeLast();
route.didPop(result);
int index = _modal.length-1;
while (index >= 0 && _modal[index].didPopNext(route))
index -= 1;
config.observer?.didPopModal(route, index >= 0 ? _modal[index] : null);
}
assert(_modal.length > 1);
Route route = _modal.removeLast();
route.didPop(result);
int index = _modal.length-1;
while (index >= 0 && _modal[index].didPopNext(route))
index -= 1;
config.observer?.didPopModal(route, index >= 0 ? _modal[index] : null);
});
}
Widget build(BuildContext context) {
assert(_modal.isNotEmpty);
return new Overlay(
key: _overlayKey,
initialEntries: _modal.first.overlayEntries
......
......@@ -15,6 +15,8 @@ import 'overlay.dart';
import 'page_storage.dart';
import 'status_transitions.dart';
const _kTransparent = const Color(0x00000000);
class StateRoute extends Route {
StateRoute({ this.onPop });
......@@ -72,7 +74,13 @@ abstract class TransitionRoute<T> extends OverlayRoute<T> {
}) : _popCompleter = popCompleter,
_transitionCompleter = transitionCompleter;
/// This future completes once the animation has been dismissed. For
/// ModalRoutes, this will be after the completer that's passed in, since that
/// one completes before the animation even starts, as soon as the route is
/// popped.
Future<T> get popped => _popCompleter?.future;
final Completer<T> _popCompleter;
Future<T> get completed => _transitionCompleter?.future;
final Completer<T> _transitionCompleter;
Duration get transitionDuration;
......@@ -101,7 +109,9 @@ abstract class TransitionRoute<T> extends OverlayRoute<T> {
overlayEntries.first.opaque = false;
break;
case PerformanceStatus.dismissed:
super.finished(); // clear the overlays
assert(!overlayEntries.first.opaque);
finished(); // clear the overlays
assert(overlayEntries.isEmpty);
break;
}
}
......@@ -239,9 +249,14 @@ abstract class ModalRoute<T> extends TransitionRoute<T> {
return child;
}
// The API for subclasses to override - used by this class
Color get barrierColor => kTransparent;
/// Whether you can dismiss this route by tapping the modal barrier.
bool get barrierDismissable;
/// The color to use for the modal barrier. If this is null, the barrier will
/// be transparent.
Color get barrierColor;
// The API for _ModalScope and HeroController
......@@ -300,11 +315,16 @@ abstract class ModalRoute<T> extends TransitionRoute<T> {
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
);
if (barrierColor != null) {
assert(barrierColor != _kTransparent);
return new AnimatedModalBarrier(
color: new AnimatedColorValue(_kTransparent, end: barrierColor, curve: Curves.ease),
performance: performance,
dismissable: barrierDismissable
);
} else {
return new ModalBarrier(dismissable: barrierDismissable);
}
}
Widget _buildModalScope(BuildContext context) {
......@@ -315,6 +335,7 @@ abstract class ModalRoute<T> extends TransitionRoute<T> {
performance: performance,
current: isCurrent,
route: this
// calls buildTransition() and buildPage(), defined above
);
}
......@@ -324,3 +345,18 @@ abstract class ModalRoute<T> extends TransitionRoute<T> {
];
}
/// A modal route that overlays a widget over the current route.
abstract class PopupRoute<T> extends ModalRoute<T> {
PopupRoute({ Completer<T> completer }) : super(completer: completer);
bool get opaque => false;
}
/// A modal route that replaces the entire screen.
abstract class PageRoute<T> extends ModalRoute<T> {
PageRoute({
Completer<T> completer,
NamedRouteSettings settings: const NamedRouteSettings()
}) : super(completer: completer, settings: settings);
bool get opaque => true;
}
\ No newline at end of file
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