Commit e3d05449 authored by Hans Muller's avatar Hans Muller Committed by GitHub

Report why a snackbar was closed (#6996)

parent 1d4292f7
...@@ -537,7 +537,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin { ...@@ -537,7 +537,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
// SNACKBAR API // SNACKBAR API
Queue<ScaffoldFeatureController<SnackBar, Null>> _snackBars = new Queue<ScaffoldFeatureController<SnackBar, Null>>(); Queue<ScaffoldFeatureController<SnackBar, SnackBarClosedReason>> _snackBars = new Queue<ScaffoldFeatureController<SnackBar, SnackBarClosedReason>>();
AnimationController _snackBarController; AnimationController _snackBarController;
Timer _snackBarTimer; Timer _snackBarTimer;
...@@ -554,23 +554,23 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin { ...@@ -554,23 +554,23 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
/// [removeCurrentSnackBar]. /// [removeCurrentSnackBar].
/// ///
/// See [Scaffold.of] for information about how to obtain the [ScaffoldState]. /// See [Scaffold.of] for information about how to obtain the [ScaffoldState].
ScaffoldFeatureController<SnackBar, Null> showSnackBar(SnackBar snackbar) { ScaffoldFeatureController<SnackBar, SnackBarClosedReason> showSnackBar(SnackBar snackbar) {
_snackBarController ??= SnackBar.createAnimationController(vsync: this) _snackBarController ??= SnackBar.createAnimationController(vsync: this)
..addStatusListener(_handleSnackBarStatusChange); ..addStatusListener(_handleSnackBarStatusChange);
if (_snackBars.isEmpty) { if (_snackBars.isEmpty) {
assert(_snackBarController.isDismissed); assert(_snackBarController.isDismissed);
_snackBarController.forward(); _snackBarController.forward();
} }
ScaffoldFeatureController<SnackBar, Null> controller; ScaffoldFeatureController<SnackBar, SnackBarClosedReason> controller;
controller = new ScaffoldFeatureController<SnackBar, Null>._( controller = new ScaffoldFeatureController<SnackBar, SnackBarClosedReason>._(
// We provide a fallback key so that if back-to-back snackbars happen to // We provide a fallback key so that if back-to-back snackbars happen to
// match in structure, material ink splashes and highlights don't survive // match in structure, material ink splashes and highlights don't survive
// from one to the next. // from one to the next.
snackbar.withAnimation(_snackBarController, fallbackKey: new UniqueKey()), snackbar.withAnimation(_snackBarController, fallbackKey: new UniqueKey()),
new Completer<Null>(), new Completer<SnackBarClosedReason>(),
() { () {
assert(_snackBars.first == controller); assert(_snackBars.first == controller);
_hideSnackBar(); hideCurrentSnackBar(reason: SnackBarClosedReason.hide);
}, },
null // SnackBar doesn't use a builder function so setState() wouldn't rebuild it null // SnackBar doesn't use a builder function so setState() wouldn't rebuild it
); );
...@@ -606,27 +606,31 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin { ...@@ -606,27 +606,31 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
/// ///
/// The removed snack bar does not run its normal exit animation. If there are /// The removed snack bar does not run its normal exit animation. If there are
/// any queued snack bars, they begin their entrance animation immediately. /// any queued snack bars, they begin their entrance animation immediately.
void removeCurrentSnackBar() { void removeCurrentSnackBar({ SnackBarClosedReason reason: SnackBarClosedReason.remove }) {
assert(reason != null);
if (_snackBars.isEmpty) if (_snackBars.isEmpty)
return; return;
Completer<Null> completer = _snackBars.first._completer; final Completer<SnackBarClosedReason> completer = _snackBars.first._completer;
if (!completer.isCompleted) if (!completer.isCompleted)
completer.complete(); completer.complete(reason);
_snackBarTimer?.cancel(); _snackBarTimer?.cancel();
_snackBarTimer = null; _snackBarTimer = null;
_snackBarController.value = 0.0; _snackBarController.value = 0.0;
} }
void _hideSnackBar() { /// Removes the current [SnackBar] by running its normal exit animation.
assert(_snackBarController.status == AnimationStatus.forward || void hideCurrentSnackBar({ SnackBarClosedReason reason: SnackBarClosedReason.hide }) {
_snackBarController.status == AnimationStatus.completed); assert(reason != null);
_snackBars.first._completer.complete(); if (_snackBars.isEmpty || _snackBarController.status == AnimationStatus.dismissed)
return;
final Completer<SnackBarClosedReason> completer = _snackBars.first._completer;
if (!completer.isCompleted)
completer.complete(reason);
_snackBarController.reverse(); _snackBarController.reverse();
_snackBarTimer?.cancel(); _snackBarTimer?.cancel();
_snackBarTimer = null; _snackBarTimer = null;
} }
// PERSISTENT BOTTOM SHEET API // PERSISTENT BOTTOM SHEET API
final List<_PersistentBottomSheet> _dismissedBottomSheets = <_PersistentBottomSheet>[]; final List<_PersistentBottomSheet> _dismissedBottomSheets = <_PersistentBottomSheet>[];
...@@ -922,7 +926,11 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin { ...@@ -922,7 +926,11 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
final ModalRoute<dynamic> route = ModalRoute.of(context); final ModalRoute<dynamic> route = ModalRoute.of(context);
if (route == null || route.isCurrent) { if (route == null || route.isCurrent) {
if (_snackBarController.isCompleted && _snackBarTimer == null) if (_snackBarController.isCompleted && _snackBarTimer == null)
_snackBarTimer = new Timer(_snackBars.first._widget.duration, _hideSnackBar); _snackBarTimer = new Timer(_snackBars.first._widget.duration, () {
assert(_snackBarController.status == AnimationStatus.forward ||
_snackBarController.status == AnimationStatus.completed);
hideCurrentSnackBar(reason: SnackBarClosedReason.timeout);
});
} else { } else {
_snackBarTimer?.cancel(); _snackBarTimer?.cancel();
_snackBarTimer = null; _snackBarTimer = null;
......
...@@ -30,6 +30,40 @@ const Duration _kSnackBarDisplayDuration = const Duration(milliseconds: 1500); ...@@ -30,6 +30,40 @@ const Duration _kSnackBarDisplayDuration = const Duration(milliseconds: 1500);
const Curve _snackBarHeightCurve = Curves.fastOutSlowIn; const Curve _snackBarHeightCurve = Curves.fastOutSlowIn;
const Curve _snackBarFadeCurve = const Interval(0.72, 1.0, curve: Curves.fastOutSlowIn); const Curve _snackBarFadeCurve = const Interval(0.72, 1.0, curve: Curves.fastOutSlowIn);
/// Specify how a [SnackBar] was closed.
///
/// The [showSnackBar] function returns a [ScaffoldFeatureController]. The value
/// of the controller's closed property is a Future that resolves to a
/// SnackBarClosedReason. Applications that need to know how a snackbar
/// was closed can use this value.
///
/// Example:
///
/// ```dart
/// Scaffold.of(context).showSnackBar(
/// new SnackBar( ... )
/// ).closed.then((SnackBarClosedReason reason) {
/// ...
/// });
/// ```
enum SnackBarClosedReason {
/// The snack bar was closed after the user tapped a [SnackBarAction].
action,
/// The snack bar was closed by a user's swipe.
swipe,
/// The snack bar was closed by the [ScaffoldFeatureController] close callback
/// or by calling [hideCurrentSnackBar] directly.
hide,
/// The snack bar was closed by an call to [removeCurrentSnackBar].
remove,
/// The snack bar was closed because its timer expired.
timeout,
}
/// A button for a [SnackBar], known as an "action". /// A button for a [SnackBar], known as an "action".
/// ///
/// Snack bar actions are always enabled. If you want to disable a snack bar /// Snack bar actions are always enabled. If you want to disable a snack bar
...@@ -77,6 +111,7 @@ class _SnackBarActionState extends State<SnackBarAction> { ...@@ -77,6 +111,7 @@ class _SnackBarActionState extends State<SnackBarAction> {
_haveTriggeredAction = true; _haveTriggeredAction = true;
}); });
config.onPressed(); config.onPressed();
Scaffold.of(context).hideCurrentSnackBar(reason: SnackBarClosedReason.action);
} }
@override @override
...@@ -201,7 +236,7 @@ class SnackBar extends StatelessWidget { ...@@ -201,7 +236,7 @@ class SnackBar extends StatelessWidget {
direction: DismissDirection.down, direction: DismissDirection.down,
resizeDuration: null, resizeDuration: null,
onDismissed: (DismissDirection direction) { onDismissed: (DismissDirection direction) {
Scaffold.of(context).removeCurrentSnackBar(); Scaffold.of(context).removeCurrentSnackBar(reason: SnackBarClosedReason.swipe);
}, },
child: new Material( child: new Material(
elevation: 6, elevation: 6,
......
...@@ -130,7 +130,7 @@ void main() { ...@@ -130,7 +130,7 @@ void main() {
int snackBarCount = 0; int snackBarCount = 0;
Key tapTarget = new Key('tap-target'); Key tapTarget = new Key('tap-target');
int time; int time;
ScaffoldFeatureController<SnackBar, Null> lastController; ScaffoldFeatureController<SnackBar, SnackBarClosedReason> lastController;
await tester.pumpWidget(new MaterialApp( await tester.pumpWidget(new MaterialApp(
home: new Scaffold( home: new Scaffold(
body: new Builder( body: new Builder(
...@@ -158,7 +158,7 @@ void main() { ...@@ -158,7 +158,7 @@ void main() {
expect(find.text('bar2'), findsNothing); expect(find.text('bar2'), findsNothing);
time = 1000; time = 1000;
await tester.tap(find.byKey(tapTarget)); // queue bar1 await tester.tap(find.byKey(tapTarget)); // queue bar1
ScaffoldFeatureController<SnackBar, Null> firstController = lastController; ScaffoldFeatureController<SnackBar, SnackBarClosedReason> firstController = lastController;
time = 2; time = 2;
await tester.tap(find.byKey(tapTarget)); // queue bar2 await tester.tap(find.byKey(tapTarget)); // queue bar2
expect(find.text('bar1'), findsNothing); expect(find.text('bar1'), findsNothing);
...@@ -334,4 +334,79 @@ void main() { ...@@ -334,4 +334,79 @@ void main() {
expect(actionTextBottomLeft.x - textBottomRight.x, 24.0); expect(actionTextBottomLeft.x - textBottomRight.x, 24.0);
expect(snackBarBottomRight.x - actionTextBottomRight.x, 24.0); expect(snackBarBottomRight.x - actionTextBottomRight.x, 24.0);
}); });
testWidgets('SnackBarClosedReason', (WidgetTester tester) async {
final GlobalKey<ScaffoldState> scaffoldKey = new GlobalKey<ScaffoldState>();
bool actionPressed = false;
SnackBarClosedReason closedReason;
await tester.pumpWidget(new MaterialApp(
home: new Scaffold(
key: scaffoldKey,
body: new Builder(
builder: (BuildContext context) {
return new GestureDetector(
onTap: () {
Scaffold.of(context).showSnackBar(new SnackBar(
content: new Text('snack'),
duration: new Duration(seconds: 2),
action: new SnackBarAction(
label: 'ACTION',
onPressed: () {
actionPressed = true;
}
),
)).closed.then((SnackBarClosedReason reason) {
closedReason = reason;
});
},
child: new Text('X')
);
},
)
)
));
// Pop up the snack bar and then press its action button.
await tester.tap(find.text('X'));
await tester.pump(); // start animation
await tester.pump(const Duration(milliseconds: 750));
expect(actionPressed, isFalse);
await tester.tap(find.text('ACTION'));
expect(actionPressed, isTrue);
await tester.pump(const Duration(seconds: 1));
expect(closedReason, equals(SnackBarClosedReason.action));
// Pop up the snack bar and then swipe downwards to dismiss it.
await tester.tap(find.text('X'));
await tester.pump(const Duration(milliseconds: 750));
await tester.pump(const Duration(milliseconds: 750));
await tester.scroll(find.text('snack'), new Offset(0.0, 50.0));
await tester.pump();
expect(closedReason, equals(SnackBarClosedReason.swipe));
// Pop up the snack bar and then remove it.
await tester.tap(find.text('X'));
await tester.pump(const Duration(milliseconds: 750));
scaffoldKey.currentState.removeCurrentSnackBar();
await tester.pump(const Duration(seconds: 1));
expect(closedReason, equals(SnackBarClosedReason.remove));
// Pop up the snack bar and then hide it.
await tester.tap(find.text('X'));
await tester.pump(const Duration(milliseconds: 750));
scaffoldKey.currentState.hideCurrentSnackBar();
await tester.pump(const Duration(seconds: 1));
expect(closedReason, equals(SnackBarClosedReason.hide));
// Pop up the snack bar and then let it time out.
await tester.tap(find.text('X'));
await tester.pump(new Duration(milliseconds: 750));
await tester.pump(new Duration(milliseconds: 750));
await tester.pump(new Duration(milliseconds: 1500));
await tester.pump(); // begin animation
await tester.pump(new Duration(milliseconds: 750));
expect(closedReason, equals(SnackBarClosedReason.timeout));
});
} }
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