snack_bar.dart 9.55 KB
Newer Older
1 2 3 4
// 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.

5
import 'package:flutter/rendering.dart';
6
import 'package:flutter/widgets.dart';
7

8
import 'button_theme.dart';
9
import 'flat_button.dart';
10
import 'material.dart';
11
import 'scaffold.dart';
12
import 'theme.dart';
13
import 'theme_data.dart';
Matt Perry's avatar
Matt Perry committed
14

15
const double _kSnackBarPadding = 24.0;
Hixie's avatar
Hixie committed
16
const double _kSingleLineVerticalPadding = 14.0;
17
const Color _kSnackBackground = Color(0xFF323232);
Matt Perry's avatar
Matt Perry committed
18

Hixie's avatar
Hixie committed
19 20 21 22 23 24
// 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".

25 26
const Duration _kSnackBarTransitionDuration = Duration(milliseconds: 250);
const Duration _kSnackBarDisplayDuration = Duration(milliseconds: 4000);
27
const Curve _snackBarHeightCurve = Curves.fastOutSlowIn;
28
const Curve _snackBarFadeCurve = Interval(0.72, 1.0, curve: Curves.fastOutSlowIn);
Hixie's avatar
Hixie committed
29

30 31
/// Specify how a [SnackBar] was closed.
///
32 33 34 35
/// 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.
36 37 38 39 40
///
/// Example:
///
/// ```dart
/// Scaffold.of(context).showSnackBar(
41
///   SnackBar( ... )
42 43 44 45 46 47 48 49
/// ).closed.then((SnackBarClosedReason reason) {
///    ...
/// });
/// ```
enum SnackBarClosedReason {
  /// The snack bar was closed after the user tapped a [SnackBarAction].
  action,

50 51 52
  /// The snack bar was closed through a [SemanticAction.dismiss].
  dismiss,

53 54 55 56
  /// The snack bar was closed by a user's swipe.
  swipe,

  /// The snack bar was closed by the [ScaffoldFeatureController] close callback
57
  /// or by calling [ScaffoldState.hideCurrentSnackBar] directly.
58 59
  hide,

60
  /// The snack bar was closed by an call to [ScaffoldState.removeCurrentSnackBar].
61 62 63 64 65 66
  remove,

  /// The snack bar was closed because its timer expired.
  timeout,
}

67 68 69 70 71
/// 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.
///
72 73
/// Snack bar actions can only be pressed once. Subsequent presses are ignored.
///
74 75 76
/// See also:
///
///  * [SnackBar]
jslavitz's avatar
jslavitz committed
77
///  * <https://material.io/design/components/snackbars.html>
78
class SnackBarAction extends StatefulWidget {
79 80 81
  /// Creates an action for a [SnackBar].
  ///
  /// The [label] and [onPressed] arguments must be non-null.
82
  const SnackBarAction({
83
    Key key,
84
    @required this.label,
85
    @required this.onPressed,
86 87 88
  }) : assert(label != null),
       assert(onPressed != null),
       super(key: key);
89

90
  /// The button label.
91
  final String label;
92

93
  /// The callback to be called when the button is pressed. Must not be null.
94
  ///
95
  /// This callback will be called at most once each time this action is
96
  /// displayed in a [SnackBar].
97
  final VoidCallback onPressed;
98

99
  @override
100
  _SnackBarActionState createState() => _SnackBarActionState();
101 102 103 104 105 106 107 108 109 110 111
}

class _SnackBarActionState extends State<SnackBarAction> {
  bool _haveTriggeredAction = false;

  void _handlePressed() {
    if (_haveTriggeredAction)
      return;
    setState(() {
      _haveTriggeredAction = true;
    });
112
    widget.onPressed();
113
    Scaffold.of(context).hideCurrentSnackBar(reason: SnackBarClosedReason.action);
114 115
  }

116
  @override
Hixie's avatar
Hixie committed
117
  Widget build(BuildContext context) {
118
    return FlatButton(
119
      onPressed: _haveTriggeredAction ? null : _handlePressed,
120
      child: Text(widget.label),
121 122 123
    );
  }
}
124

125 126 127
/// A lightweight message with an optional action which briefly displays at the
/// bottom of the screen.
///
128 129 130 131
/// 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].
132
///
133 134 135
/// A SnackBar with an action will not time out when TalkBack or VoiceOver are
/// enabled. This is controlled by [AccessibilityFeatures.accessibleNavigation].
///
136
/// See also:
137
///
138 139 140 141 142 143 144
///  * [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.
jslavitz's avatar
jslavitz committed
145
///  * <https://material.io/design/components/snackbars.html>
146
class SnackBar extends StatelessWidget {
147 148 149
  /// Creates a snack bar.
  ///
  /// The [content] argument must be non-null.
150
  const SnackBar({
151
    Key key,
152
    @required this.content,
153
    this.backgroundColor,
154
    this.action,
155
    this.duration = _kSnackBarDisplayDuration,
156
    this.animation,
157
  }) : assert(content != null),
158
       assert(duration != null),
159
       super(key: key);
160

161 162 163
  /// The primary content of the snack bar.
  ///
  /// Typically a [Text] widget.
164
  final Widget content;
165

166 167 168
  /// The Snackbar's background color. By default the color is dark grey.
  final Color backgroundColor;

169 170 171 172
  /// (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.
173 174
  ///
  /// The action should not be "dismiss" or "cancel".
175
  final SnackBarAction action;
176 177

