snack_bar.dart 9.31 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/foundation.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
// https://material.google.com/components/snackbars-toasts.html#snackbars-toasts-specs
16
const double _kSnackBarPadding = 24.0;
Hixie's avatar
Hixie committed
17
const double _kSingleLineVerticalPadding = 14.0;
18
const Color _kSnackBackground = const Color(0xFF323232);
Matt Perry's avatar
Matt Perry committed
19

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

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

31 32
/// Specify how a [SnackBar] was closed.
///
33 34 35 36
/// 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.
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
///
/// 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
55
  /// or by calling [ScaffoldState.hideCurrentSnackBar] directly.
56 57
  hide,

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

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

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

88
  /// The button label.
89
  final String label;
90

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

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

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

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

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

123 124 125
/// A lightweight message with an optional action which briefly displays at the
/// bottom of the screen.
///
126 127 128 129
/// 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].
130 131
///
/// See also:
132
///
133 134 135 136 137 138 139
///  * [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.
140
///  * <https://material.google.com/components/snackbars-toasts.html>
141
class SnackBar extends StatelessWidget {
142 143 144
  /// Creates a snack bar.
  ///
  /// The [content] argument must be non-null.
145
  const SnackBar({
146
    Key key,
147
    @required this.content,
148
    this.backgroundColor,
149
    this.action,
150
    this.duration: _kSnackBarDisplayDuration,
151
    this.animation,
152 153
  }) : assert(content != null),
       super(key: key);
154

155 156 157
  /// The primary content of the snack bar.
  ///
  /// Typically a [Text] widget.
158
  final Widget content;
159

160 161 162
  /// The Snackbar's background color. By default the color is dark grey.
  final Color backgroundColor;

163 164 165 166
  /// (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.
167 168
  ///
  /// The action should not be "dismiss" or "cancel".
169
  final SnackBarAction action;
170 171

  /// The amount of time the snack bar should be displayed.
172 173 174 175 176 177 178 179 180
  ///
  /// 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>
Hixie's avatar
Hixie committed
181
  final Duration duration;
182 183

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

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

Hixie's avatar
Hixie committed
260
  // API for Scaffold.addSnackBar():
261

262
  /// Creates an animation controller useful for driving a snack bar's entrance and exit animation.
263
  static AnimationController createAnimationController({ @required TickerProvider vsync }) {
264
    return new AnimationController(
265
      duration: _kSnackBarTransitionDuration,
266 267
      debugLabel: 'SnackBar',
      vsync: vsync,
Hixie's avatar
Hixie committed
268 269
    );
  }
270

271 272 273 274
  /// 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.
275
  SnackBar withAnimation(Animation<double> newAnimation, { Key fallbackKey }) {
Hixie's avatar
Hixie committed
276
    return new SnackBar(
277
      key: key ?? fallbackKey,
Hixie's avatar
Hixie committed
278
      content: content,
279
      backgroundColor: backgroundColor,
280
      action: action,
Hixie's avatar
Hixie committed
281
      duration: duration,
282
      animation: newAnimation,
Hixie's avatar
Hixie committed
283 284
    );
  }
285
}