blob: 2daa02482c4786ada5a86e805346c5799339a147 [file] [log] [blame]
// 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/widgets.dart';
import 'button_theme.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 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 SafeArea(
top: false,
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,
);
}
}