Introduce a showPopupMenu() function

Instead of having to manage the popup menu from your app's build
function, you now just call showPopupMenu() with the menu's position and
it takes care of everything for you.

This solves the problem that the popup menu was trying to mutate the
state of the navigator from within its own initState() function.

Also, remove the "route" argument to RouteBase.build() since it equals
"this" by definition...

Also, remove ModalOverlay, and instead put that logic in the navigator.
This commit is contained in:
Hixie 2015-09-14 13:35:05 -07:00
parent f15b948060
commit a3ae46b97e
9 changed files with 176 additions and 165 deletions

View file

@ -4,6 +4,7 @@
library stocks;
import 'dart:async';
import 'dart:math' as math;
import 'dart:sky' as sky;

View file

@ -74,28 +74,6 @@ class StockHome extends StatefulComponent {
});
}
bool _menuShowing = false;
AnimationStatus _menuStatus = AnimationStatus.dismissed;
void _handleMenuShow() {
setState(() {
_menuShowing = true;
_menuStatus = AnimationStatus.forward;
});
}
void _handleMenuHide() {
setState(() {
_menuShowing = false;
});
}
void _handleMenuDismissed() {
setState(() {
_menuStatus = AnimationStatus.dismissed;
});
}
bool _autorefresh = false;
void _handleAutorefreshChanged(bool value) {
setState(() {
@ -112,6 +90,13 @@ class StockHome extends StatefulComponent {
return EventDisposition.processed;
}
void _handleMenuShow() {
showStockMenu(navigator,
autorefresh: _autorefresh,
onAutorefreshChanged: _handleAutorefreshChanged
);
}
Drawer buildDrawer() {
if (_drawerStatus == AnimationStatus.dismissed)
return null;
@ -282,31 +267,13 @@ class StockHome extends StatefulComponent {
);
}
void addMenuToOverlays(List<Widget> overlays) {
if (_menuStatus == AnimationStatus.dismissed)
return;
overlays.add(new ModalOverlay(
children: [new StockMenu(
showing: _menuShowing,
onDismissed: _handleMenuDismissed,
navigator: navigator,
autorefresh: _autorefresh,
onAutorefreshChanged: _handleAutorefreshChanged
)],
onDismiss: _handleMenuHide));
}
Widget build() {
List<Widget> overlays = [
new Scaffold(
toolbar: _isSearching ? buildSearchBar() : buildToolBar(),
body: buildTabNavigator(),
snackBar: buildSnackBar(),
floatingActionButton: buildFloatingActionButton(),
drawer: buildDrawer()
),
];
addMenuToOverlays(overlays);
return new Stack(overlays);
return new Scaffold(
toolbar: _isSearching ? buildSearchBar() : buildToolBar(),
body: buildTabNavigator(),
snackBar: buildSnackBar(),
floatingActionButton: buildFloatingActionButton(),
drawer: buildDrawer()
);
}
}

View file

@ -4,45 +4,29 @@
part of stocks;
class StockMenu extends Component {
StockMenu({
Key key,
this.showing,
this.onDismissed,
this.navigator,
this.autorefresh: false,
this.onAutorefreshChanged
}) : super(key: key);
final bool showing;
final PopupMenuDismissedCallback onDismissed;
final Navigator navigator;
final bool autorefresh;
final ValueChanged onAutorefreshChanged;
Widget build() {
var checkbox = new Checkbox(
value: this.autorefresh,
onChanged: this.onAutorefreshChanged
);
return new Positioned(
child: new PopupMenu(
items: [
new PopupMenuItem(child: new Text('Add stock')),
new PopupMenuItem(child: new Text('Remove stock')),
new PopupMenuItem(
onPressed: () => onAutorefreshChanged(!autorefresh),
child: new Row([new Flexible(child: new Text('Autorefresh')), checkbox])
),
],
level: 4,
showing: showing,
onDismissed: onDismissed,
navigator: navigator
),
Future showStockMenu(Navigator navigator, { bool autorefresh, ValueChanged onAutorefreshChanged }) {
return showMenu(
navigator: navigator,
position: new MenuPosition(
right: sky.view.paddingRight,
top: sky.view.paddingTop
);
}
}
),
builder: (Navigator navigator) {
return <PopupMenuItem>[
new PopupMenuItem(child: new Text('Add stock')),
new PopupMenuItem(child: new Text('Remove stock')),
new PopupMenuItem(
onPressed: () => onAutorefreshChanged(!autorefresh),
child: new Row([
new Flexible(child: new Text('Autorefresh')),
new Checkbox(
value: autorefresh,
onChanged: onAutorefreshChanged
)
]
)
),
];
}
);
}

View file

@ -141,11 +141,11 @@ class DialogRoute extends RouteBase {
Duration get transitionDuration => _kTransitionDuration;
bool get isOpaque => false;
Widget build(Key key, Navigator navigator, RouteBase route, WatchableAnimationPerformance performance) {
Widget build(Key key, Navigator navigator, WatchableAnimationPerformance performance) {
return new FadeTransition(
performance: performance,
opacity: new AnimatedValue<double>(0.0, end: 1.0, curve: easeOut),
child: builder(navigator, route)
child: builder(navigator, this)
);
}

View file

@ -1,23 +0,0 @@
// 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:sky/src/widgets/basic.dart';
import 'package:sky/src/widgets/framework.dart';
import 'package:sky/src/widgets/gesture_detector.dart';
class ModalOverlay extends Component {
ModalOverlay({ Key key, this.children, this.onDismiss }) : super(key: key);
final List<Widget> children;
final Function onDismiss;
Widget build() {
return new GestureDetector(
onTap: onDismiss,
child: new Stack(children)
);
}
}

View file

@ -49,7 +49,7 @@ abstract class RouteBase {
Duration get transitionDuration;
bool get isOpaque;
Widget build(Key key, Navigator navigator, RouteBase route, WatchableAnimationPerformance performance);
Widget build(Key key, Navigator navigator, WatchableAnimationPerformance performance);
void popState([dynamic result]) { assert(result == null); }
String toString() => '$runtimeType()';
@ -67,7 +67,7 @@ class Route extends RouteBase {
Duration get transitionDuration => _kTransitionDuration;
Widget build(Key key, Navigator navigator, RouteBase route, WatchableAnimationPerformance performance) {
Widget build(Key key, Navigator navigator, WatchableAnimationPerformance performance) {
// TODO(jackson): Hit testing should ignore transform
// TODO(jackson): Block input unless content is interactive
return new SlideTransition(
@ -77,7 +77,7 @@ class Route extends RouteBase {
child: new FadeTransition(
performance: performance,
opacity: new AnimatedValue<double>(0.0, end: 1.0, curve: easeOut),
child: builder(navigator, route)
child: builder(navigator, this)
)
);
}
@ -102,7 +102,7 @@ class RouteState extends RouteBase {
bool get hasContent => false;
Duration get transitionDuration => const Duration();
Widget build(Key key, Navigator navigator, RouteBase route, WatchableAnimationPerformance performance) => null;
Widget build(Key key, Navigator navigator, WatchableAnimationPerformance performance) => null;
}
class NavigationState {
@ -202,11 +202,17 @@ class Navigator extends StatefulComponent {
});
};
Key key = new ObjectKey(route);
Widget widget = route.build(key, this, route, performance);
Widget widget = route.build(key, this, performance);
visibleRoutes.add(widget);
if (route.isActuallyOpaque)
break;
}
if (visibleRoutes.length > 1) {
visibleRoutes.insert(1, new Listener(
onPointerDown: (_) { pop(); return EventDisposition.consumed; },
child: new Container()
));
}
return new Focus(child: new Stack(visibleRoutes.reversed.toList()));
}
}

View file

@ -2,12 +2,14 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:sky' as sky;
import 'package:sky/animation.dart';
import 'package:sky/painting.dart';
import 'package:sky/material.dart';
import 'package:sky/src/widgets/basic.dart';
import 'package:sky/src/widgets/focus.dart';
import 'package:sky/src/widgets/framework.dart';
import 'package:sky/src/widgets/navigator.dart';
import 'package:sky/src/widgets/popup_menu_item.dart';
@ -23,80 +25,75 @@ const double _kMenuMaxWidth = 5.0 * _kMenuWidthStep;
const double _kMenuHorizontalPadding = 16.0;
const double _kMenuVerticalPadding = 8.0;
typedef void PopupMenuDismissedCallback();
class PopupMenu extends StatefulComponent {
PopupMenu({
Key key,
this.showing,
this.onDismissed,
this.items,
this.level,
this.navigator
}) : super(key: key);
this.level: 4,
this.navigator,
this.performance
}) : super(key: key) {
assert(items != null);
assert(performance != null);
}
bool showing;
PopupMenuDismissedCallback onDismissed;
List<PopupMenuItem> items;
int level;
Navigator navigator;
WatchableAnimationPerformance performance;
AnimationPerformance _performance;
BoxPainter _painter;
void initState() {
_performance = new AnimationPerformance(duration: _kMenuDuration);
_performance.timing = new AnimationTiming()
..reverseInterval = new Interval(0.0, _kMenuCloseIntervalEnd);
_performance.addStatusListener((AnimationStatus status) {
if (status == AnimationStatus.dismissed)
_handleDismissed();
});
_updateBoxPainter();
}
if (showing)
_open();
void _updateBoxPainter() {
_painter = new BoxPainter(
new BoxDecoration(
backgroundColor: Colors.grey[50],
borderRadius: 2.0,
boxShadow: shadows[level]
)
);
}
void syncConstructorArguments(PopupMenu source) {
if (!showing && source.showing)
_open();
showing = source.showing;
items = source.items;
if (level != source.level) {
level = source.level;
_updateBoxPainter();
}
items = source.items;
navigator = source.navigator;
if (mounted)
performance.removeListener(_performanceChanged);
performance = source.performance;
if (mounted)
performance.addListener(_performanceChanged);
}
void _open() {
navigator.pushState(this, (_) => _close());
_performance.play();
void didMount() {
performance.addListener(_performanceChanged);
super.didMount();
}
void _close() {
_performance.reverse();
void didUnmount() {
performance.removeListener(_performanceChanged);
super.didMount();
}
void _updateBoxPainter() {
_painter = new BoxPainter(new BoxDecoration(
backgroundColor: Colors.grey[50],
borderRadius: 2.0,
boxShadow: shadows[level]));
void _performanceChanged() {
setState(() {
// the performance changed, and our state is tied up with the performance
});
}
void _handleDismissed() {
if (navigator != null &&
navigator.currentRoute is RouteState &&
(navigator.currentRoute as RouteState).owner == this) // TODO(ianh): remove cast once analyzer is cleverer
navigator.pop();
if (onDismissed != null)
onDismissed();
void itemPressed(PopupMenuItem item) {
if (navigator != null)
navigator.pop(item.value);
}
BoxPainter _painter;
Widget build() {
double unit = 1.0 / (items.length + 1.5); // 1.0 for the width and 0.5 for the last item's fade.
List<Widget> children = [];
@ -104,21 +101,20 @@ class PopupMenu extends StatefulComponent {
double start = (i + 1) * unit;
double end = (start + 1.5 * unit).clamp(0.0, 1.0);
children.add(new FadeTransition(
performance: _performance.view,
performance: performance,
opacity: new AnimatedValue<double>(0.0, end: 1.0, interval: new Interval(start, end)),
child: items[i])
);
}
final width = new AnimatedValue<double>(0.0, end: 1.0, interval: new Interval(0.0, unit));
final height = new AnimatedValue<double>(0.0, end: 1.0, interval: new Interval(0.0, unit * items.length));
return new FadeTransition(
performance: _performance.view,
performance: performance,
opacity: new AnimatedValue<double>(0.0, end: 1.0, interval: new Interval(0.0, 1.0 / 3.0)),
child: new Container(
margin: new EdgeDims.all(_kMenuMargin),
child: new BuilderTransition(
performance: _performance.view,
performance: performance,
variables: [width, height],
builder: () {
return new CustomPaint(
@ -153,3 +149,67 @@ class PopupMenu extends StatefulComponent {
}
}
class MenuPosition {
const MenuPosition({ this.top, this.right, this.bottom, this.left });
final double top;
final double right;
final double bottom;
final double left;
}
class MenuRoute extends RouteBase {
MenuRoute({ this.completer, this.position, this.builder, this.level });
final Completer completer;
final MenuPosition position;
final PopupMenuItemsBuilder builder;
final int level;
AnimationPerformance createPerformance() {
AnimationPerformance result = super.createPerformance();
AnimationTiming timing = new AnimationTiming();
timing.reverseInterval = new Interval(0.0, _kMenuCloseIntervalEnd);
result.timing = timing;
return result;
}
Duration get transitionDuration => _kMenuDuration;
bool get isOpaque => false;
Widget build(Key key, Navigator navigator, WatchableAnimationPerformance performance) {
return new Positioned(
top: position?.top,
right: position?.right,
bottom: position?.bottom,
left: position?.left,
child: new Focus(
key: new GlobalObjectKey(this),
autofocus: true,
child: new PopupMenu(
key: key,
items: builder != null ? builder(navigator) : const <PopupMenuItem>[],
level: level,
navigator: navigator,
performance: performance
)
)
);
}
void popState([dynamic result]) {
completer.complete(result);
}
}
typedef List<PopupMenuItem> PopupMenuItemsBuilder(Navigator navigator);
Future showMenu({ Navigator navigator, MenuPosition position, PopupMenuItemsBuilder builder, int level: 4 }) {
Completer completer = new Completer();
navigator.push(new MenuRoute(
completer: completer,
position: position,
builder: builder,
level: level
));
return completer.future;
}

View file

@ -8,6 +8,7 @@ import 'package:sky/src/widgets/default_text_style.dart';
import 'package:sky/src/widgets/framework.dart';
import 'package:sky/src/widgets/gesture_detector.dart';
import 'package:sky/src/widgets/ink_well.dart';
import 'package:sky/src/widgets/popup_menu.dart';
import 'package:sky/src/widgets/theme.dart';
const double _kMenuItemHeight = 48.0;
@ -17,17 +18,33 @@ class PopupMenuItem extends Component {
PopupMenuItem({
Key key,
this.onPressed,
this.value,
this.child
}) : super(key: key);
final Widget child;
final Function onPressed;
final dynamic value;
TextStyle get textStyle => Theme.of(this).text.subhead;
PopupMenu findAncestorPopupMenu() {
Widget ancestor = parent;
while (ancestor != null && ancestor is! PopupMenu)
ancestor = ancestor.parent;
return ancestor;
}
void handlePressed() {
if (onPressed != null)
onPressed();
PopupMenu menu = findAncestorPopupMenu();
menu?.itemPressed(this);
}
Widget build() {
return new GestureDetector(
onTap: onPressed,
onTap: handlePressed,
child: new InkWell(
child: new Container(
height: _kMenuItemHeight,

View file

@ -36,7 +36,6 @@ export 'package:sky/src/widgets/material.dart';
export 'package:sky/src/widgets/material_button.dart';
export 'package:sky/src/widgets/mimic.dart';
export 'package:sky/src/widgets/mixed_viewport.dart';
export 'package:sky/src/widgets/modal_overlay.dart';
export 'package:sky/src/widgets/navigator.dart';
export 'package:sky/src/widgets/popup_menu.dart';
export 'package:sky/src/widgets/popup_menu_item.dart';