Snackbar Refactor

"showSnackBar()" is now a feature of a Scaffold. To get to a Scaffold
you either use a global key (`scaffoldKey.currentState.showSnackBar(...)`),
or you use `Scaffold.of(context)`.

Snack bars no longer have a route. They are entirely managed by the
Scaffold. Fixes #432.

Snack bars now queue up when you have several of them. Fixes #374.

Snack bars now auto-size themselves around their contents. This is step
one towards implementing multiline snack bars.

Snack bars now self-dismiss after some per-snackbar configurable period.

The self-dismissing pauses while a dialog is up above the snackbar (or
anything that uses ModalRoute). To enable this, there's now a
`ModalRoute.of(context)` API that returns the current ModalRoute, and
you will be rebuilt if you asked for this and the route's "current"
status changes. To implement this, the Navigator now rebuilds
unconditionally any time it pushes or pops a route.

Snack bars now use the curves that Android uses for snack bars.

Snack bar contents now fade in.
This commit is contained in:
Hixie 2015-11-17 16:20:55 -08:00
parent 67b2ee3a3b
commit 954713ab23
9 changed files with 396 additions and 183 deletions

View file

@ -54,7 +54,6 @@ class FeedFragment extends StatefulComponent {
}
class FeedFragmentState extends State<FeedFragment> {
final GlobalKey<PlaceholderState> _snackBarPlaceholderKey = new GlobalKey<PlaceholderState>();
FitnessMode _fitnessMode = FitnessMode.feed;
void _handleFitnessModeChange(FitnessMode value) {
@ -115,15 +114,14 @@ class FeedFragmentState extends State<FeedFragment> {
void _handleItemDismissed(FitnessItem item) {
config.onItemDeleted(item);
showSnackBar(
context: context,
placeholderKey: _snackBarPlaceholderKey,
Scaffold.of(context).showSnackBar(new SnackBar(
content: new Text("Item deleted."),
actions: <SnackBarAction>[new SnackBarAction(label: "UNDO", onPressed: () {
config.onItemCreated(item);
Navigator.of(context).pop();
})]
);
actions: <SnackBarAction>[
new SnackBarAction(label: "UNDO", onPressed: () {
config.onItemCreated(item);
}),
]
));
}
Widget buildChart() {
@ -212,7 +210,6 @@ class FeedFragmentState extends State<FeedFragment> {
return new Scaffold(
toolBar: buildToolBar(),
body: buildBody(),
snackBar: new Placeholder(key: _snackBarPlaceholderKey),
floatingActionButton: buildFloatingActionButton()
);
}

View file

@ -112,8 +112,6 @@ class MeasurementFragment extends StatefulComponent {
}
class MeasurementFragmentState extends State<MeasurementFragment> {
final GlobalKey<PlaceholderState> _snackBarPlaceholderKey = new GlobalKey<PlaceholderState>();
String _weight = "";
DateTime _when = new DateTime.now();
@ -123,11 +121,9 @@ class MeasurementFragmentState extends State<MeasurementFragment> {
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<MeasurementFragment> {
Widget build(BuildContext context) {
return new Scaffold(
toolBar: buildToolBar(),
body: buildBody(context),
snackBar: new Placeholder(key: _snackBarPlaceholderKey)
body: buildBody(context)
);
}
}

View file

@ -19,7 +19,7 @@ class StockHome extends StatefulComponent {
class StockHomeState extends State<StockHome> {
final GlobalKey<PlaceholderState> _snackBarPlaceholderKey = new GlobalKey<PlaceholderState>();
final GlobalKey scaffoldKey = new GlobalKey();
final GlobalKey<PlaceholderState> _bottomSheetPlaceholderKey = new GlobalKey<PlaceholderState>();
bool _isSearching = false;
String _searchQuery;
@ -179,19 +179,23 @@ class StockHomeState extends State<StockHome> {
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) {
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<Key> mostValuableKeys = new Set<Key>();
mostValuableKeys.add(arrowKey);
@ -229,6 +233,7 @@ class StockHomeState extends State<StockHome> {
}
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<StockHome> {
);
}
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: <SnackBarAction>[
new SnackBarAction(label: "UNDO", onPressed: _handleUndo)
]
child: new Column([
new Input(
key: companyNameKey,
placeholder: 'Company Name'
),
])
);
}
@ -266,15 +269,15 @@ class StockHomeState extends State<StockHome> {
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()
);

View file

@ -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;

View file

@ -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<LayoutId> 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<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) {
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 List<LayoutId>children = new List<LayoutId>();
_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);
}
}

View file

@ -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<SnackBarAction> actions;
final _SnackBarRoute route;
final Duration duration;
final PerformanceView performance;
Widget build(BuildContext context) {
assert(performance != null);
List<Widget> children = <Widget>[
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<double>(
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<FractionalOffset>(const FractionalOffset(0.0, 0.0)),
heightFactor: new AnimatedValue<double>(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<double>(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<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
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
);
}
}

View file

@ -109,13 +109,15 @@ class NavigatorState extends State<Navigator> {
}
void push(Route route, { Set<Key> 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<Navigator> {
}
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) {

View file

@ -31,7 +31,7 @@ class StateRoute extends Route {
bool didPopNext(Route nextRoute) => true;
}
class OverlayRoute extends Route {
abstract class OverlayRoute extends Route {
List<WidgetBuilder> get builders => const <WidgetBuilder>[];
List<OverlayEntry> 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<String> 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<StatusTransitionState> _scopeKey = new GlobalKey<StatusTransitionState>();
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
);
}

View file

@ -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<PlaceholderState> placeholderKey = new GlobalKey<PlaceholderState>();
Key tapTarget = new Key('tap-target');
BuildContext context;
bool showSnackBarThenCalled = false;
tester.pumpWidget(new MaterialApp(
routes: <String, RouteBuilder>{
'/': (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: <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);
});
});
}