Commit ffb95f05 authored by Ian Hickson's avatar Ian Hickson

Merge pull request #517 from Hixie/snackbar-dismiss

Expose the close()/closed API for snack bars
parents 98333cb2 ee8c0ad3
...@@ -102,20 +102,31 @@ class ScaffoldState extends State<Scaffold> { ...@@ -102,20 +102,31 @@ class ScaffoldState extends State<Scaffold> {
// SNACKBAR API // SNACKBAR API
Queue<SnackBar> _snackBars = new Queue<SnackBar>(); Queue<ScaffoldFeatureController<SnackBar>> _snackBars = new Queue<ScaffoldFeatureController<SnackBar>>();
Performance _snackBarPerformance; Performance _snackBarPerformance;
Timer _snackBarTimer; Timer _snackBarTimer;
void showSnackBar(SnackBar snackbar) { ScaffoldFeatureController showSnackBar(SnackBar snackbar) {
_snackBarPerformance ??= SnackBar.createPerformance() _snackBarPerformance ??= SnackBar.createPerformance()
..addStatusListener(_handleSnackBarStatusChange); ..addStatusListener(_handleSnackBarStatusChange);
if (_snackBars.isEmpty) { if (_snackBars.isEmpty) {
assert(_snackBarPerformance.isDismissed); assert(_snackBarPerformance.isDismissed);
_snackBarPerformance.forward(); _snackBarPerformance.forward();
} }
ScaffoldFeatureController<SnackBar> controller;
controller = new ScaffoldFeatureController<SnackBar>._(
snackbar.withPerformance(_snackBarPerformance),
new Completer(),
() {
assert(_snackBars.first == controller);
_hideSnackBar();
},
null // SnackBar doesn't use a builder function so setState() wouldn't rebuild it
);
setState(() { setState(() {
_snackBars.addLast(snackbar.withPerformance(_snackBarPerformance)); _snackBars.addLast(controller);
}); });
return controller;
} }
void _handleSnackBarStatusChange(PerformanceStatus status) { void _handleSnackBarStatusChange(PerformanceStatus status) {
...@@ -141,6 +152,9 @@ class ScaffoldState extends State<Scaffold> { ...@@ -141,6 +152,9 @@ class ScaffoldState extends State<Scaffold> {
} }
void _hideSnackBar() { void _hideSnackBar() {
assert(_snackBarPerformance.status == PerformanceStatus.forward ||
_snackBarPerformance.status == PerformanceStatus.completed);
_snackBars.first._completer.complete();
_snackBarPerformance.reverse(); _snackBarPerformance.reverse();
_snackBarTimer = null; _snackBarTimer = null;
} }
...@@ -149,9 +163,9 @@ class ScaffoldState extends State<Scaffold> { ...@@ -149,9 +163,9 @@ class ScaffoldState extends State<Scaffold> {
// PERSISTENT BOTTOM SHEET API // PERSISTENT BOTTOM SHEET API
List<Widget> _dismissedBottomSheets; List<Widget> _dismissedBottomSheets;
BottomSheetController _currentBottomSheet; ScaffoldFeatureController _currentBottomSheet;
BottomSheetController showBottomSheet(WidgetBuilder builder) { ScaffoldFeatureController showBottomSheet(WidgetBuilder builder) {
if (_currentBottomSheet != null) { if (_currentBottomSheet != null) {
_currentBottomSheet.close(); _currentBottomSheet.close();
assert(_currentBottomSheet == null); assert(_currentBottomSheet == null);
...@@ -189,9 +203,9 @@ class ScaffoldState extends State<Scaffold> { ...@@ -189,9 +203,9 @@ class ScaffoldState extends State<Scaffold> {
); );
Navigator.of(context).push(route); Navigator.of(context).push(route);
setState(() { setState(() {
_currentBottomSheet = new BottomSheetController._( _currentBottomSheet = new ScaffoldFeatureController._(
bottomSheet, bottomSheet,
completer.future, completer,
() => Navigator.of(context).remove(route), () => Navigator.of(context).remove(route),
setState setState
); );
...@@ -223,7 +237,7 @@ class ScaffoldState extends State<Scaffold> { ...@@ -223,7 +237,7 @@ class ScaffoldState extends State<Scaffold> {
ModalRoute route = ModalRoute.of(context); ModalRoute route = ModalRoute.of(context);
if (route == null || route.isCurrent) { if (route == null || route.isCurrent) {
if (_snackBarPerformance.isCompleted && _snackBarTimer == null) if (_snackBarPerformance.isCompleted && _snackBarTimer == null)
_snackBarTimer = new Timer(_snackBars.first.duration, _hideSnackBar); _snackBarTimer = new Timer(_snackBars.first._widget.duration, _hideSnackBar);
} else { } else {
_snackBarTimer?.cancel(); _snackBarTimer?.cancel();
_snackBarTimer = null; _snackBarTimer = null;
...@@ -249,7 +263,7 @@ class ScaffoldState extends State<Scaffold> { ...@@ -249,7 +263,7 @@ class ScaffoldState extends State<Scaffold> {
} }
if (_snackBars.isNotEmpty) if (_snackBars.isNotEmpty)
_addIfNonNull(children, _snackBars.first, _Child.snackBar); _addIfNonNull(children, _snackBars.first._widget, _Child.snackBar);
_addIfNonNull(children, config.floatingActionButton, _Child.floatingActionButton); _addIfNonNull(children, config.floatingActionButton, _Child.floatingActionButton);
...@@ -258,11 +272,12 @@ class ScaffoldState extends State<Scaffold> { ...@@ -258,11 +272,12 @@ class ScaffoldState extends State<Scaffold> {
} }
class BottomSheetController { class ScaffoldFeatureController<T extends Widget> {
const BottomSheetController._(this._widget, this.closed, this.close, this.setState); const ScaffoldFeatureController._(this._widget, this._completer, this.close, this.setState);
final Widget _widget; final T _widget;
final Future closed; final Completer _completer;
final VoidCallback close; // call this to close the bottom sheet Future get closed => _completer.future;
final VoidCallback close; // call this to close the bottom sheet or snack bar
final StateSetter setState; final StateSetter setState;
} }
......
...@@ -145,4 +145,96 @@ void main() { ...@@ -145,4 +145,96 @@ void main() {
expect(tester.findText('bar2'), isNull); expect(tester.findText('bar2'), isNull);
}); });
}); });
test('SnackBar cancel test', () {
testWidgets((WidgetTester tester) {
int snackBarCount = 0;
Key tapTarget = new Key('tap-target');
int time;
ScaffoldFeatureController<SnackBar> lastController;
tester.pumpWidget(new MaterialApp(
routes: <String, RouteBuilder>{
'/': (RouteArguments args) {
return new Scaffold(
body: new Builder(
builder: (BuildContext context) {
return new GestureDetector(
onTap: () {
snackBarCount += 1;
lastController = Scaffold.of(context).showSnackBar(new SnackBar(
content: new Text("bar$snackBarCount"),
duration: new Duration(seconds: time)
));
},
behavior: HitTestBehavior.opaque,
child: new Container(
height: 100.0,
width: 100.0,
key: tapTarget
)
);
}
)
);
}
}
));
expect(tester.findText('bar1'), isNull);
expect(tester.findText('bar2'), isNull);
time = 1000;
tester.tap(tester.findElementByKey(tapTarget)); // queue bar1
ScaffoldFeatureController<SnackBar> firstController = lastController;
time = 2;
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: 10000)); // 12.25s
expect(tester.findText('bar1'), isNotNull);
expect(tester.findText('bar2'), isNull);
firstController.close(); // snackbar is manually dismissed
tester.pump(new Duration(milliseconds: 750)); // 13.00s // reverse animation is scheduled
tester.pump(); // begin animation
expect(tester.findText('bar1'), isNotNull);
expect(tester.findText('bar2'), isNull);
tester.pump(new Duration(milliseconds: 750)); // 13.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)); // 14.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)); // 15.25s
expect(tester.findText('bar1'), isNull);
expect(tester.findText('bar2'), isNotNull);
tester.pump(new Duration(milliseconds: 750)); // 16.00s
expect(tester.findText('bar1'), isNull);
expect(tester.findText('bar2'), isNotNull);
tester.pump(new Duration(milliseconds: 750)); // 16.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)); // 17.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