diff --git a/examples/fitness/lib/feed.dart b/examples/fitness/lib/feed.dart index 73ba71ad678..1737f919835 100644 --- a/examples/fitness/lib/feed.dart +++ b/examples/fitness/lib/feed.dart @@ -54,7 +54,6 @@ class FeedFragment extends StatefulComponent { } class FeedFragmentState extends State { - final GlobalKey _snackBarPlaceholderKey = new GlobalKey(); FitnessMode _fitnessMode = FitnessMode.feed; void _handleFitnessModeChange(FitnessMode value) { @@ -115,15 +114,14 @@ class FeedFragmentState extends State { void _handleItemDismissed(FitnessItem item) { config.onItemDeleted(item); - showSnackBar( - context: context, - placeholderKey: _snackBarPlaceholderKey, + Scaffold.of(context).showSnackBar(new SnackBar( content: new Text("Item deleted."), - actions: [new SnackBarAction(label: "UNDO", onPressed: () { - config.onItemCreated(item); - Navigator.of(context).pop(); - })] - ); + actions: [ + new SnackBarAction(label: "UNDO", onPressed: () { + config.onItemCreated(item); + }), + ] + )); } Widget buildChart() { @@ -212,7 +210,6 @@ class FeedFragmentState extends State { return new Scaffold( toolBar: buildToolBar(), body: buildBody(), - snackBar: new Placeholder(key: _snackBarPlaceholderKey), floatingActionButton: buildFloatingActionButton() ); } diff --git a/examples/fitness/lib/measurement.dart b/examples/fitness/lib/measurement.dart index 8429215a88b..72ffbb93704 100644 --- a/examples/fitness/lib/measurement.dart +++ b/examples/fitness/lib/measurement.dart @@ -112,8 +112,6 @@ class MeasurementFragment extends StatefulComponent { } class MeasurementFragmentState extends State { - final GlobalKey _snackBarPlaceholderKey = new GlobalKey(); - String _weight = ""; DateTime _when = new DateTime.now(); @@ -123,11 +121,9 @@ class MeasurementFragmentState extends State { parsedWeight = double.parse(_weight); } on FormatException catch(e) { print("Exception $e"); - showSnackBar( - context: context, - placeholderKey: _snackBarPlaceholderKey, + Scaffold.of(context).showSnackBar(new SnackBar( content: new Text('Save failed') - ); + )); } config.onCreated(new Measurement(when: _when, weight: parsedWeight)); Navigator.of(context).pop(); @@ -198,8 +194,7 @@ class MeasurementFragmentState extends State { Widget build(BuildContext context) { return new Scaffold( toolBar: buildToolBar(), - body: buildBody(context), - snackBar: new Placeholder(key: _snackBarPlaceholderKey) + body: buildBody(context) ); } } diff --git a/examples/stocks/lib/stock_home.dart b/examples/stocks/lib/stock_home.dart index be9b5314c7d..f07267ce7bb 100644 --- a/examples/stocks/lib/stock_home.dart +++ b/examples/stocks/lib/stock_home.dart @@ -19,7 +19,7 @@ class StockHome extends StatefulComponent { class StockHomeState extends State { - final GlobalKey _snackBarPlaceholderKey = new GlobalKey(); + final GlobalKey scaffoldKey = new GlobalKey(); final GlobalKey _bottomSheetPlaceholderKey = new GlobalKey(); bool _isSearching = false; String _searchQuery; @@ -179,19 +179,23 @@ class StockHomeState extends State { 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: [ + new SnackBarAction(label: "BUY MORE", onPressed: () { _buyStock(stock, arrowKey); }) + ] + )); + } + Widget buildStockList(BuildContext context, Iterable stocks) { return new StockList( stocks: stocks.toList(), - onAction: (Stock stock, Key arrowKey) { - setState(() { - stock.percentChange = 100.0 * (1.0 / stock.lastSale); - stock.lastSale += 1.0; - }); - showModalBottomSheet( - context: context, - child: new StockSymbolBottomSheet(stock: stock) - ); - }, + onAction: _buyStock, onOpen: (Stock stock, Key arrowKey) { Set mostValuableKeys = new Set(); mostValuableKeys.add(arrowKey); @@ -229,6 +233,7 @@ class StockHomeState extends State { } static GlobalKey searchFieldKey = new GlobalKey(); + static GlobalKey companyNameKey = new GlobalKey(); // TODO(abarth): Should we factor this into a SearchBar in the framework? Widget buildSearchBar() { @@ -247,18 +252,16 @@ class StockHomeState extends State { ); } - void _handleUndo() { - Navigator.of(context).pop(); - } - - void _handleStockPurchased() { - showSnackBar( + void _handleCreateCompany() { + showModalBottomSheet( + // TODO(ianh): Fill this out. context: context, - placeholderKey: _snackBarPlaceholderKey, - content: new Text("Stock purchased!"), - actions: [ - new SnackBarAction(label: "UNDO", onPressed: _handleUndo) - ] + child: new Column([ + new Input( + key: companyNameKey, + placeholder: 'Company Name' + ), + ]) ); } @@ -266,15 +269,15 @@ class StockHomeState extends State { return new FloatingActionButton( child: new Icon(icon: 'content/add'), backgroundColor: Colors.redAccent[200], - onPressed: _handleStockPurchased + onPressed: _handleCreateCompany ); } Widget build(BuildContext context) { return new Scaffold( + key: scaffoldKey, toolBar: _isSearching ? buildSearchBar() : buildToolBar(), body: buildTabNavigator(), - snackBar: new Placeholder(key: _snackBarPlaceholderKey), bottomSheet: new Placeholder(key: _bottomSheetPlaceholderKey), floatingActionButton: buildFloatingActionButton() ); diff --git a/packages/flutter/lib/src/material/constants.dart b/packages/flutter/lib/src/material/constants.dart index a996fc5736e..d27a0fcdd3a 100644 --- a/packages/flutter/lib/src/material/constants.dart +++ b/packages/flutter/lib/src/material/constants.dart @@ -15,7 +15,6 @@ const double kStatusBarHeight = 50.0; // Tablet/Desktop: 64dp const double kToolBarHeight = 56.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 const double kListTitleHeight = 72.0; diff --git a/packages/flutter/lib/src/material/scaffold.dart b/packages/flutter/lib/src/material/scaffold.dart index 3a88ce9b70e..af78e38dbef 100644 --- a/packages/flutter/lib/src/material/scaffold.dart +++ b/packages/flutter/lib/src/material/scaffold.dart @@ -2,15 +2,18 @@ // 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:collection'; import 'dart:math' as math; import 'dart:ui' as ui; +import 'package:flutter/animation.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; -import 'constants.dart'; import 'material.dart'; import 'tool_bar.dart'; +import 'snack_bar.dart'; const double _kFloatingActionButtonMargin = 16.0; // TODO(hmuller): should be device dependent @@ -77,46 +80,103 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate { final _ScaffoldLayout _scaffoldLayout = new _ScaffoldLayout(); -void _addIfNonNull(List children, Widget child, Object childId) { - if (child != null) - children.add(new LayoutId(child: child, id: childId)); -} - -class Scaffold extends StatelessComponent { +class Scaffold extends StatefulComponent { Scaffold({ Key key, - this.body, this.toolBar, - this.snackBar, - this.floatingActionButton, - this.bottomSheet + this.body, + this.bottomSheet, + this.floatingActionButton }) : super(key: key); - final Widget body; final ToolBar toolBar; - final Widget snackBar; + final Widget body; + final Widget bottomSheet; // this is for non-modal bottom sheets final Widget floatingActionButton; - final Widget bottomSheet; + + static ScaffoldState of(BuildContext context) => context.ancestorStateOfType(ScaffoldState); + + ScaffoldState createState() => new ScaffoldState(); +} + +class ScaffoldState extends State { + + Queue _snackBars = new Queue(); + 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 children, Widget child, Object childId) { + if (child != null) + children.add(new LayoutId(child: child, id: childId)); + } Widget build(BuildContext context) { - final Widget paddedToolBar = toolBar?.withPadding(new EdgeDims.only(top: ui.window.padding.top)); - final Widget materialBody = body != null ? new Material(child: body) : null; - Widget constrainedSnackBar; - if (snackBar != null) { - // TODO(jackson): On tablet/desktop, minWidth = 288, maxWidth = 568 - constrainedSnackBar = new ConstrainedBox( - constraints: const BoxConstraints(maxHeight: kSnackBarHeight), - child: snackBar - ); + final Widget paddedToolBar = config.toolBar?.withPadding(new EdgeDims.only(top: ui.window.padding.top)); + final Widget materialBody = config.body != null ? new Material(child: config.body) : null; + + if (_snackBars.length > 0) { + if (_snackBarPerformance.isDismissed) + _snackBarPerformance.forward(); + ModalRoute route = ModalRoute.of(context); + if (route == null || route.isCurrent) { + if (_snackBarPerformance.isCompleted && _snackBarTimer == null) + _snackBarTimer = new Timer(_snackBars.first.duration, _hideSnackBar); + } else { + _snackBarTimer?.cancel(); + _snackBarTimer = null; + } } final Listchildren = new List(); _addIfNonNull(children, materialBody, _Child.body); _addIfNonNull(children, paddedToolBar, _Child.toolBar); - _addIfNonNull(children, bottomSheet, _Child.bottomSheet); - _addIfNonNull(children, constrainedSnackBar, _Child.snackBar); - _addIfNonNull(children, floatingActionButton, _Child.floatingActionButton); + _addIfNonNull(children, config.bottomSheet, _Child.bottomSheet); + if (_snackBars.isNotEmpty) + _addIfNonNull(children, _snackBars.first, _Child.snackBar); + _addIfNonNull(children, config.floatingActionButton, _Child.floatingActionButton); return new CustomMultiChildLayout(children, delegate: _scaffoldLayout); } + } diff --git a/packages/flutter/lib/src/material/snack_bar.dart b/packages/flutter/lib/src/material/snack_bar.dart index 426cb400097..8254874bc09 100644 --- a/packages/flutter/lib/src/material/snack_bar.dart +++ b/packages/flutter/lib/src/material/snack_bar.dart @@ -2,20 +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 'package:flutter/widgets.dart'; -import 'constants.dart'; import 'material.dart'; import 'theme.dart'; import 'typography.dart'; +// https://www.google.com/design/spec/components/snackbars-toasts.html#snackbars-toasts-specs 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); +// 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 { SnackBarAction({Key key, this.label, this.onPressed }) : super(key: key) { assert(label != null); @@ -29,32 +39,35 @@ class SnackBarAction extends StatelessComponent { onTap: onPressed, child: new Container( margin: const EdgeDims.only(left: _kSideMargins), - padding: const EdgeDims.symmetric(vertical: _kVerticalPadding), + padding: const EdgeDims.symmetric(vertical: _kSingleLineVerticalPadding), child: new Text(label) ) ); } } -class _SnackBar extends StatelessComponent { - _SnackBar({ +class SnackBar extends StatelessComponent { + SnackBar({ Key key, this.content, this.actions, - this.route + this.duration: kSnackBarShortDisplayDuration, + this.performance }) : super(key: key) { assert(content != null); } final Widget content; final List actions; - final _SnackBarRoute route; + final Duration duration; + final PerformanceView performance; Widget build(BuildContext context) { + assert(performance != null); List children = [ new Flexible( child: new Container( - margin: const EdgeDims.symmetric(vertical: _kVerticalPadding), + margin: const EdgeDims.symmetric(vertical: _kSingleLineVerticalPadding), child: new DefaultTextStyle( style: Typography.white.subhead, child: content @@ -64,25 +77,21 @@ class _SnackBar extends StatelessComponent { ]; if (actions != null) children.addAll(actions); - return new SquashTransition( - performance: route.performance, - height: new AnimatedValue( - 0.0, - end: kSnackBarHeight, - curve: Curves.easeIn, - reverseCurve: Curves.easeOut - ), - child: new ClipRect( - child: new OverflowBox( - minHeight: kSnackBarHeight, - maxHeight: kSnackBarHeight, - child: new Material( - elevation: 6, - color: _kSnackBackground, - child: new Container( - margin: const EdgeDims.symmetric(horizontal: _kSideMargins), - child: new DefaultTextStyle( - style: new TextStyle(color: Theme.of(context).accentColor), + return new ClipRect( + child: new AlignTransition( + performance: performance, + alignment: new AnimatedValue(const FractionalOffset(0.0, 0.0)), + heightFactor: new AnimatedValue(0.0, end: 1.0, curve: Curves.fastOutSlowIn), + child: new Material( + elevation: 6, + color: _kSnackBackground, + child: new Container( + margin: const EdgeDims.symmetric(horizontal: _kSideMargins), + child: new DefaultTextStyle( + style: new TextStyle(color: Theme.of(context).accentColor), + child: new FadeTransition( + performance: performance, + opacity: new AnimatedValue(0.0, end: 1.0, curve: snackBarFadeCurve), child: new Row(children) ) ) @@ -91,33 +100,23 @@ class _SnackBar extends StatelessComponent { ) ); } -} - -class _SnackBarRoute extends TransitionRoute { - _SnackBarRoute({ Completer completer }) : super(completer: completer); - - bool get opaque => false; - Duration get transitionDuration => const Duration(milliseconds: 200); -} - -Future showSnackBar({ BuildContext context, GlobalKey placeholderKey, Widget content, List 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 - assert(placeholderKey.currentState.child == null); - - placeholderKey.currentState.child = snackBar; - Navigator.of(context).pushEphemeral(route); - return completer.future.then((_) { - // If our overlay has been obscured by an opaque OverlayEntry currentState - // will have been cleared already. - if (placeholderKey.currentState != null) - placeholderKey.currentState.child = null; - }); + + // API for Scaffold.addSnackBar(): + + static Performance createPerformance() { + return new Performance( + duration: kSnackBarTransitionDuration, + debugLabel: 'SnackBar' + ); + } + + SnackBar withPerformance(Performance newPerformance) { + return new SnackBar( + key: key, + content: content, + actions: actions, + duration: duration, + performance: newPerformance + ); + } } diff --git a/packages/flutter/lib/src/widgets/navigator.dart b/packages/flutter/lib/src/widgets/navigator.dart index 8293121f310..b4813694b06 100644 --- a/packages/flutter/lib/src/widgets/navigator.dart +++ b/packages/flutter/lib/src/widgets/navigator.dart @@ -109,13 +109,15 @@ class NavigatorState extends State { } void push(Route route, { Set mostValuableKeys }) { - _popAllEphemeralRoutes(); - int index = _modal.length-1; - while (index >= 0 && _modal[index].willPushNext(route)) - index -= 1; - route.didPush(overlay, _currentOverlay); - config.observer?.didPushModal(route, index >= 0 ? _modal[index] : null); - _modal.add(route); + setState(() { + _popAllEphemeralRoutes(); + int index = _modal.length-1; + while (index >= 0 && _modal[index].willPushNext(route)) + index -= 1; + route.didPush(overlay, _currentOverlay); + config.observer?.didPushModal(route, index >= 0 ? _modal[index] : null); + _modal.add(route); + }); } void pushEphemeral(Route route) { @@ -132,17 +134,22 @@ class NavigatorState extends State { } void pop([dynamic result]) { - 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); - } + setState(() { + // 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); + } + }); } Widget build(BuildContext context) { diff --git a/packages/flutter/lib/src/widgets/routes.dart b/packages/flutter/lib/src/widgets/routes.dart index 38cd708330d..df78c6311e2 100644 --- a/packages/flutter/lib/src/widgets/routes.dart +++ b/packages/flutter/lib/src/widgets/routes.dart @@ -31,7 +31,7 @@ class StateRoute extends Route { bool didPopNext(Route nextRoute) => true; } -class OverlayRoute extends Route { +abstract class OverlayRoute extends Route { List get builders => const []; List get overlayEntries => _overlayEntries; @@ -108,24 +108,56 @@ abstract class TransitionRoute extends OverlayRoute { 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 description) { + super.debugFillDescription(description); + description.add('${current ? "active" : "inactive"}'); + } +} + class _ModalScope extends StatusTransitionComponent { _ModalScope({ Key key, this.subtreeKey, this.storageBucket, PerformanceView performance, + this.current, this.route }) : super(key: key, performance: performance); final GlobalKey subtreeKey; final PageStorageBucket storageBucket; + final bool current; final ModalRoute route; Widget build(BuildContext context) { Widget contents = new PageStorage( key: subtreeKey, bucket: storageBucket, - child: route.buildPage(context) + child: new _ModalScopeStatus( + current: current, + route: route, + child: route.buildPage(context) + ) ); if (route.offstage) { contents = new OffStage(child: contents); @@ -165,8 +197,18 @@ abstract class ModalRoute extends TransitionRoute { this.settings: const NamedRouteSettings() }) : super(completer: completer); + // The API for general users of this class + 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 @@ -204,6 +246,34 @@ abstract class ModalRoute extends TransitionRoute { // 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 _scopeKey = new GlobalKey(); final GlobalKey _subtreeKey = new GlobalKey(); final PageStorageBucket _storageBucket = new PageStorageBucket(); @@ -222,6 +292,7 @@ abstract class ModalRoute extends TransitionRoute { subtreeKey: _subtreeKey, storageBucket: _storageBucket, performance: performance, + current: isCurrent, route: this ); } diff --git a/packages/unit/test/widget/snack_bar_test.dart b/packages/unit/test/widget/snack_bar_test.dart index bdcc96471fa..dd64b50a2e0 100644 --- a/packages/unit/test/widget/snack_bar_test.dart +++ b/packages/unit/test/widget/snack_bar_test.dart @@ -7,60 +7,142 @@ import 'package:test/test.dart'; import 'widget_tester.dart'; +class Builder extends StatelessComponent { + Builder({ this.builder }); + final WidgetBuilder builder; + Widget build(BuildContext context) => builder(context); +} + void main() { test('SnackBar control test', () { testWidgets((WidgetTester tester) { String helloSnackBar = 'Hello SnackBar'; - GlobalKey placeholderKey = new GlobalKey(); Key tapTarget = new Key('tap-target'); - BuildContext context; - bool showSnackBarThenCalled = false; - tester.pumpWidget(new MaterialApp( routes: { '/': (RouteArguments args) { - context = args.context; - return new GestureDetector( - onTap: () { - showSnackBar( - context: args.context, - placeholderKey: placeholderKey, - content: new Text(helloSnackBar) - ).then((_) { - showSnackBarThenCalled = true; - }); - }, - child: new Container( - decoration: const BoxDecoration( - backgroundColor: const Color(0xFF00FF00) - ), - child: new Center( - key: tapTarget, - child: new Placeholder(key: placeholderKey) - ) + return new Scaffold( + body: new Builder( + builder: (BuildContext context) { + return new GestureDetector( + onTap: () { + Scaffold.of(context).showSnackBar(new SnackBar( + content: new Text(helloSnackBar), + duration: new Duration(seconds: 2) + )); + }, + behavior: HitTestBehavior.opaque, + child: new Container( + height: 100.0, + width: 100.0, + key: tapTarget + ) + ); + } ) ); } } )); - - // TODO(hansmuller): find a way to avoid calling pump over and over. - // https://github.com/flutter/flutter/issues/348 - + expect(tester.findText(helloSnackBar), isNull); tester.tap(tester.findElementByKey(tapTarget)); expect(tester.findText(helloSnackBar), isNull); - tester.pump(new Duration(seconds: 1)); - tester.pump(new Duration(seconds: 1)); + tester.pump(); // schedule animation expect(tester.findText(helloSnackBar), isNotNull); - - Navigator.of(context).pop(); + tester.pump(); // begin animation expect(tester.findText(helloSnackBar), isNotNull); - tester.pump(new Duration(seconds: 1)); - tester.pump(new Duration(seconds: 1)); - tester.pump(new Duration(seconds: 1)); - expect(showSnackBarThenCalled, isTrue); + 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); + tester.pump(new Duration(milliseconds: 750)); // 2.25s + expect(tester.findText(helloSnackBar), isNotNull); + tester.pump(new Duration(milliseconds: 750)); // 3.00s // timer triggers to dismiss snackbar, reverse animation is scheduled + 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(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: { + '/': (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); }); }); }