Commit 5734821f authored by Ian Hickson's avatar Ian Hickson

Merge pull request #479 from Hixie/yak3-snackbar-PENDING

Snackbar Refactor
parents 67b2ee3a 954713ab
...@@ -54,7 +54,6 @@ class FeedFragment extends StatefulComponent { ...@@ -54,7 +54,6 @@ class FeedFragment extends StatefulComponent {
} }
class FeedFragmentState extends State<FeedFragment> { class FeedFragmentState extends State<FeedFragment> {
final GlobalKey<PlaceholderState> _snackBarPlaceholderKey = new GlobalKey<PlaceholderState>();
FitnessMode _fitnessMode = FitnessMode.feed; FitnessMode _fitnessMode = FitnessMode.feed;
void _handleFitnessModeChange(FitnessMode value) { void _handleFitnessModeChange(FitnessMode value) {
...@@ -115,15 +114,14 @@ class FeedFragmentState extends State<FeedFragment> { ...@@ -115,15 +114,14 @@ class FeedFragmentState extends State<FeedFragment> {
void _handleItemDismissed(FitnessItem item) { void _handleItemDismissed(FitnessItem item) {
config.onItemDeleted(item); config.onItemDeleted(item);
showSnackBar( Scaffold.of(context).showSnackBar(new SnackBar(
context: context,
placeholderKey: _snackBarPlaceholderKey,
content: new Text("Item deleted."), content: new Text("Item deleted."),
actions: <SnackBarAction>[new SnackBarAction(label: "UNDO", onPressed: () { actions: <SnackBarAction>[
config.onItemCreated(item); new SnackBarAction(label: "UNDO", onPressed: () {
Navigator.of(context).pop(); config.onItemCreated(item);
})] }),
); ]
));
} }
Widget buildChart() { Widget buildChart() {
...@@ -212,7 +210,6 @@ class FeedFragmentState extends State<FeedFragment> { ...@@ -212,7 +210,6 @@ class FeedFragmentState extends State<FeedFragment> {
return new Scaffold( return new Scaffold(
toolBar: buildToolBar(), toolBar: buildToolBar(),
body: buildBody(), body: buildBody(),
snackBar: new Placeholder(key: _snackBarPlaceholderKey),
floatingActionButton: buildFloatingActionButton() floatingActionButton: buildFloatingActionButton()
); );
} }
......
...@@ -112,8 +112,6 @@ class MeasurementFragment extends StatefulComponent { ...@@ -112,8 +112,6 @@ class MeasurementFragment extends StatefulComponent {
} }
class MeasurementFragmentState extends State<MeasurementFragment> { class MeasurementFragmentState extends State<MeasurementFragment> {
final GlobalKey<PlaceholderState> _snackBarPlaceholderKey = new GlobalKey<PlaceholderState>();
String _weight = ""; String _weight = "";
DateTime _when = new DateTime.now(); DateTime _when = new DateTime.now();
...@@ -123,11 +121,9 @@ class MeasurementFragmentState extends State<MeasurementFragment> { ...@@ -123,11 +121,9 @@ class MeasurementFragmentState extends State<MeasurementFragment> {
parsedWeight = double.parse(_weight); parsedWeight = double.parse(_weight);
} on FormatException catch(e) { } on FormatException catch(e) {
print("Exception $e"); print("Exception $e");
showSnackBar( Scaffold.of(context).showSnackBar(new SnackBar(
context: context,
placeholderKey: _snackBarPlaceholderKey,
content: new Text('Save failed') content: new Text('Save failed')
); ));
} }
config.onCreated(new Measurement(when: _when, weight: parsedWeight)); config.onCreated(new Measurement(when: _when, weight: parsedWeight));
Navigator.of(context).pop(); Navigator.of(context).pop();
...@@ -198,8 +194,7 @@ class MeasurementFragmentState extends State<MeasurementFragment> { ...@@ -198,8 +194,7 @@ class MeasurementFragmentState extends State<MeasurementFragment> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new Scaffold( return new Scaffold(
toolBar: buildToolBar(), toolBar: buildToolBar(),
body: buildBody(context), body: buildBody(context)
snackBar: new Placeholder(key: _snackBarPlaceholderKey)
); );
} }
} }
...@@ -19,7 +19,7 @@ class StockHome extends StatefulComponent { ...@@ -19,7 +19,7 @@ class StockHome extends StatefulComponent {
class StockHomeState extends State<StockHome> { class StockHomeState extends State<StockHome> {
final GlobalKey<PlaceholderState> _snackBarPlaceholderKey = new GlobalKey<PlaceholderState>(); final GlobalKey scaffoldKey = new GlobalKey();
final GlobalKey<PlaceholderState> _bottomSheetPlaceholderKey = new GlobalKey<PlaceholderState>(); final GlobalKey<PlaceholderState> _bottomSheetPlaceholderKey = new GlobalKey<PlaceholderState>();
bool _isSearching = false; bool _isSearching = false;
String _searchQuery; String _searchQuery;
...@@ -179,19 +179,23 @@ class StockHomeState extends State<StockHome> { ...@@ -179,19 +179,23 @@ class StockHomeState extends State<StockHome> {
return stocks.where((Stock stock) => stock.symbol.contains(regexp)); return stocks.where((Stock stock) => stock.symbol.contains(regexp));
} }
void _buyStock(Stock stock, Key arrowKey) {
setState(() {
stock.percentChange = 100.0 * (1.0 / stock.lastSale);
stock.lastSale += 1.0;
});
scaffoldKey.currentState.showSnackBar(new SnackBar(
content: new Text("Purchased ${stock.symbol} for ${stock.lastSale}"),
actions: <SnackBarAction>[
new SnackBarAction(label: "BUY MORE", onPressed: () { _buyStock(stock, arrowKey); })
]
));
}
Widget buildStockList(BuildContext context, Iterable<Stock> stocks) { Widget buildStockList(BuildContext context, Iterable<Stock> stocks) {
return new StockList( return new StockList(
stocks: stocks.toList(), stocks: stocks.toList(),
onAction: (Stock stock, Key arrowKey) { onAction: _buyStock,
setState(() {
stock.percentChange = 100.0 * (1.0 / stock.lastSale);
stock.lastSale += 1.0;
});
showModalBottomSheet(
context: context,
child: new StockSymbolBottomSheet(stock: stock)
);
},
onOpen: (Stock stock, Key arrowKey) { onOpen: (Stock stock, Key arrowKey) {
Set<Key> mostValuableKeys = new Set<Key>(); Set<Key> mostValuableKeys = new Set<Key>();
mostValuableKeys.add(arrowKey); mostValuableKeys.add(arrowKey);
...@@ -229,6 +233,7 @@ class StockHomeState extends State<StockHome> { ...@@ -229,6 +233,7 @@ class StockHomeState extends State<StockHome> {
} }
static GlobalKey searchFieldKey = new GlobalKey(); static GlobalKey searchFieldKey = new GlobalKey();
static GlobalKey companyNameKey = new GlobalKey();
// TODO(abarth): Should we factor this into a SearchBar in the framework? // TODO(abarth): Should we factor this into a SearchBar in the framework?
Widget buildSearchBar() { Widget buildSearchBar() {
...@@ -247,18 +252,16 @@ class StockHomeState extends State<StockHome> { ...@@ -247,18 +252,16 @@ class StockHomeState extends State<StockHome> {
); );
} }
void _handleUndo() { void _handleCreateCompany() {
Navigator.of(context).pop(); showModalBottomSheet(
} // TODO(ianh): Fill this out.
void _handleStockPurchased() {
showSnackBar(
context: context, context: context,
placeholderKey: _snackBarPlaceholderKey, child: new Column([
content: new Text("Stock purchased!"), new Input(
actions: <SnackBarAction>[ key: companyNameKey,
new SnackBarAction(label: "UNDO", onPressed: _handleUndo) placeholder: 'Company Name'
] ),
])
); );
} }
...@@ -266,15 +269,15 @@ class StockHomeState extends State<StockHome> { ...@@ -266,15 +269,15 @@ class StockHomeState extends State<StockHome> {
return new FloatingActionButton( return new FloatingActionButton(
child: new Icon(icon: 'content/add'), child: new Icon(icon: 'content/add'),
backgroundColor: Colors.redAccent[200], backgroundColor: Colors.redAccent[200],
onPressed: _handleStockPurchased onPressed: _handleCreateCompany
); );
} }
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new Scaffold( return new Scaffold(
key: scaffoldKey,
toolBar: _isSearching ? buildSearchBar() : buildToolBar(), toolBar: _isSearching ? buildSearchBar() : buildToolBar(),
body: buildTabNavigator(), body: buildTabNavigator(),
snackBar: new Placeholder(key: _snackBarPlaceholderKey),
bottomSheet: new Placeholder(key: _bottomSheetPlaceholderKey), bottomSheet: new Placeholder(key: _bottomSheetPlaceholderKey),
floatingActionButton: buildFloatingActionButton() floatingActionButton: buildFloatingActionButton()
); );
......
...@@ -15,7 +15,6 @@ const double kStatusBarHeight = 50.0; ...@@ -15,7 +15,6 @@ const double kStatusBarHeight = 50.0;
// Tablet/Desktop: 64dp // Tablet/Desktop: 64dp
const double kToolBarHeight = 56.0; const double kToolBarHeight = 56.0;
const double kExtendedToolBarHeight = 128.0; const double kExtendedToolBarHeight = 128.0;
const double kSnackBarHeight = 52.0;
// https://www.google.com/design/spec/layout/metrics-keylines.html#metrics-keylines-keylines-spacing // https://www.google.com/design/spec/layout/metrics-keylines.html#metrics-keylines-keylines-spacing
const double kListTitleHeight = 72.0; const double kListTitleHeight = 72.0;
......
...@@ -2,15 +2,18 @@ ...@@ -2,15 +2,18 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async';
import 'dart:collection';
import 'dart:math' as math; import 'dart:math' as math;
import 'dart:ui' as ui; import 'dart:ui' as ui;
import 'package:flutter/animation.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'constants.dart';
import 'material.dart'; import 'material.dart';
import 'tool_bar.dart'; import 'tool_bar.dart';
import 'snack_bar.dart';
const double _kFloatingActionButtonMargin = 16.0; // TODO(hmuller): should be device dependent const double _kFloatingActionButtonMargin = 16.0; // TODO(hmuller): should be device dependent
...@@ -77,46 +80,103 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate { ...@@ -77,46 +80,103 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
final _ScaffoldLayout _scaffoldLayout = new _ScaffoldLayout(); final _ScaffoldLayout _scaffoldLayout = new _ScaffoldLayout();
void _addIfNonNull(List<LayoutId> children, Widget child, Object childId) { class Scaffold extends StatefulComponent {
if (child != null)
children.add(new LayoutId(child: child, id: childId));
}
class Scaffold extends StatelessComponent {
Scaffold({ Scaffold({
Key key, Key key,
this.body,
this.toolBar, this.toolBar,
this.snackBar, this.body,
this.floatingActionButton, this.bottomSheet,
this.bottomSheet this.floatingActionButton
}) : super(key: key); }) : super(key: key);
final Widget body;
final ToolBar toolBar; final ToolBar toolBar;
final Widget snackBar; final Widget body;
final Widget bottomSheet; // this is for non-modal bottom sheets
final Widget floatingActionButton; final Widget floatingActionButton;
final Widget bottomSheet;
static ScaffoldState of(BuildContext context) => context.ancestorStateOfType(ScaffoldState);
ScaffoldState createState() => new ScaffoldState();
}
class ScaffoldState extends State<Scaffold> {
Queue<SnackBar> _snackBars = new Queue<SnackBar>();
Performance _snackBarPerformance;
Timer _snackBarTimer;
void showSnackBar(SnackBar snackbar) {
_snackBarPerformance ??= SnackBar.createPerformance()
..addStatusListener(_handleSnackBarStatusChange);
setState(() {
_snackBars.addLast(snackbar.withPerformance(_snackBarPerformance));
});
}
void _handleSnackBarStatusChange(PerformanceStatus status) {
switch (status) {
case PerformanceStatus.dismissed:
assert(_snackBars.isNotEmpty);
setState(() {
_snackBars.removeFirst();
});
break;
case PerformanceStatus.completed:
setState(() {
assert(_snackBarTimer == null);
// build will create a new timer if necessary to dismiss the snack bar
});
break;
case PerformanceStatus.forward:
case PerformanceStatus.reverse:
break;
}
}
void _hideSnackBar() {
_snackBarPerformance.reverse();
_snackBarTimer = null;
}
void dispose() {
_snackBarPerformance?.stop();
_snackBarPerformance = null;
_snackBarTimer?.cancel();
_snackBarTimer = null;
super.dispose();
}
void _addIfNonNull(List<LayoutId> children, Widget child, Object childId) {
if (child != null)
children.add(new LayoutId(child: child, id: childId));
}
Widget build(BuildContext context) { Widget build(BuildContext context) {
final Widget paddedToolBar = toolBar?.withPadding(new EdgeDims.only(top: ui.window.padding.top)); final Widget paddedToolBar = config.toolBar?.withPadding(new EdgeDims.only(top: ui.window.padding.top));
final Widget materialBody = body != null ? new Material(child: body) : null; final Widget materialBody = config.body != null ? new Material(child: config.body) : null;
Widget constrainedSnackBar;
if (snackBar != null) { if (_snackBars.length > 0) {
// TODO(jackson): On tablet/desktop, minWidth = 288, maxWidth = 568 if (_snackBarPerformance.isDismissed)
constrainedSnackBar = new ConstrainedBox( _snackBarPerformance.forward();
constraints: const BoxConstraints(maxHeight: kSnackBarHeight), ModalRoute route = ModalRoute.of(context);
child: snackBar if (route == null || route.isCurrent) {
); if (_snackBarPerformance.isCompleted && _snackBarTimer == null)
_snackBarTimer = new Timer(_snackBars.first.duration, _hideSnackBar);
} else {
_snackBarTimer?.cancel();
_snackBarTimer = null;
}
} }
final List<LayoutId>children = new List<LayoutId>(); final List<LayoutId>children = new List<LayoutId>();
_addIfNonNull(children, materialBody, _Child.body); _addIfNonNull(children, materialBody, _Child.body);
_addIfNonNull(children, paddedToolBar, _Child.toolBar); _addIfNonNull(children, paddedToolBar, _Child.toolBar);
_addIfNonNull(children, bottomSheet, _Child.bottomSheet); _addIfNonNull(children, config.bottomSheet, _Child.bottomSheet);
_addIfNonNull(children, constrainedSnackBar, _Child.snackBar); if (_snackBars.isNotEmpty)
_addIfNonNull(children, floatingActionButton, _Child.floatingActionButton); _addIfNonNull(children, _snackBars.first, _Child.snackBar);
_addIfNonNull(children, config.floatingActionButton, _Child.floatingActionButton);
return new CustomMultiChildLayout(children, delegate: _scaffoldLayout); return new CustomMultiChildLayout(children, delegate: _scaffoldLayout);
} }
} }
...@@ -2,20 +2,30 @@ ...@@ -2,20 +2,30 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async';
import 'package:flutter/animation.dart'; import 'package:flutter/animation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'constants.dart';
import 'material.dart'; import 'material.dart';
import 'theme.dart'; import 'theme.dart';
import 'typography.dart'; import 'typography.dart';
// https://www.google.com/design/spec/components/snackbars-toasts.html#snackbars-toasts-specs
const double _kSideMargins = 24.0; const double _kSideMargins = 24.0;
const double _kVerticalPadding = 14.0; const double _kSingleLineVerticalPadding = 14.0;
const double _kMultiLineVerticalPadding = 24.0;
const Color _kSnackBackground = const Color(0xFF323232); const Color _kSnackBackground = const Color(0xFF323232);
// TODO(ianh): We should check if the given text and actions are going to fit on
// one line or not, and if they are, use the single-line layout, and if not, use
// the multiline layout. See link above.
// TODO(ianh): Implement the Tablet version of snackbar if we're "on a tablet".
const Duration kSnackBarTransitionDuration = const Duration(milliseconds: 250);
const Duration kSnackBarShortDisplayDuration = const Duration(milliseconds: 1500);
const Duration kSnackBarMediumDisplayDuration = const Duration(milliseconds: 2750);
const Curve snackBarFadeCurve = const Interval(0.72, 1.0, curve: Curves.fastOutSlowIn);
class SnackBarAction extends StatelessComponent { class SnackBarAction extends StatelessComponent {
SnackBarAction({Key key, this.label, this.onPressed }) : super(key: key) { SnackBarAction({Key key, this.label, this.onPressed }) : super(key: key) {
assert(label != null); assert(label != null);
...@@ -29,32 +39,35 @@ class SnackBarAction extends StatelessComponent { ...@@ -29,32 +39,35 @@ class SnackBarAction extends StatelessComponent {
onTap: onPressed, onTap: onPressed,
child: new Container( child: new Container(
margin: const EdgeDims.only(left: _kSideMargins), margin: const EdgeDims.only(left: _kSideMargins),
padding: const EdgeDims.symmetric(vertical: _kVerticalPadding), padding: const EdgeDims.symmetric(vertical: _kSingleLineVerticalPadding),
child: new Text(label) child: new Text(label)
) )
); );
} }
} }
class _SnackBar extends StatelessComponent { class SnackBar extends StatelessComponent {
_SnackBar({ SnackBar({
Key key, Key key,
this.content, this.content,
this.actions, this.actions,
this.route this.duration: kSnackBarShortDisplayDuration,
this.performance
}) : super(key: key) { }) : super(key: key) {
assert(content != null); assert(content != null);
} }
final Widget content; final Widget content;
final List<SnackBarAction> actions; final List<SnackBarAction> actions;
final _SnackBarRoute route; final Duration duration;
final PerformanceView performance;
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(performance != null);
List<Widget> children = <Widget>[ List<Widget> children = <Widget>[
new Flexible( new Flexible(
child: new Container( child: new Container(
margin: const EdgeDims.symmetric(vertical: _kVerticalPadding), margin: const EdgeDims.symmetric(vertical: _kSingleLineVerticalPadding),
child: new DefaultTextStyle( child: new DefaultTextStyle(
style: Typography.white.subhead, style: Typography.white.subhead,
child: content child: content
...@@ -64,25 +77,21 @@ class _SnackBar extends StatelessComponent { ...@@ -64,25 +77,21 @@ class _SnackBar extends StatelessComponent {
]; ];
if (actions != null) if (actions != null)
children.addAll(actions); children.addAll(actions);
return new SquashTransition( return new ClipRect(
performance: route.performance, child: new AlignTransition(
height: new AnimatedValue<double>( performance: performance,
0.0, alignment: new AnimatedValue<FractionalOffset>(const FractionalOffset(0.0, 0.0)),
end: kSnackBarHeight, heightFactor: new AnimatedValue<double>(0.0, end: 1.0, curve: Curves.fastOutSlowIn),
curve: Curves.easeIn, child: new Material(
reverseCurve: Curves.easeOut elevation: 6,
), color: _kSnackBackground,
child: new ClipRect( child: new Container(
child: new OverflowBox( margin: const EdgeDims.symmetric(horizontal: _kSideMargins),
minHeight: kSnackBarHeight, child: new DefaultTextStyle(
maxHeight: kSnackBarHeight, style: new TextStyle(color: Theme.of(context).accentColor),
child: new Material( child: new FadeTransition(
elevation: 6, performance: performance,
color: _kSnackBackground, opacity: new AnimatedValue<double>(0.0, end: 1.0, curve: snackBarFadeCurve),
child: new Container(
margin: const EdgeDims.symmetric(horizontal: _kSideMargins),
child: new DefaultTextStyle(
style: new TextStyle(color: Theme.of(context).accentColor),
child: new Row(children) child: new Row(children)
) )
) )
...@@ -91,33 +100,23 @@ class _SnackBar extends StatelessComponent { ...@@ -91,33 +100,23 @@ class _SnackBar extends StatelessComponent {
) )
); );
} }
}
class _SnackBarRoute extends TransitionRoute {
_SnackBarRoute({ Completer completer }) : super(completer: completer);
bool get opaque => false; // API for Scaffold.addSnackBar():
Duration get transitionDuration => const Duration(milliseconds: 200);
}
Future showSnackBar({ BuildContext context, GlobalKey<PlaceholderState> placeholderKey, Widget content, List<SnackBarAction> actions }) {
final Completer completer = new Completer();
_SnackBarRoute route = new _SnackBarRoute(completer: completer);
_SnackBar snackBar = new _SnackBar(
route: route,
content: content,
actions: actions
);
// TODO(hansmuller): https://github.com/flutter/flutter/issues/374 static Performance createPerformance() {
assert(placeholderKey.currentState.child == null); return new Performance(
duration: kSnackBarTransitionDuration,
debugLabel: 'SnackBar'
);
}
placeholderKey.currentState.child = snackBar; SnackBar withPerformance(Performance newPerformance) {
Navigator.of(context).pushEphemeral(route); return new SnackBar(
return completer.future.then((_) { key: key,
// If our overlay has been obscured by an opaque OverlayEntry currentState content: content,
// will have been cleared already. actions: actions,
if (placeholderKey.currentState != null) duration: duration,
placeholderKey.currentState.child = null; performance: newPerformance
}); );
}
} }
...@@ -109,13 +109,15 @@ class NavigatorState extends State<Navigator> { ...@@ -109,13 +109,15 @@ class NavigatorState extends State<Navigator> {
} }
void push(Route route, { Set<Key> mostValuableKeys }) { void push(Route route, { Set<Key> mostValuableKeys }) {
_popAllEphemeralRoutes(); setState(() {
int index = _modal.length-1; _popAllEphemeralRoutes();
while (index >= 0 && _modal[index].willPushNext(route)) int index = _modal.length-1;
index -= 1; while (index >= 0 && _modal[index].willPushNext(route))
route.didPush(overlay, _currentOverlay); index -= 1;
config.observer?.didPushModal(route, index >= 0 ? _modal[index] : null); route.didPush(overlay, _currentOverlay);
_modal.add(route); config.observer?.didPushModal(route, index >= 0 ? _modal[index] : null);
_modal.add(route);
});
} }
void pushEphemeral(Route route) { void pushEphemeral(Route route) {
...@@ -132,17 +134,22 @@ class NavigatorState extends State<Navigator> { ...@@ -132,17 +134,22 @@ class NavigatorState extends State<Navigator> {
} }
void pop([dynamic result]) { void pop([dynamic result]) {
if (_ephemeral.isNotEmpty) { setState(() {
_ephemeral.removeLast().didPop(result); // We use setState to guarantee that we'll rebuild, since the routes can't
} else { // do that for themselves, even if they have changed their own state (e.g.
assert(_modal.length > 1); // ModalScope.isCurrent).
Route route = _modal.removeLast(); if (_ephemeral.isNotEmpty) {
route.didPop(result); _ephemeral.removeLast().didPop(result);
int index = _modal.length-1; } else {
while (index >= 0 && _modal[index].didPopNext(route)) assert(_modal.length > 1);
index -= 1; Route route = _modal.removeLast();
config.observer?.didPopModal(route, index >= 0 ? _modal[index] : null); 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) { Widget build(BuildContext context) {
......
...@@ -31,7 +31,7 @@ class StateRoute extends Route { ...@@ -31,7 +31,7 @@ class StateRoute extends Route {
bool didPopNext(Route nextRoute) => true; bool didPopNext(Route nextRoute) => true;
} }
class OverlayRoute extends Route { abstract class OverlayRoute extends Route {
List<WidgetBuilder> get builders => const <WidgetBuilder>[]; List<WidgetBuilder> get builders => const <WidgetBuilder>[];
List<OverlayEntry> get overlayEntries => _overlayEntries; List<OverlayEntry> get overlayEntries => _overlayEntries;
...@@ -108,24 +108,56 @@ abstract class TransitionRoute extends OverlayRoute { ...@@ -108,24 +108,56 @@ abstract class TransitionRoute extends OverlayRoute {
String toString() => '$runtimeType(performance: $_performance)'; String toString() => '$runtimeType(performance: $_performance)';
} }
class _ModalScopeStatus extends InheritedWidget {
_ModalScopeStatus({
Key key,
this.current,
this.route,
Widget child
}) : super(key: key, child: child) {
assert(current != null);
assert(route != null);
assert(child != null);
}
final bool current;
final Route route;
bool updateShouldNotify(_ModalScopeStatus old) {
return current != old.current ||
route != old.route;
}
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('${current ? "active" : "inactive"}');
}
}
class _ModalScope extends StatusTransitionComponent { class _ModalScope extends StatusTransitionComponent {
_ModalScope({ _ModalScope({
Key key, Key key,
this.subtreeKey, this.subtreeKey,
this.storageBucket, this.storageBucket,
PerformanceView performance, PerformanceView performance,
this.current,
this.route this.route
}) : super(key: key, performance: performance); }) : super(key: key, performance: performance);
final GlobalKey subtreeKey; final GlobalKey subtreeKey;
final PageStorageBucket storageBucket; final PageStorageBucket storageBucket;
final bool current;
final ModalRoute route; final ModalRoute route;
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget contents = new PageStorage( Widget contents = new PageStorage(
key: subtreeKey, key: subtreeKey,
bucket: storageBucket, bucket: storageBucket,
child: route.buildPage(context) child: new _ModalScopeStatus(
current: current,
route: route,
child: route.buildPage(context)
)
); );
if (route.offstage) { if (route.offstage) {
contents = new OffStage(child: contents); contents = new OffStage(child: contents);
...@@ -165,8 +197,18 @@ abstract class ModalRoute extends TransitionRoute { ...@@ -165,8 +197,18 @@ abstract class ModalRoute extends TransitionRoute {
this.settings: const NamedRouteSettings() this.settings: const NamedRouteSettings()
}) : super(completer: completer); }) : super(completer: completer);
// The API for general users of this class
final NamedRouteSettings settings; final NamedRouteSettings settings;
static ModalRoute of(BuildContext context) {
_ModalScopeStatus widget = context.inheritFromWidgetOfType(_ModalScopeStatus);
return widget?.route;
}
bool get isCurrent => _isCurrent;
bool _isCurrent = false;
// The API for subclasses to override - used by _ModalScope // The API for subclasses to override - used by _ModalScope
...@@ -204,6 +246,34 @@ abstract class ModalRoute extends TransitionRoute { ...@@ -204,6 +246,34 @@ abstract class ModalRoute extends TransitionRoute {
// Internals // Internals
void didPush(OverlayState overlay, OverlayEntry insertionPoint) {
assert(!_isCurrent);
_isCurrent = true;
super.didPush(overlay, insertionPoint);
}
void didPop(dynamic result) {
assert(_isCurrent);
_isCurrent = false;
super.didPop(result);
}
bool willPushNext(Route nextRoute) {
if (nextRoute is ModalRoute) {
assert(_isCurrent);
_isCurrent = false;
}
return false;
}
bool didPopNext(Route nextRoute) {
if (nextRoute is ModalRoute) {
assert(!_isCurrent);
_isCurrent = true;
}
return false;
}
final GlobalKey<StatusTransitionState> _scopeKey = new GlobalKey<StatusTransitionState>(); final GlobalKey<StatusTransitionState> _scopeKey = new GlobalKey<StatusTransitionState>();
final GlobalKey _subtreeKey = new GlobalKey(); final GlobalKey _subtreeKey = new GlobalKey();
final PageStorageBucket _storageBucket = new PageStorageBucket(); final PageStorageBucket _storageBucket = new PageStorageBucket();
...@@ -222,6 +292,7 @@ abstract class ModalRoute extends TransitionRoute { ...@@ -222,6 +292,7 @@ abstract class ModalRoute extends TransitionRoute {
subtreeKey: _subtreeKey, subtreeKey: _subtreeKey,
storageBucket: _storageBucket, storageBucket: _storageBucket,
performance: performance, performance: performance,
current: isCurrent,
route: this route: this
); );
} }
......
...@@ -7,60 +7,142 @@ import 'package:test/test.dart'; ...@@ -7,60 +7,142 @@ import 'package:test/test.dart';
import 'widget_tester.dart'; import 'widget_tester.dart';
class Builder extends StatelessComponent {
Builder({ this.builder });
final WidgetBuilder builder;
Widget build(BuildContext context) => builder(context);
}
void main() { void main() {
test('SnackBar control test', () { test('SnackBar control test', () {
testWidgets((WidgetTester tester) { testWidgets((WidgetTester tester) {
String helloSnackBar = 'Hello SnackBar'; String helloSnackBar = 'Hello SnackBar';
GlobalKey<PlaceholderState> placeholderKey = new GlobalKey<PlaceholderState>();
Key tapTarget = new Key('tap-target'); Key tapTarget = new Key('tap-target');
BuildContext context;
bool showSnackBarThenCalled = false;
tester.pumpWidget(new MaterialApp( tester.pumpWidget(new MaterialApp(
routes: <String, RouteBuilder>{ routes: <String, RouteBuilder>{
'/': (RouteArguments args) { '/': (RouteArguments args) {
context = args.context; return new Scaffold(
return new GestureDetector( body: new Builder(
onTap: () { builder: (BuildContext context) {
showSnackBar( return new GestureDetector(
context: args.context, onTap: () {
placeholderKey: placeholderKey, Scaffold.of(context).showSnackBar(new SnackBar(
content: new Text(helloSnackBar) content: new Text(helloSnackBar),
).then((_) { duration: new Duration(seconds: 2)
showSnackBarThenCalled = true; ));
}); },
}, behavior: HitTestBehavior.opaque,
child: new Container( child: new Container(
decoration: const BoxDecoration( height: 100.0,
backgroundColor: const Color(0xFF00FF00) width: 100.0,
), key: tapTarget
child: new Center( )
key: tapTarget, );
child: new Placeholder(key: placeholderKey) }
)
) )
); );
} }
} }
)); ));
expect(tester.findText(helloSnackBar), isNull);
// TODO(hansmuller): find a way to avoid calling pump over and over.
// https://github.com/flutter/flutter/issues/348
tester.tap(tester.findElementByKey(tapTarget)); tester.tap(tester.findElementByKey(tapTarget));
expect(tester.findText(helloSnackBar), isNull); expect(tester.findText(helloSnackBar), isNull);
tester.pump(new Duration(seconds: 1)); tester.pump(); // schedule animation
tester.pump(new Duration(seconds: 1));
expect(tester.findText(helloSnackBar), isNotNull); expect(tester.findText(helloSnackBar), isNotNull);
tester.pump(); // begin animation
Navigator.of(context).pop(); expect(tester.findText(helloSnackBar), isNotNull);
tester.pump(new Duration(milliseconds: 750)); // 0.75s // animation last frame; two second timer starts here
expect(tester.findText(helloSnackBar), isNotNull);
tester.pump(new Duration(milliseconds: 750)); // 1.50s
expect(tester.findText(helloSnackBar), isNotNull); expect(tester.findText(helloSnackBar), isNotNull);
tester.pump(new Duration(seconds: 1)); tester.pump(new Duration(milliseconds: 750)); // 2.25s
tester.pump(new Duration(seconds: 1)); expect(tester.findText(helloSnackBar), isNotNull);
tester.pump(new Duration(seconds: 1)); tester.pump(new Duration(milliseconds: 750)); // 3.00s // timer triggers to dismiss snackbar, reverse animation is scheduled
expect(showSnackBarThenCalled, isTrue); tester.pump(); // begin animation
expect(tester.findText(helloSnackBar), isNotNull); // frame 0 of dismiss animation
tester.pump(new Duration(milliseconds: 750)); // 3.75s // last frame of animation, snackbar removed from build
expect(tester.findText(helloSnackBar), isNull); expect(tester.findText(helloSnackBar), isNull);
expect(placeholderKey.currentState.child, isNull); });
});
test('SnackBar twice test', () {
testWidgets((WidgetTester tester) {
int snackBarCount = 0;
Key tapTarget = new Key('tap-target');
tester.pumpWidget(new MaterialApp(
routes: <String, RouteBuilder>{
'/': (RouteArguments args) {
return new Scaffold(
body: new Builder(
builder: (BuildContext context) {
return new GestureDetector(
onTap: () {
snackBarCount += 1;
Scaffold.of(context).showSnackBar(new SnackBar(
content: new Text("bar$snackBarCount"),
duration: new Duration(seconds: 2)
));
},
behavior: HitTestBehavior.opaque,
child: new Container(
height: 100.0,
width: 100.0,
key: tapTarget
)
);
}
)
);
}
}
));
expect(tester.findText('bar1'), isNull);
expect(tester.findText('bar2'), isNull);
tester.tap(tester.findElementByKey(tapTarget)); // queue bar1
tester.tap(tester.findElementByKey(tapTarget)); // queue bar2
expect(tester.findText('bar1'), isNull);
expect(tester.findText('bar2'), isNull);
tester.pump(); // schedule animation for bar1
expect(tester.findText('bar1'), isNotNull);
expect(tester.findText('bar2'), isNull);
tester.pump(); // begin animation
expect(tester.findText('bar1'), isNotNull);
expect(tester.findText('bar2'), isNull);
tester.pump(new Duration(milliseconds: 750)); // 0.75s // animation last frame; two second timer starts here
expect(tester.findText('bar1'), isNotNull);
expect(tester.findText('bar2'), isNull);
tester.pump(new Duration(milliseconds: 750)); // 1.50s
expect(tester.findText('bar1'), isNotNull);
expect(tester.findText('bar2'), isNull);
tester.pump(new Duration(milliseconds: 750)); // 2.25s
expect(tester.findText('bar1'), isNotNull);
expect(tester.findText('bar2'), isNull);
tester.pump(new Duration(milliseconds: 750)); // 3.00s // timer triggers to dismiss snackbar, reverse animation is scheduled
tester.pump(); // begin animation
expect(tester.findText('bar1'), isNotNull);
expect(tester.findText('bar2'), isNull);
tester.pump(new Duration(milliseconds: 750)); // 3.75s // last frame of animation, snackbar removed from build, new snack bar put in its place
expect(tester.findText('bar1'), isNull);
expect(tester.findText('bar2'), isNotNull);
tester.pump(); // begin animation
expect(tester.findText('bar1'), isNull);
expect(tester.findText('bar2'), isNotNull);
tester.pump(new Duration(milliseconds: 750)); // 4.50s // animation last frame; two second timer starts here
expect(tester.findText('bar1'), isNull);
expect(tester.findText('bar2'), isNotNull);
tester.pump(new Duration(milliseconds: 750)); // 5.25s
expect(tester.findText('bar1'), isNull);
expect(tester.findText('bar2'), isNotNull);
tester.pump(new Duration(milliseconds: 750)); // 6.00s
expect(tester.findText('bar1'), isNull);
expect(tester.findText('bar2'), isNotNull);
tester.pump(new Duration(milliseconds: 750)); // 6.75s // timer triggers to dismiss snackbar, reverse animation is scheduled
tester.pump(); // begin animation
expect(tester.findText('bar1'), isNull);
expect(tester.findText('bar2'), isNotNull);
tester.pump(new Duration(milliseconds: 750)); // 7.50s // last frame of animation, snackbar removed from build, new snack bar put in its place
expect(tester.findText('bar1'), isNull);
expect(tester.findText('bar2'), isNull);
}); });
}); });
} }
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