blob: 9c6656b0141d23cd3bb14661f483cb03058a028b [file] [log] [blame]
// Copyright 2014 The Flutter 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/rendering.dart';
import 'package:flutter/widgets.dart';
import 'button_theme.dart';
import 'color_scheme.dart';
import 'flat_button.dart';
import 'material.dart';
import 'scaffold.dart';
import 'snack_bar_theme.dart';
import 'theme.dart';
import 'theme_data.dart';
const double _singleLineVerticalPadding = 14.0;
// 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 _snackBarTransitionDuration = Duration(milliseconds: 250);
const Duration _snackBarDisplayDuration = Duration(milliseconds: 4000);
const Curve _snackBarHeightCurve = Curves.fastOutSlowIn;
const Curve _snackBarFadeInCurve = Interval(0.45, 1.0, curve: Curves.fastOutSlowIn);
const Curve _snackBarFadeOutCurve = 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(
/// SnackBar( ... )
/// ).closed.then((SnackBarClosedReason reason) {
/// ...
/// });
/// ```
enum SnackBarClosedReason {
/// The snack bar was closed after the user tapped a [SnackBarAction].
action,
/// The snack bar was closed through a [SemanticAction.dismiss].
dismiss,
/// 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.io/design/components/snackbars.html>
class SnackBarAction extends StatefulWidget {
/// Creates an action for a [SnackBar].
///
/// The [label] and [onPressed] arguments must be non-null.
const SnackBarAction({
Key key,
this.textColor,
this.disabledTextColor,
@required this.label,
@required this.onPressed,
}) : assert(label != null),
assert(onPressed != null),
super(key: key);
/// The button label color. If not provided, defaults to [accentColor].
final Color textColor;
/// The button disabled label color. This color is shown after the
/// [snackBarAction] is dismissed.
final Color disabledTextColor;
/// 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() => _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) {
final SnackBarThemeData snackBarTheme = Theme.of(context).snackBarTheme;
final Color textColor = widget.textColor ?? snackBarTheme.actionTextColor;
final Color disabledTextColor = widget.disabledTextColor ?? snackBarTheme.disabledActionTextColor;
return FlatButton(
onPressed: _haveTriggeredAction ? null : _handlePressed,
child: Text(widget.label),
textColor: textColor,
disabledTextColor: disabledTextColor,
);
}
}
/// 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].
///
/// A SnackBar with an action will not time out when TalkBack or VoiceOver are
/// enabled. This is controlled by [AccessibilityFeatures.accessibleNavigation].
///
/// 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.
/// * [SnackBarThemeData], to configure the default property values for
/// [SnackBar] widgets.
/// * <https://material.io/design/components/snackbars.html>
class SnackBar extends StatefulWidget {
/// Creates a snack bar.
///
/// The [content] argument must be non-null. The [elevation] must be null or
/// non-negative.
const SnackBar({
Key key,
@required this.content,
this.backgroundColor,
this.elevation,
this.shape,
this.behavior,
this.action,
this.duration = _snackBarDisplayDuration,
this.animation,
this.onVisible,
}) : assert(elevation == null || elevation >= 0.0),
assert(content != null),
assert(duration != null),
super(key: key);
/// The primary content of the snack bar.
///
/// Typically a [Text] widget.
final Widget content;
/// The Snackbar's background color. If not specified it will use
/// [ThemeData.snackBarTheme.backgroundColor]. If that is not specified
/// it will default to a dark variation of [ColorScheme.surface] for light
/// themes, or [ColorScheme.onSurface] for dark themes.
final Color backgroundColor;
/// The z-coordinate at which to place the snack bar. This controls the size
/// of the shadow below the snack bar.
///
/// Defines the card's [Material.elevation].
///
/// If this property is null, then [ThemeData.snackBarTheme.elevation] is
/// used, if that is also null, the default value is 6.0.
final double elevation;
/// The shape of the snack bar's [Material].
///
/// Defines the snack bar's [Material.shape].
///
/// If this property is null then [ThemeData.snackBarTheme.shape] is used.
/// If that's null then the shape will depend on the [SnackBarBehavior]. For
/// [SnackBarBehavior.fixed], no overriding shape is specified, so the
/// [SnackBar] is rectangular. For [SnackBarBehavior.floating], it uses a
/// [RoundedRectangleBorder] with a circular corner radius of 4.0.
final ShapeBorder shape;
/// This defines the behavior and location of the snack bar.
///
/// Defines where a [SnackBar] should appear within a [Scaffold] and how its
/// location should be adjusted when the scaffold also includes a
/// [FloatingActionButton] or a [BottomNavigationBar]
///
/// If this property is null, then [ThemeData.snackBarTheme.behavior]
/// is used. If that is null, then the default is [SnackBarBehavior.fixed].
final SnackBarBehavior behavior;
/// (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 4.0s.
///
/// See also:
///
/// * [ScaffoldState.removeCurrentSnackBar], which abruptly hides the
/// currently displayed snack bar, if any, and allows the next to be
/// displayed.
/// * <https://material.io/design/components/snackbars.html>
final Duration duration;
/// The animation driving the entrance and exit of the snack bar.
final Animation<double> animation;
/// Called the first time that the snackbar is visible within a [Scaffold].
final VoidCallback onVisible;
// API for Scaffold.showSnackBar():
/// Creates an animation controller useful for driving a snack bar's entrance and exit animation.
static AnimationController createAnimationController({ @required TickerProvider vsync }) {
return AnimationController(
duration: _snackBarTransitionDuration,
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 SnackBar(
key: key ?? fallbackKey,
content: content,
backgroundColor: backgroundColor,
elevation: elevation,
shape: shape,
behavior: behavior,
action: action,
duration: duration,
animation: newAnimation,
onVisible: onVisible,
);
}
@override
State<SnackBar> createState() => _SnackBarState();
}
class _SnackBarState extends State<SnackBar> {
bool _wasVisible = false;
@override
void initState() {
super.initState();
widget.animation.addStatusListener(_onAnimationStatusChanged);
}
@override
void didUpdateWidget(SnackBar oldWidget) {
if (widget.animation != oldWidget.animation) {
oldWidget.animation.removeStatusListener(_onAnimationStatusChanged);
widget.animation.addStatusListener(_onAnimationStatusChanged);
}
super.didUpdateWidget(oldWidget);
}
@override
void dispose() {
widget.animation.removeStatusListener(_onAnimationStatusChanged);
super.dispose();
}
void _onAnimationStatusChanged(AnimationStatus animationStatus) {
switch (animationStatus) {
case AnimationStatus.dismissed:
case AnimationStatus.forward:
case AnimationStatus.reverse:
break;
case AnimationStatus.completed:
if (widget.onVisible != null && !_wasVisible) {
widget.onVisible();
}
_wasVisible = true;
}
}
@override
Widget build(BuildContext context) {
final MediaQueryData mediaQueryData = MediaQuery.of(context);
assert(widget.animation != null);
final ThemeData theme = Theme.of(context);
final ColorScheme colorScheme = theme.colorScheme;
final SnackBarThemeData snackBarTheme = theme.snackBarTheme;
final bool isThemeDark = theme.brightness == Brightness.dark;
// SnackBar uses a theme that is the opposite brightness from
// the surrounding theme.
final Brightness brightness = isThemeDark ? Brightness.light : Brightness.dark;
final Color themeBackgroundColor = isThemeDark
? colorScheme.onSurface
: Color.alphaBlend(colorScheme.onSurface.withOpacity(0.80), colorScheme.surface);
final ThemeData inverseTheme = ThemeData(
brightness: brightness,
backgroundColor: themeBackgroundColor,
colorScheme: ColorScheme(
primary: colorScheme.onPrimary,
primaryVariant: colorScheme.onPrimary,
// For the button color, the spec says it should be primaryVariant, but for
// backward compatibility on light themes we are leaving it as secondary.
secondary: isThemeDark ? colorScheme.primaryVariant : colorScheme.secondary,
secondaryVariant: colorScheme.onSecondary,
surface: colorScheme.onSurface,
background: themeBackgroundColor,
error: colorScheme.onError,
onPrimary: colorScheme.primary,
onSecondary: colorScheme.secondary,
onSurface: colorScheme.surface,
onBackground: colorScheme.background,
onError: colorScheme.error,
brightness: brightness,
),
snackBarTheme: snackBarTheme,
);
final TextStyle contentTextStyle = snackBarTheme.contentTextStyle ?? inverseTheme.textTheme.subtitle1;
final SnackBarBehavior snackBarBehavior = widget.behavior ?? snackBarTheme.behavior ?? SnackBarBehavior.fixed;
final bool isFloatingSnackBar = snackBarBehavior == SnackBarBehavior.floating;
final double snackBarPadding = isFloatingSnackBar ? 16.0 : 24.0;
final CurvedAnimation heightAnimation = CurvedAnimation(parent: widget.animation, curve: _snackBarHeightCurve);
final CurvedAnimation fadeInAnimation = CurvedAnimation(parent: widget.animation, curve: _snackBarFadeInCurve);
final CurvedAnimation fadeOutAnimation = CurvedAnimation(
parent: widget.animation,
curve: _snackBarFadeOutCurve,
reverseCurve: const Threshold(0.0),
);
Widget snackBar = SafeArea(
top: false,
bottom: !isFloatingSnackBar,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
SizedBox(width: snackBarPadding),
Expanded(
child: Container(
padding: const EdgeInsets.symmetric(vertical: _singleLineVerticalPadding),
child: DefaultTextStyle(
style: contentTextStyle,
child: widget.content,
),
),
),
if (widget.action != null)
ButtonTheme(
textTheme: ButtonTextTheme.accent,
minWidth: 64.0,
padding: EdgeInsets.symmetric(horizontal: snackBarPadding),
child: widget.action,
)
else
SizedBox(width: snackBarPadding),
],
),
);
final double elevation = widget.elevation ?? snackBarTheme.elevation ?? 6.0;
final Color backgroundColor = widget.backgroundColor ?? snackBarTheme.backgroundColor ?? inverseTheme.backgroundColor;
final ShapeBorder shape = widget.shape
?? snackBarTheme.shape
?? (isFloatingSnackBar ? RoundedRectangleBorder(borderRadius: BorderRadius.circular(4.0)) : null);
snackBar = Material(
shape: shape,
elevation: elevation,
color: backgroundColor,
child: Theme(
data: inverseTheme,
child: mediaQueryData.accessibleNavigation
? snackBar
: FadeTransition(
opacity: fadeOutAnimation,
child: snackBar,
),
),
);
if (isFloatingSnackBar) {
snackBar = Padding(
padding: const EdgeInsets.fromLTRB(15.0, 5.0, 15.0, 10.0),
child: snackBar,
);
}
snackBar = Semantics(
container: true,
liveRegion: true,
onDismiss: () {
Scaffold.of(context).removeCurrentSnackBar(reason: SnackBarClosedReason.dismiss);
},
child: Dismissible(
key: const Key('dismissible'),
direction: DismissDirection.down,
resizeDuration: null,
onDismissed: (DismissDirection direction) {
Scaffold.of(context).removeCurrentSnackBar(reason: SnackBarClosedReason.swipe);
},
child: snackBar,
),
);
Widget snackBarTransition;
if (mediaQueryData.accessibleNavigation) {
snackBarTransition = snackBar;
} else if (isFloatingSnackBar) {
snackBarTransition = FadeTransition(
opacity: fadeInAnimation,
child: snackBar,
);
} else {
snackBarTransition = AnimatedBuilder(
animation: heightAnimation,
builder: (BuildContext context, Widget child) {
return Align(
alignment: AlignmentDirectional.topStart,
heightFactor: heightAnimation.value,
child: child,
);
},
child: snackBar,
);
}
return ClipRect(child: snackBarTransition);
}
}