  /// The amount of time the snack bar should be displayed.
178
  ///
jslavitz's avatar
jslavitz committed
179
  /// Defaults to 4.0s.
180 181 182 183 184 185
  ///
  /// See also:
  ///
  ///  * [ScaffoldState.removeCurrentSnackBar], which abruptly hides the
  ///    currently displayed snack bar, if any, and allows the next to be
  ///    displayed.
jslavitz's avatar
jslavitz committed
186
  ///  * <https://material.io/design/components/snackbars.html>
Hixie's avatar
Hixie committed
187
  final Duration duration;
188 189

  /// The animation driving the entrance and exit of the snack bar.
190
  final Animation<double> animation;
191

192
  @override
193
  Widget build(BuildContext context) {
194
    final MediaQueryData mediaQueryData = MediaQuery.of(context);
195
    assert(animation != null);
196
    final ThemeData theme = Theme.of(context);
197
    final ThemeData darkTheme = ThemeData(
198 199
      brightness: Brightness.dark,
      accentColor: theme.accentColor,
200
      accentColorBrightness: theme.accentColorBrightness,
201
    );
202
    final List<Widget> children = <Widget>[
203
      const SizedBox(width: _kSnackBarPadding),
204 205
      Expanded(
        child: Container(
206
          padding: const EdgeInsets.symmetric(vertical: _kSingleLineVerticalPadding),
207
          child: DefaultTextStyle(
208
            style: darkTheme.textTheme.subhead,
209
            child: content,
210 211 212
          ),
        ),
      ),
213
    ];
214
    if (action != null) {
215
      children.add(ButtonTheme.bar(
216 217
        padding: const EdgeInsets.symmetric(horizontal: _kSnackBarPadding),
        textTheme: ButtonTextTheme.accent,
218
        child: action,
219 220 221 222
      ));
    } else {
      children.add(const SizedBox(width: _kSnackBarPadding));
    }
223 224 225
    final CurvedAnimation heightAnimation = CurvedAnimation(parent: animation, curve: _snackBarHeightCurve);
    final CurvedAnimation fadeAnimation = CurvedAnimation(parent: animation, curve: _snackBarFadeCurve, reverseCurve: const Threshold(0.0));
    Widget snackbar = SafeArea(
226
      top: false,
227
      child: Row(
228 229 230 231
        children: children,
        crossAxisAlignment: CrossAxisAlignment.center,
      ),
    );
232
    snackbar = Semantics(
233 234 235 236 237
      container: true,
      liveRegion: true,
      onDismiss: () {
        Scaffold.of(context).removeCurrentSnackBar(reason: SnackBarClosedReason.dismiss);
      },
238
      child: Dismissible(
239 240 241 242 243 244
        key: const Key('dismissible'),
        direction: DismissDirection.down,
        resizeDuration: null,
        onDismissed: (DismissDirection direction) {
          Scaffold.of(context).removeCurrentSnackBar(reason: SnackBarClosedReason.swipe);
        },
245
        child: Material(
246 247
          elevation: 6.0,
          color: backgroundColor ?? _kSnackBackground,
248
          child: Theme(
249
            data: darkTheme,
250
            child: mediaQueryData.accessibleNavigation ? snackbar : FadeTransition(
251 252 253 254 255 256 257
              opacity: fadeAnimation,
              child: snackbar,
            ),
          ),
        ),
      ),
    );
258 259
    return ClipRect(
      child: mediaQueryData.accessibleNavigation ? snackbar : AnimatedBuilder(
260
        animation: heightAnimation,
261
        builder: (BuildContext context, Widget child) {
262
          return Align(
263
            alignment: AlignmentDirectional.topStart,
264
            heightFactor: heightAnimation.value,
265
            child: child,
266
          );
267
        },
268
        child: snackbar,
269
      ),
270
    );
271
  }
272

Hixie's avatar
Hixie committed
273
  // API for Scaffold.addSnackBar():
274

275
  /// Creates an animation controller useful for driving a snack bar's entrance and exit animation.
276
  static AnimationController createAnimationController({ @required TickerProvider vsync }) {
277
    return AnimationController(
278
      duration: _kSnackBarTransitionDuration,
279 280
      debugLabel: 'SnackBar',
      vsync: vsync,
Hixie's avatar
Hixie committed
281 282
    );
  }
283

284 285 286 287
  /// 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.
288
  SnackBar withAnimation(Animation<double> newAnimation, { Key fallbackKey }) {
289
    return SnackBar(
290
      key: key ?? fallbackKey,
Hixie's avatar
Hixie committed
291
      content: content,
292
      backgroundColor: backgroundColor,
293
      action: action,
Hixie's avatar
Hixie committed
294
      duration: duration,
295
      animation: newAnimation,
Hixie's avatar
Hixie committed
296 297
    );
  }
298
}