snack_bar.dart 9.24 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.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 19
const double _kMultiLineVerticalTopPadding = 24.0;
const double _kMultiLineVerticalSpaceBetweenTextAndButtons = 10.0;
20
const Color _kSnackBackground = const Color(0xFF323232);
Matt Perry's avatar
Matt Perry committed
21

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

28
const Duration _kSnackBarTransitionDuration = const Duration(milliseconds: 250);
29
const Duration _kSnackBarDisplayDuration = const Duration(milliseconds: 1500);
30
const Curve _snackBarHeightCurve = Curves.fastOutSlowIn;
31
const Curve _snackBarFadeCurve = const Interval(0.72, 1.0, curve: Curves.fastOutSlowIn);
Hixie's avatar
Hixie committed
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
/// 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,
}

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]
77
///  * <https://material.google.com/components/snackbars-toasts.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 83
  SnackBarAction({
    Key key,
84
    @required this.label,
85 86
    @required this.onPressed
  }) : super(key: key) {
87
    assert(label != null);
88
    assert(onPressed != null);
89 90
  }

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

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

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

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

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

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

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

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

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

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

  /// The amount of time the snack bar should be displayed.
176 177 178 179 180 181 182 183 184
  ///
  /// 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
185
  final Duration duration;
186 187

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

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

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

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

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