1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'button.dart';
import 'flat_button.dart';
import 'material.dart';
import 'scaffold.dart';
import 'theme.dart';
import 'theme_data.dart';
// https://material.google.com/components/snackbars-toasts.html#snackbars-toasts-specs
const double _kSnackBarPadding = 24.0;
const double _kSingleLineVerticalPadding = 14.0;
const double _kMultiLineVerticalTopPadding = 24.0;
const double _kMultiLineVerticalSpaceBetweenTextAndButtons = 10.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 _kSnackBarDisplayDuration = const Duration(milliseconds: 1500);
const Curve _snackBarHeightCurve = Curves.fastOutSlowIn;
const Curve _snackBarFadeCurve = const Interval(0.72, 1.0, curve: Curves.fastOutSlowIn);
/// Specify how a [SnackBar] was closed.
///
/// The [ScaffoldState.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 [ScaffoldState.hideCurrentSnackBar] directly.
hide,
/// The snack bar was closed by an call to [ScaffoldState.removeCurrentSnackBar].
remove,
/// The snack bar was closed because its timer expired.
timeout,
}
/// A button for a [SnackBar], known as an "action".
///
/// Snack bar actions are always enabled. If you want to disable a snack bar
/// action, simply don't include it in the snack bar.
///
/// Snack bar actions can only be pressed once. Subsequent presses are ignored.
///
/// See also:
///
/// * [SnackBar]
/// * <https://material.google.com/components/snackbars-toasts.html>
class SnackBarAction extends StatefulWidget {
/// Creates an action for a [SnackBar].
///
/// The [label] and [onPressed] arguments must be non-null.
const SnackBarAction({
Key key,
@required this.label,
@required this.onPressed,
}) : assert(label != null),
assert(onPressed != null),
super(key: key);
/// The button label.
final String label;
/// The callback to be called when the button is pressed. Must not be null.
///
/// This callback will be called at most once each time this action is
/// displayed in a [SnackBar].
final VoidCallback onPressed;
@override
_SnackBarActionState createState() => new _SnackBarActionState();
}
class _SnackBarActionState extends State<SnackBarAction> {
bool _haveTriggeredAction = false;
void _handlePressed() {
if (_haveTriggeredAction)
return;
setState(() {
_haveTriggeredAction = true;
});
widget.onPressed();
Scaffold.of(context).hideCurrentSnackBar(reason: SnackBarClosedReason.action);
}
@override
Widget build(BuildContext context) {
return new FlatButton(
onPressed: _haveTriggeredAction ? null : _handlePressed,
child: new Text(widget.label),
);
}
}
/// A lightweight message with an optional action which briefly displays at the
/// bottom of the screen.
///
/// To display a snack bar, call `Scaffold.of(context).showSnackBar()`, passing
/// an instance of [SnackBar] that describes the message.
///
/// To control how long the [SnackBar] remains visible, specify a [duration].
///
/// See also:
///
/// * [Scaffold.of], to obtain the current [ScaffoldState], which manages the
/// display and animation of snack bars.
/// * [ScaffoldState.showSnackBar], which displays a [SnackBar].
/// * [ScaffoldState.removeCurrentSnackBar], which abruptly hides the currently
/// displayed snack bar, if any, and allows the next to be displayed.
/// * [SnackBarAction], which is used to specify an [action] button to show
/// on the snack bar.
/// * <https://material.google.com/components/snackbars-toasts.html>
class SnackBar extends StatelessWidget {
/// Creates a snack bar.
///
/// The [content] argument must be non-null.
const SnackBar({
Key key,
@required this.content,
this.backgroundColor,
this.action,
this.duration: _kSnackBarDisplayDuration,
this.animation,
}) : assert(content != null),
super(key: key);
/// The primary content of the snack bar.
///
/// Typically a [Text] widget.
final Widget content;
/// The Snackbar's background color. By default the color is dark grey.
final Color backgroundColor;
/// (optional) An action that the user can take based on the snack bar.
///
/// For example, the snack bar might let the user undo the operation that
/// prompted the snackbar. Snack bars can have at most one action.
///
/// The action should not be "dismiss" or "cancel".
final SnackBarAction action;
/// The amount of time the snack bar should be displayed.
///
/// Defaults to 1.5s.
///
/// See also:
///
/// * [ScaffoldState.removeCurrentSnackBar], which abruptly hides the
/// currently displayed snack bar, if any, and allows the next to be
/// displayed.
/// * <https://material.google.com/components/snackbars-toasts.html>
final Duration duration;
/// The animation driving the entrance and exit of the snack bar.
final Animation<double> animation;
@override
Widget build(BuildContext context) {
assert(animation != null);
final ThemeData theme = Theme.of(context);
final ThemeData darkTheme = new ThemeData(
brightness: Brightness.dark,
accentColor: theme.accentColor,
accentColorBrightness: theme.accentColorBrightness,
);
final List<Widget> children = <Widget>[
const SizedBox(width: _kSnackBarPadding),
new Expanded(
child: new Container(
padding: const EdgeInsets.symmetric(vertical: _kSingleLineVerticalPadding),
child: new DefaultTextStyle(
style: darkTheme.textTheme.subhead,
child: content,
),
),
),
];
if (action != null) {
children.add(new ButtonTheme.bar(
padding: const EdgeInsets.symmetric(horizontal: _kSnackBarPadding),
textTheme: ButtonTextTheme.accent,
child: action,
));
} else {
children.add(const SizedBox(width: _kSnackBarPadding));
}
final CurvedAnimation heightAnimation = new CurvedAnimation(parent: animation, curve: _snackBarHeightCurve);
final CurvedAnimation fadeAnimation = new CurvedAnimation(parent: animation, curve: _snackBarFadeCurve, reverseCurve: const Threshold(0.0));
return new ClipRect(
child: new AnimatedBuilder(
animation: heightAnimation,
builder: (BuildContext context, Widget child) {
return new Align(
alignment: AlignmentDirectional.topStart,
heightFactor: heightAnimation.value,
child: child,
);
},
child: new Semantics(
container: true,
child: new Dismissible(
key: const Key('dismissible'),
direction: DismissDirection.down,
resizeDuration: null,
onDismissed: (DismissDirection direction) {
Scaffold.of(context).removeCurrentSnackBar(reason: SnackBarClosedReason.swipe);
},
child: new Material(
elevation: 6.0,
color: backgroundColor ?? _kSnackBackground,
child: new Theme(
data: darkTheme,
child: new FadeTransition(
opacity: fadeAnimation,
child: new Row(
children: children,
crossAxisAlignment: CrossAxisAlignment.center,
),
),
),
),
),
),
),
);
}
// API for Scaffold.addSnackBar():
/// Creates an animation controller useful for driving a snack bar's entrance and exit animation.
static AnimationController createAnimationController({ @required TickerProvider vsync }) {
return new AnimationController(
duration: _kSnackBarTransitionDuration,
debugLabel: 'SnackBar',
vsync: vsync,
);
}
/// Creates a copy of this snack bar but with the animation replaced with the given animation.
///
/// If the original snack bar lacks a key, the newly created snack bar will
/// use the given fallback key.
SnackBar withAnimation(Animation<double> newAnimation, { Key fallbackKey }) {
return new SnackBar(
key: key ?? fallbackKey,
content: content,
backgroundColor: backgroundColor,
action: action,
duration: duration,
animation: newAnimation,
);
}
}