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 {
}
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()
);
}
......
......@@ -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)
);
}
}
......@@ -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()
);
......
......@@ -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;
......
......@@ -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);
}
}
......@@ -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
);
// API for Scaffold.addSnackBar():
// TODO(hansmuller): https://github.com/flutter/flutter/issues/374
assert(placeholderKey.currentState.child == null);
static Performance createPerformance() {
return new Performance(
duration: kSnackBarTransitionDuration,
debugLabel: 'SnackBar'
);
}
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;
});
SnackBar withPerformance(Performance newPerformance) {
return new SnackBar(
key: key,
content: content,
actions: actions,
duration: duration,
performance: newPerformance
);
}
}
......@@ -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) {
......
......@@ -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
);
}
......
......@@ -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(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(seconds: 1));
tester.pump(new Duration(seconds: 1));
tester.pump(new Duration(seconds: 1));
expect(showSnackBarThenCalled, isTrue);
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);
});
});
}
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