Commit 884ec65c authored by Adam Barth's avatar Adam Barth

Don't delay between dismissing a snack bar and the next snack bar

Previously, the Dismissable widget was animating down to zero width off
screen. Now, we tell it to skip the resize animation.

Fixes #3030
parent c51d3914
...@@ -125,6 +125,7 @@ class SnackBar extends StatelessWidget { ...@@ -125,6 +125,7 @@ class SnackBar extends StatelessWidget {
child: new Dismissable( child: new Dismissable(
key: new Key('dismissable'), key: new Key('dismissable'),
direction: DismissDirection.down, direction: DismissDirection.down,
resizeDuration: null,
onDismissed: (DismissDirection direction) { onDismissed: (DismissDirection direction) {
Scaffold.of(context).removeCurrentSnackBar(); Scaffold.of(context).removeCurrentSnackBar();
}, },
......
...@@ -8,7 +8,6 @@ import 'framework.dart'; ...@@ -8,7 +8,6 @@ import 'framework.dart';
import 'gesture_detector.dart'; import 'gesture_detector.dart';
const Duration _kDismissDuration = const Duration(milliseconds: 200); const Duration _kDismissDuration = const Duration(milliseconds: 200);
const Duration _kResizeDuration = const Duration(milliseconds: 300);
const Curve _kResizeTimeCurve = const Interval(0.4, 1.0, curve: Curves.ease); const Curve _kResizeTimeCurve = const Interval(0.4, 1.0, curve: Curves.ease);
const double _kMinFlingVelocity = 700.0; const double _kMinFlingVelocity = 700.0;
const double _kMinFlingVelocityDelta = 400.0; const double _kMinFlingVelocityDelta = 400.0;
...@@ -43,17 +42,19 @@ enum DismissDirection { ...@@ -43,17 +42,19 @@ enum DismissDirection {
/// Can be dismissed by dragging in the indicated [direction]. /// Can be dismissed by dragging in the indicated [direction].
/// ///
/// Dragging or flinging this widget in the [DismissDirection] causes the child /// Dragging or flinging this widget in the [DismissDirection] causes the child
/// to slide out of view. Following the slide animation, the Dismissable widget /// to slide out of view. Following the slide animation, if [resizeDuration] is
/// animates its height (or width, whichever is perpendicular to the dismiss /// non-null, the Dismissable widget animates its height (or width, whichever is
/// direction) to zero. /// perpendicular to the dismiss direction) to zero over the [resizeDuration].
/// ///
/// Backgrounds can be used to implement the "leave-behind" idiom. If a background /// Backgrounds can be used to implement the "leave-behind" idiom. If a background
/// is specified it is stacked behind the Dismissable's child and is exposed when /// is specified it is stacked behind the Dismissable's child and is exposed when
/// the child moves. /// the child moves.
/// ///
/// The [onDimissed] callback runs after Dismissable's size has collapsed to zero. /// The widget calls the [onDimissed] callback either after its size has
/// If the Dismissable is a list item, it must have a key that distinguishes it from /// collapsed to zero (if [resizeDuration] is non-null) or immediately after
/// the other items and its onDismissed callback must remove the item from the list. /// the slide animation (if [resizeDuration] is null). If the Dismissable is a
/// list item, it must have a key that distinguishes it from the other items and
/// its [onDismissed] callback must remove the item from the list.
class Dismissable extends StatefulWidget { class Dismissable extends StatefulWidget {
Dismissable({ Dismissable({
Key key, Key key,
...@@ -62,7 +63,8 @@ class Dismissable extends StatefulWidget { ...@@ -62,7 +63,8 @@ class Dismissable extends StatefulWidget {
this.secondaryBackground, this.secondaryBackground,
this.onResize, this.onResize,
this.onDismissed, this.onDismissed,
this.direction: DismissDirection.horizontal this.direction: DismissDirection.horizontal,
this.resizeDuration: const Duration(milliseconds: 300)
}) : super(key: key) { }) : super(key: key) {
assert(key != null); assert(key != null);
assert(secondaryBackground != null ? background != null : true); assert(secondaryBackground != null ? background != null : true);
...@@ -90,6 +92,12 @@ class Dismissable extends StatefulWidget { ...@@ -90,6 +92,12 @@ class Dismissable extends StatefulWidget {
/// The direction in which the widget can be dismissed. /// The direction in which the widget can be dismissed.
final DismissDirection direction; final DismissDirection direction;
/// The amount of time the widget will spend contracting before [onDismissed] is called.
///
/// If null, the widget will not contract and [onDismissed] will be called
/// immediately after the the widget is dismissed.
final Duration resizeDuration;
@override @override
_DismissableState createState() => new _DismissableState(); _DismissableState createState() => new _DismissableState();
} }
...@@ -253,18 +261,23 @@ class _DismissableState extends State<Dismissable> { ...@@ -253,18 +261,23 @@ class _DismissableState extends State<Dismissable> {
assert(_moveController != null); assert(_moveController != null);
assert(_moveController.isCompleted); assert(_moveController.isCompleted);
assert(_resizeController == null); assert(_resizeController == null);
_resizeController = new AnimationController(duration: _kResizeDuration) if (config.resizeDuration == null) {
..addListener(_handleResizeProgressChanged); if (config.onDismissed != null)
_resizeController.forward(); config.onDismissed(_dismissDirection);
setState(() { } else {
_resizeAnimation = new Tween<double>( _resizeController = new AnimationController(duration: config.resizeDuration)
begin: 1.0, ..addListener(_handleResizeProgressChanged);
end: 0.0 _resizeController.forward();
).animate(new CurvedAnimation( setState(() {
parent: _resizeController, _resizeAnimation = new Tween<double>(
curve: _kResizeTimeCurve begin: 1.0,
)); end: 0.0
}); ).animate(new CurvedAnimation(
parent: _resizeController,
curve: _kResizeTimeCurve
));
});
}
} }
void _handleResizeProgressChanged() { void _handleResizeProgressChanged() {
......
...@@ -230,4 +230,56 @@ void main() { ...@@ -230,4 +230,56 @@ void main() {
expect(tester.findText('bar2'), isNull); expect(tester.findText('bar2'), isNull);
}); });
}); });
test('SnackBar dismiss test', () {
testWidgets((WidgetTester tester) {
int snackBarCount = 0;
Key tapTarget = new Key('tap-target');
tester.pumpWidget(new MaterialApp(
routes: <String, WidgetBuilder>{
'/': (BuildContext context) {
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
tester.scroll(tester.findText('bar1'), new Offset(0.0, 50.0));
tester.pump(); // bar1 dismissed, bar2 begins animating
expect(tester.findText('bar1'), isNull);
expect(tester.findText('bar2'), isNotNull);
});
});
} }
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