blob: ce3df0509556757ef28590aa7a45396f4d40b735 [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_style.dart';
import 'color_scheme.dart';
import 'colors.dart';
import 'icon_button.dart';
import 'icons.dart';
import 'material.dart';
import 'material_state.dart';
import 'scaffold.dart';
import 'snack_bar_theme.dart';
import 'text_button.dart';
import 'text_button_theme.dart';
import 'theme.dart';
// Examples can assume:
// late BuildContext context;
const double _singleLineVerticalPadding = 14.0;
const Duration _snackBarTransitionDuration = Duration(milliseconds: 250);
const Duration _snackBarDisplayDuration = Duration(milliseconds: 4000);
const Curve _snackBarHeightCurve = Curves.fastOutSlowIn;
const Curve _snackBarM3HeightCurve = Curves.easeInOutQuart;
const Curve _snackBarFadeInCurve = Interval(0.4, 1.0);
const Curve _snackBarM3FadeInCurve = Interval(0.4, 0.6, curve: Curves.easeInCirc);
const Curve _snackBarFadeOutCurve = Interval(0.72, 1.0, curve: Curves.fastOutSlowIn);
/// Specify how a [SnackBar] was closed.
///
/// The [ScaffoldMessengerState.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
/// ScaffoldMessenger.of(context).showSnackBar(
/// const SnackBar(
/// content: Text('He likes me. I think he likes me.'),
/// )
/// ).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 [SemanticsAction.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 [ScaffoldMessengerState.hideCurrentSnackBar] directly.
hide,
/// The snack bar was closed by an call to [ScaffoldMessengerState.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. Instead of disabling a snack bar
/// action, avoid including it in the snack bar in the first place.
///
/// 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({
super.key,
this.textColor,
this.disabledTextColor,
this.backgroundColor,
this.disabledBackgroundColor,
required this.label,
required this.onPressed,
}) : assert(backgroundColor is! MaterialStateColor || disabledBackgroundColor == null,
'disabledBackgroundColor must not be provided when background color is '
'a MaterialStateColor');
/// The button label color. If not provided, defaults to
/// [SnackBarThemeData.actionTextColor].
///
/// If [textColor] is a [MaterialStateColor], then the text color will be
/// resolved against the set of [MaterialState]s that the action text
/// is in, thus allowing for different colors for states such as pressed,
/// hovered and others.
final Color? textColor;
/// The button background fill color. If not provided, defaults to
/// [SnackBarThemeData.actionBackgroundColor].
///
/// If [backgroundColor] is a [MaterialStateColor], then the text color will
/// be resolved against the set of [MaterialState]s that the action text is
/// in, thus allowing for different colors for the states.
final Color? backgroundColor;
/// The button disabled label color. This color is shown after the
/// [SnackBarAction] is dismissed.
final Color? disabledTextColor;
/// The button diabled background color. This color is shown after the
/// [SnackBarAction] is dismissed.
///
/// If not provided, defaults to [SnackBarThemeData.disabledActionBackgroundColor].
final Color? disabledBackgroundColor;
/// 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
State<SnackBarAction> createState() => _SnackBarActionState();
}
class _SnackBarActionState extends State<SnackBarAction> {
bool _haveTriggeredAction = false;
void _handlePressed() {
if (_haveTriggeredAction) {
return;
}
setState(() {
_haveTriggeredAction = true;
});
widget.onPressed();
ScaffoldMessenger.of(context).hideCurrentSnackBar(reason: SnackBarClosedReason.action);
}
@override
Widget build(BuildContext context) {
final SnackBarThemeData defaults = Theme.of(context).useMaterial3
? _SnackbarDefaultsM3(context)
: _SnackbarDefaultsM2(context);
final SnackBarThemeData snackBarTheme = Theme.of(context).snackBarTheme;
MaterialStateColor resolveForegroundColor() {
if (widget.textColor != null) {
if (widget.textColor is MaterialStateColor) {
return widget.textColor! as MaterialStateColor;
}
} else if (snackBarTheme.actionTextColor != null) {
if (snackBarTheme.actionTextColor is MaterialStateColor) {
return snackBarTheme.actionTextColor! as MaterialStateColor;
}
} else if (defaults.actionTextColor != null) {
if (defaults.actionTextColor is MaterialStateColor) {
return defaults.actionTextColor! as MaterialStateColor;
}
}
return MaterialStateColor.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return widget.disabledTextColor ??
snackBarTheme.disabledActionTextColor ??
defaults.disabledActionTextColor!;
}
return widget.textColor ??
snackBarTheme.actionTextColor ??
defaults.actionTextColor!;
});
}
MaterialStateColor? resolveBackgroundColor() {
if (widget.backgroundColor is MaterialStateColor) {
return widget.backgroundColor! as MaterialStateColor;
}
if (snackBarTheme.actionBackgroundColor is MaterialStateColor) {
return snackBarTheme.actionBackgroundColor! as MaterialStateColor;
}
return MaterialStateColor.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return widget.disabledBackgroundColor ??
snackBarTheme.disabledActionBackgroundColor ??
Colors.transparent;
}
return widget.backgroundColor ??
snackBarTheme.actionBackgroundColor ??
Colors.transparent;
});
}
return TextButton(
style: ButtonStyle(
foregroundColor: resolveForegroundColor(),
backgroundColor: resolveBackgroundColor(),
),
onPressed: _haveTriggeredAction ? null : _handlePressed,
child: Text(widget.label),
);
}
}
/// A lightweight message with an optional action which briefly displays at the
/// bottom of the screen.
///
/// {@youtube 560 315 https://www.youtube.com/watch?v=zpO6n_oZWw0}
///
/// To display a snack bar, call `ScaffoldMessenger.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].
///
/// During page transitions, the [SnackBar] will smoothly animate to its
/// location on the other page. For example if the [SnackBar.behavior] is set to
/// [SnackBarBehavior.floating] and the next page has a floating action button,
/// while the current one does not, the [SnackBar] will smoothly animate above
/// the floating action button. It also works in the case of a back gesture
/// transition.
///
/// {@tool dartpad}
/// Here is an example of a [SnackBar] with an [action] button implemented using
/// [SnackBarAction].
///
/// ** See code in examples/api/lib/material/snack_bar/snack_bar.0.dart **
/// {@end-tool}
///
/// {@tool dartpad}
/// Here is an example of a customized [SnackBar]. It utilizes
/// [behavior], [shape], [padding], [width], and [duration] to customize the
/// location, appearance, and the duration for which the [SnackBar] is visible.
///
/// ** See code in examples/api/lib/material/snack_bar/snack_bar.1.dart **
/// {@end-tool}
///
/// {@tool dartpad}
/// This example demonstrates the various [SnackBar] widget components,
/// including an optional icon, in either floating or fixed format.
///
/// ** See code in examples/api/lib/material/snack_bar/snack_bar.2.dart **
/// {@end-tool}
///
/// See also:
///
/// * [ScaffoldMessenger.of], to obtain the current [ScaffoldMessengerState],
/// which manages the display and animation of snack bars.
/// * [ScaffoldMessengerState.showSnackBar], which displays a [SnackBar].
/// * [ScaffoldMessengerState.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({
super.key,
required this.content,
this.backgroundColor,
this.elevation,
this.margin,
this.padding,
this.width,
this.shape,
this.hitTestBehavior,
this.behavior,
this.action,
this.actionOverflowThreshold,
this.showCloseIcon,
this.closeIconColor,
this.duration = _snackBarDisplayDuration,
this.animation,
this.onVisible,
this.dismissDirection = DismissDirection.down,
this.clipBehavior = Clip.hardEdge,
}) : assert(elevation == null || elevation >= 0.0),
assert(width == null || margin == null,
'Width and margin can not be used together',
),
assert(actionOverflowThreshold == null || (actionOverflowThreshold >= 0 && actionOverflowThreshold <= 1),
'Action overflow threshold must be between 0 and 1 inclusive');
/// The primary content of the snack bar.
///
/// Typically a [Text] widget.
final Widget content;
/// The snack bar's background color.
///
/// If not specified, it will use [SnackBarThemeData.backgroundColor] of
/// [ThemeData.snackBarTheme]. 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 [SnackBarThemeData.elevation] of
/// [ThemeData.snackBarTheme] is used, if that is also null, the default value
/// is 6.0.
final double? elevation;
/// Empty space to surround the snack bar.
///
/// This property is only used when [behavior] is [SnackBarBehavior.floating].
/// It can not be used if [width] is specified.
///
/// If this property is null, then [SnackBarThemeData.insetPadding] of
/// [ThemeData.snackBarTheme] is used. If that is also null, then the default is
/// `EdgeInsets.fromLTRB(15.0, 5.0, 15.0, 10.0)`.
///
/// If this property is not null and [hitTestBehavior] is null, then [hitTestBehavior] default is [HitTestBehavior.deferToChild].
final EdgeInsetsGeometry? margin;
/// The amount of padding to apply to the snack bar's content and optional
/// action.
///
/// If this property is null, the default padding values are as follows:
///
/// * [content]
/// * Top and bottom paddings are 14.
/// * Left padding is 24 if [behavior] is [SnackBarBehavior.fixed],
/// 16 if [behavior] is [SnackBarBehavior.floating].
/// * Right padding is same as start padding if there is no [action],
/// otherwise 0.
/// * [action]
/// * Top and bottom paddings are 14.
/// * Left and right paddings are half of [content]'s left padding.
///
/// If this property is not null, the padding is as follows:
///
/// * [content]
/// * Left, top and bottom paddings are assigned normally.
/// * Right padding is assigned normally if there is no [action],
/// otherwise 0.
/// * [action]
/// * Left padding is replaced with half the right padding.
/// * Top and bottom paddings are assigned normally.
/// * Right padding is replaced with one and a half times the
/// right padding.
final EdgeInsetsGeometry? padding;
/// The width of the snack bar.
///
/// If width is specified, the snack bar will be centered horizontally in the
/// available space. This property is only used when [behavior] is
/// [SnackBarBehavior.floating]. It can not be used if [margin] is specified.
///
/// If this property is null, then [SnackBarThemeData.width] of
/// [ThemeData.snackBarTheme] is used. If that is null, the snack bar will
/// take up the full device width less the margin.
final double? width;
/// The shape of the snack bar's [Material].
///
/// Defines the snack bar's [Material.shape].
///
/// If this property is null then [SnackBarThemeData.shape] of
/// [ThemeData.snackBarTheme] 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;
/// Defines how the snack bar area, including margin, will behave during hit testing.
///
/// If this property is null and [margin] is not null, then [HitTestBehavior.deferToChild] is used by default.
///
/// Please refer to [HitTestBehavior] for a detailed explanation of every behavior.
final HitTestBehavior? hitTestBehavior;
/// 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 [SnackBarThemeData.behavior] of
/// [ThemeData.snackBarTheme] is used. If that is null, then the default is
/// [SnackBarBehavior.fixed].
///
/// If this value is [SnackBarBehavior.floating], the length of the bar
/// is defined by either [width] or [margin].
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;
/// (optional) The percentage threshold for action widget's width before it overflows
/// to a new line.
///
/// Must be between 0 and 1. If the width of the snackbar's [content] is greater
/// than this percentage of the width of the snackbar less the width of its [action],
/// then the [action] will appear below the [content].
///
/// At a value of 0, the action will not overflow to a new line.
///
/// Defaults to 0.25.
final double? actionOverflowThreshold;
/// (optional) Whether to include a "close" icon widget.
///
/// Tapping the icon will close the snack bar.
final bool? showCloseIcon;
/// (optional) An optional color for the close icon, if [showCloseIcon] is
/// true.
///
/// If this property is null, then [SnackBarThemeData.closeIconColor] of
/// [ThemeData.snackBarTheme] is used. If that is null, then the default is
/// inverse surface.
///
/// If [closeIconColor] is a [MaterialStateColor], then the icon color will be
/// resolved against the set of [MaterialState]s that the action text
/// is in, thus allowing for different colors for states such as pressed,
/// hovered and others.
final Color? closeIconColor;
/// The amount of time the snack bar should be displayed.
///
/// Defaults to 4.0s.
///
/// See also:
///
/// * [ScaffoldMessengerState.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;
/// The direction in which the SnackBar can be dismissed.
///
/// Cannot be null, defaults to [DismissDirection.down].
final DismissDirection dismissDirection;
/// {@macro flutter.material.Material.clipBehavior}
///
/// Defaults to [Clip.hardEdge], and must not be null.
final Clip clipBehavior;
// API for ScaffoldMessengerState.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,
margin: margin,
padding: padding,
width: width,
shape: shape,
hitTestBehavior: hitTestBehavior,
behavior: behavior,
action: action,
actionOverflowThreshold: actionOverflowThreshold,
showCloseIcon: showCloseIcon,
closeIconColor: closeIconColor,
duration: duration,
animation: newAnimation,
onVisible: onVisible,
dismissDirection: dismissDirection,
clipBehavior: clipBehavior,
);
}
@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) {
super.didUpdateWidget(oldWidget);
if (widget.animation != oldWidget.animation) {
oldWidget.animation!.removeStatusListener(_onAnimationStatusChanged);
widget.animation!.addStatusListener(_onAnimationStatusChanged);
}
}
@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) {
assert(debugCheckHasMediaQuery(context));
final bool accessibleNavigation = MediaQuery.accessibleNavigationOf(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;
final Color buttonColor = isThemeDark ? colorScheme.primary : colorScheme.secondary;
final SnackBarThemeData defaults = theme.useMaterial3
? _SnackbarDefaultsM3(context)
: _SnackbarDefaultsM2(context);
// SnackBar uses a theme that is the opposite brightness from
// the surrounding theme.
final Brightness brightness = isThemeDark ? Brightness.light : Brightness.dark;
// Invert the theme values for Material 2. Material 3 values are tokenized to pre-inverted values.
final ThemeData effectiveTheme = theme.useMaterial3
? theme
: theme.copyWith(
colorScheme: ColorScheme(
primary: colorScheme.onPrimary,
secondary: buttonColor,
surface: colorScheme.onSurface,
background: defaults.backgroundColor!,
error: colorScheme.onError,
onPrimary: colorScheme.primary,
onSecondary: colorScheme.secondary,
onSurface: colorScheme.surface,
onBackground: colorScheme.background,
onError: colorScheme.error,
brightness: brightness,
),
);
final TextStyle? contentTextStyle = snackBarTheme.contentTextStyle ?? defaults.contentTextStyle;
final SnackBarBehavior snackBarBehavior = widget.behavior ?? snackBarTheme.behavior ?? defaults.behavior!;
final double? width = widget.width ?? snackBarTheme.width;
assert((){
// Whether the behavior is set through the constructor or the theme,
// assert that our other properties are configured properly.
if (snackBarBehavior != SnackBarBehavior.floating) {
String message(String parameter) {
final String prefix = '$parameter can only be used with floating behavior.';
if (widget.behavior != null) {
return '$prefix SnackBarBehavior.fixed was set in the SnackBar constructor.';
} else if (snackBarTheme.behavior != null) {
return '$prefix SnackBarBehavior.fixed was set by the inherited SnackBarThemeData.';
} else {
return '$prefix SnackBarBehavior.fixed was set by default.';
}
}
assert(widget.margin == null, message('Margin'));
assert(width == null, message('Width'));
}
return true;
}());
final bool showCloseIcon = widget.showCloseIcon ?? snackBarTheme.showCloseIcon ?? defaults.showCloseIcon!;
final bool isFloatingSnackBar = snackBarBehavior == SnackBarBehavior.floating;
final double horizontalPadding = isFloatingSnackBar ? 16.0 : 24.0;
final EdgeInsetsGeometry padding = widget.padding ??
EdgeInsetsDirectional.only(
start: horizontalPadding,
end: widget.action != null || showCloseIcon
? 0
: horizontalPadding);
final double actionHorizontalMargin = (widget.padding?.resolve(TextDirection.ltr).right ?? horizontalPadding) / 2;
final double iconHorizontalMargin = (widget.padding?.resolve(TextDirection.ltr).right ?? horizontalPadding) / 12.0;
final CurvedAnimation heightAnimation = CurvedAnimation(parent: widget.animation!, curve: _snackBarHeightCurve);
final CurvedAnimation fadeInAnimation = CurvedAnimation(parent: widget.animation!, curve: _snackBarFadeInCurve);
final CurvedAnimation fadeInM3Animation = CurvedAnimation(parent: widget.animation!, curve: _snackBarM3FadeInCurve);
final CurvedAnimation fadeOutAnimation = CurvedAnimation(
parent: widget.animation!,
curve: _snackBarFadeOutCurve,
reverseCurve: const Threshold(0.0),
);
// Material 3 Animation has a height animation on entry, but a direct fade out on exit.
final CurvedAnimation heightM3Animation = CurvedAnimation(
parent: widget.animation!,
curve: _snackBarM3HeightCurve,
reverseCurve: const Threshold(0.0),
);
final IconButton? iconButton = showCloseIcon
? IconButton(
icon: const Icon(Icons.close),
iconSize: 24.0,
color: widget.closeIconColor ?? snackBarTheme.closeIconColor ?? defaults.closeIconColor,
onPressed: () => ScaffoldMessenger.of(context).hideCurrentSnackBar(reason: SnackBarClosedReason.dismiss),
)
: null;
// Calculate combined width of Action, Icon, and their padding, if they are present.
final TextPainter actionTextPainter = TextPainter(
text: TextSpan(
text: widget.action?.label ?? '',
style: Theme.of(context).textTheme.labelLarge,
),
maxLines: 1,
textDirection: TextDirection.ltr)
..layout();
final double actionAndIconWidth = actionTextPainter.size.width +
(widget.action != null ? actionHorizontalMargin : 0) +
(showCloseIcon ? (iconButton?.iconSize ?? 0 + iconHorizontalMargin) : 0);
final EdgeInsets margin = widget.margin?.resolve(TextDirection.ltr) ?? snackBarTheme.insetPadding ?? defaults.insetPadding!;
final double snackBarWidth = widget.width ?? MediaQuery.sizeOf(context).width - (margin.left + margin.right);
final double actionOverflowThreshold = widget.actionOverflowThreshold
?? snackBarTheme.actionOverflowThreshold
?? defaults.actionOverflowThreshold!;
final bool willOverflowAction = actionAndIconWidth / snackBarWidth > actionOverflowThreshold;
final List<Widget> maybeActionAndIcon = <Widget>[
if (widget.action != null)
Padding(
padding: EdgeInsets.symmetric(horizontal: actionHorizontalMargin),
child: TextButtonTheme(
data: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: buttonColor,
padding: EdgeInsets.symmetric(horizontal: horizontalPadding),
),
),
child: widget.action!,
),
),
if (showCloseIcon)
Padding(
padding: EdgeInsets.symmetric(horizontal: iconHorizontalMargin),
child: iconButton,
),
];
Widget snackBar = Padding(
padding: padding,
child: Wrap(
children: <Widget>[
Row(
children: <Widget>[
Expanded(
child: Container(
padding: widget.padding == null
? const EdgeInsets.symmetric(
vertical: _singleLineVerticalPadding)
: null,
child: DefaultTextStyle(
style: contentTextStyle!,
child: widget.content,
),
),
),
if (!willOverflowAction) ...maybeActionAndIcon,
if (willOverflowAction) SizedBox(width: snackBarWidth * 0.4),
],
),
if (willOverflowAction)
Padding(
padding: const EdgeInsets.only(bottom: _singleLineVerticalPadding),
child: Row(mainAxisAlignment: MainAxisAlignment.end, children: maybeActionAndIcon),
),
],
),
);
if (!isFloatingSnackBar) {
snackBar = SafeArea(
top: false,
child: snackBar,
);
}
final double elevation = widget.elevation ?? snackBarTheme.elevation ?? defaults.elevation!;
final Color backgroundColor = widget.backgroundColor ?? snackBarTheme.backgroundColor ?? defaults.backgroundColor!;
final ShapeBorder? shape = widget.shape ?? snackBarTheme.shape ?? (isFloatingSnackBar ? defaults.shape : null);
snackBar = Material(
shape: shape,
elevation: elevation,
color: backgroundColor,
clipBehavior: widget.clipBehavior,
child: Theme(
data: effectiveTheme,
child: accessibleNavigation || theme.useMaterial3
? snackBar
: FadeTransition(
opacity: fadeOutAnimation,
child: snackBar,
),
),
);
if (isFloatingSnackBar) {
// If width is provided, do not include horizontal margins.
if (width != null) {
snackBar = Container(
margin: EdgeInsets.only(top: margin.top, bottom: margin.bottom),
width: width,
child: snackBar,
);
} else {
snackBar = Padding(
padding: margin,
child: snackBar,
);
}
snackBar = SafeArea(
top: false,
bottom: false,
child: snackBar,
);
}
snackBar = Semantics(
container: true,
liveRegion: true,
onDismiss: () {
ScaffoldMessenger.of(context).removeCurrentSnackBar(reason: SnackBarClosedReason.dismiss);
},
child: Dismissible(
key: const Key('dismissible'),
direction: widget.dismissDirection,
resizeDuration: null,
behavior: widget.hitTestBehavior ?? (widget.margin != null ? HitTestBehavior.deferToChild : HitTestBehavior.opaque),
onDismissed: (DismissDirection direction) {
ScaffoldMessenger.of(context).removeCurrentSnackBar(reason: SnackBarClosedReason.swipe);
},
child: snackBar,
),
);
final Widget snackBarTransition;
if (accessibleNavigation) {
snackBarTransition = snackBar;
} else if (isFloatingSnackBar && !theme.useMaterial3) {
snackBarTransition = FadeTransition(
opacity: fadeInAnimation,
child: snackBar,
);
// Is Material 3 Floating Snack Bar.
} else if (isFloatingSnackBar && theme.useMaterial3) {
snackBarTransition = FadeTransition(
opacity: fadeInM3Animation,
child: AnimatedBuilder(
animation: heightM3Animation,
builder: (BuildContext context, Widget? child) {
return Align(
alignment: AlignmentDirectional.bottomStart,
heightFactor: heightM3Animation.value,
child: child,
);
},
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 Hero(
tag: '<SnackBar Hero tag - ${widget.content}>',
transitionOnUserGestures: true,
child: ClipRect(
clipBehavior: widget.clipBehavior,
child: snackBarTransition,
),
);
}
}
// Hand coded defaults based on Material Design 2.
class _SnackbarDefaultsM2 extends SnackBarThemeData {
_SnackbarDefaultsM2(BuildContext context)
: _theme = Theme.of(context),
_colors = Theme.of(context).colorScheme,
super(elevation: 6.0);
late final ThemeData _theme;
late final ColorScheme _colors;
@override
Color get backgroundColor => _theme.brightness == Brightness.light
? Color.alphaBlend(_colors.onSurface.withOpacity(0.80), _colors.surface)
: _colors.onSurface;
@override
TextStyle? get contentTextStyle => ThemeData(
useMaterial3: _theme.useMaterial3,
brightness: _theme.brightness == Brightness.light
? Brightness.dark
: Brightness.light)
.textTheme
.titleMedium;
@override
SnackBarBehavior get behavior => SnackBarBehavior.fixed;
@override
Color get actionTextColor => _colors.secondary;
@override
Color get disabledActionTextColor => _colors.onSurface
.withOpacity(_theme.brightness == Brightness.light ? 0.38 : 0.3);
@override
ShapeBorder get shape => const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(4.0),
),
);
@override
EdgeInsets get insetPadding => const EdgeInsets.fromLTRB(15.0, 5.0, 15.0, 10.0);
@override
bool get showCloseIcon => false;
@override
Color get closeIconColor => _colors.onSurface;
@override
double get actionOverflowThreshold => 0.25;
}
// BEGIN GENERATED TOKEN PROPERTIES - Snackbar
// Do not edit by hand. The code between the "BEGIN GENERATED" and
// "END GENERATED" comments are generated from data in the Material
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
class _SnackbarDefaultsM3 extends SnackBarThemeData {
_SnackbarDefaultsM3(this.context);
final BuildContext context;
late final ThemeData _theme = Theme.of(context);
late final ColorScheme _colors = _theme.colorScheme;
@override
Color get backgroundColor => _colors.inverseSurface;
@override
Color get actionTextColor => MaterialStateColor.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return _colors.inversePrimary;
}
if (states.contains(MaterialState.pressed)) {
return _colors.inversePrimary;
}
if (states.contains(MaterialState.hovered)) {
return _colors.inversePrimary;
}
if (states.contains(MaterialState.focused)) {
return _colors.inversePrimary;
}
return _colors.inversePrimary;
});
@override
Color get disabledActionTextColor =>
_colors.inversePrimary;
@override
TextStyle get contentTextStyle =>
Theme.of(context).textTheme.bodyMedium!.copyWith
(color: _colors.onInverseSurface,
);
@override
double get elevation => 6.0;
@override
ShapeBorder get shape => const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0)));
@override
SnackBarBehavior get behavior => SnackBarBehavior.fixed;
@override
EdgeInsets get insetPadding => const EdgeInsets.fromLTRB(15.0, 5.0, 15.0, 10.0);
@override
bool get showCloseIcon => false;
@override
Color? get closeIconColor => _colors.onInverseSurface;
@override
double get actionOverflowThreshold => 0.25;
}
// END GENERATED TOKEN PROPERTIES - Snackbar