snack_bar.dart 34.1 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'package:flutter/rendering.dart';
6
import 'package:flutter/widgets.dart';
7

8
import 'button_style.dart';
9
import 'color_scheme.dart';
10
import 'colors.dart';
11 12
import 'icon_button.dart';
import 'icons.dart';
13
import 'material.dart';
14
import 'material_state.dart';
15
import 'scaffold.dart';
16
import 'snack_bar_theme.dart';
17 18
import 'text_button.dart';
import 'text_button_theme.dart';
19
import 'theme.dart';
Matt Perry's avatar
Matt Perry committed
20

21 22 23
// Examples can assume:
// late BuildContext context;

24 25 26
const double _singleLineVerticalPadding = 14.0;
const Duration _snackBarTransitionDuration = Duration(milliseconds: 250);
const Duration _snackBarDisplayDuration = Duration(milliseconds: 4000);
27
const Curve _snackBarHeightCurve = Curves.fastOutSlowIn;
28 29 30 31
const Curve _snackBarM3HeightCurve = Curves.easeInOutQuart;

const Curve _snackBarFadeInCurve = Interval(0.4, 1.0);
const Curve _snackBarM3FadeInCurve = Interval(0.4, 0.6, curve: Curves.easeInCirc);
32
const Curve _snackBarFadeOutCurve = Interval(0.72, 1.0, curve: Curves.fastOutSlowIn);
Hixie's avatar
Hixie committed
33

34 35
/// Specify how a [SnackBar] was closed.
///
36
/// The [ScaffoldMessengerState.showSnackBar] function returns a
37 38 39
/// [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.
40 41 42 43
///
/// Example:
///
/// ```dart
44
/// ScaffoldMessenger.of(context).showSnackBar(
45 46 47
///   const SnackBar(
///     content: Text('He likes me. I think he likes me.'),
///   )
48
/// ).closed.then((SnackBarClosedReason reason) {
49
///    // ...
50 51 52 53 54 55
/// });
/// ```
enum SnackBarClosedReason {
  /// The snack bar was closed after the user tapped a [SnackBarAction].
  action,

56
  /// The snack bar was closed through a [SemanticsAction.dismiss].
57 58
  dismiss,

59 60 61 62
  /// The snack bar was closed by a user's swipe.
  swipe,

  /// The snack bar was closed by the [ScaffoldFeatureController] close callback
63
  /// or by calling [ScaffoldMessengerState.hideCurrentSnackBar] directly.
64 65
  hide,

66
  /// The snack bar was closed by an call to [ScaffoldMessengerState.removeCurrentSnackBar].
67 68 69 70 71 72
  remove,

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

73 74
/// A button for a [SnackBar], known as an "action".
///
75 76
/// Snack bar actions are always enabled. Instead of disabling a snack bar
/// action, avoid including it in the snack bar in the first place.
77
///
78 79
/// Snack bar actions can only be pressed once. Subsequent presses are ignored.
///
80 81 82
/// See also:
///
///  * [SnackBar]
jslavitz's avatar
jslavitz committed
83
///  * <https://material.io/design/components/snackbars.html>
84
class SnackBarAction extends StatefulWidget {
85
  /// Creates an action for a [SnackBar].
86
  const SnackBarAction({
87
    super.key,
jslavitz's avatar
jslavitz committed
88 89
    this.textColor,
    this.disabledTextColor,
90 91
    this.backgroundColor,
    this.disabledBackgroundColor,
92 93
    required this.label,
    required this.onPressed,
94 95 96
  }) : assert(backgroundColor is! MaterialStateColor || disabledBackgroundColor == null,
        'disabledBackgroundColor must not be provided when background color is '
        'a MaterialStateColor');
97

98 99
  /// The button label color. If not provided, defaults to
  /// [SnackBarThemeData.actionTextColor].
100 101
  ///
  /// If [textColor] is a [MaterialStateColor], then the text color will be
102
  /// resolved against the set of [MaterialState]s that the action text
103 104
  /// is in, thus allowing for different colors for states such as pressed,
  /// hovered and others.
105
  final Color? textColor;
jslavitz's avatar
jslavitz committed
106

107 108 109 110 111 112 113 114
  /// 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;

jslavitz's avatar
jslavitz committed
115
  /// The button disabled label color. This color is shown after the
116
  /// [SnackBarAction] is dismissed.
117
  final Color? disabledTextColor;
jslavitz's avatar
jslavitz committed
118

119 120 121 122 123 124
  /// The button diabled background color. This color is shown after the
  /// [SnackBarAction] is dismissed.
  ///
  /// If not provided, defaults to [SnackBarThemeData.disabledActionBackgroundColor].
  final Color? disabledBackgroundColor;

125
  /// The button label.
126
  final String label;
127

128
  /// The callback to be called when the button is pressed.
129
  ///
130
  /// This callback will be called at most once each time this action is
131
  /// displayed in a [SnackBar].
132
  final VoidCallback onPressed;
133

134
  @override
135
  State<SnackBarAction> createState() => _SnackBarActionState();
136 137 138 139 140 141
}

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

  void _handlePressed() {
142
    if (_haveTriggeredAction) {
143
      return;
144
    }
145 146 147
    setState(() {
      _haveTriggeredAction = true;
    });
148
    widget.onPressed();
149
    ScaffoldMessenger.of(context).hideCurrentSnackBar(reason: SnackBarClosedReason.action);
150 151
  }

152
  @override
Hixie's avatar
Hixie committed
153
  Widget build(BuildContext context) {
154 155 156 157 158 159
    final SnackBarThemeData defaults = Theme.of(context).useMaterial3
        ? _SnackbarDefaultsM3(context)
        : _SnackbarDefaultsM2(context);
    final SnackBarThemeData snackBarTheme = Theme.of(context).snackBarTheme;

    MaterialStateColor resolveForegroundColor() {
160 161 162 163 164 165 166 167 168 169 170 171
      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;
        }
172
      }
173

174 175 176 177 178 179 180 181 182 183
      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!;
      });
184
    }
185

186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204
    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;
      });
    }

205 206
    return TextButton(
      style: ButtonStyle(
207
        foregroundColor: resolveForegroundColor(),
208
        backgroundColor: resolveBackgroundColor(),
209
      ),
210
      onPressed: _haveTriggeredAction ? null : _handlePressed,
211
      child: Text(widget.label),
212 213 214
    );
  }
}
215

216 217 218
/// A lightweight message with an optional action which briefly displays at the
/// bottom of the screen.
///
219 220
/// {@youtube 560 315 https://www.youtube.com/watch?v=zpO6n_oZWw0}
///
221 222
/// To display a snack bar, call `ScaffoldMessenger.of(context).showSnackBar()`,
/// passing an instance of [SnackBar] that describes the message.
223 224
///
/// To control how long the [SnackBar] remains visible, specify a [duration].
225
///
226 227 228
/// A SnackBar with an action will not time out when TalkBack or VoiceOver are
/// enabled. This is controlled by [AccessibilityFeatures.accessibleNavigation].
///
229 230 231 232 233 234 235
/// 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.
///
236
/// {@tool dartpad}
237 238 239
/// Here is an example of a [SnackBar] with an [action] button implemented using
/// [SnackBarAction].
///
240
/// ** See code in examples/api/lib/material/snack_bar/snack_bar.0.dart **
241 242
/// {@end-tool}
///
243
/// {@tool dartpad}
244 245 246 247
/// 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.
///
248
/// ** See code in examples/api/lib/material/snack_bar/snack_bar.1.dart **
249 250
/// {@end-tool}
///
251 252 253 254 255 256 257
/// {@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}
///
258
/// See also:
259
///
260 261 262 263 264
///  * [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.
265 266
///  * [SnackBarAction], which is used to specify an [action] button to show
///    on the snack bar.
267 268
///  * [SnackBarThemeData], to configure the default property values for
///    [SnackBar] widgets.
jslavitz's avatar
jslavitz committed
269
///  * <https://material.io/design/components/snackbars.html>
270
class SnackBar extends StatefulWidget {
271 272
  /// Creates a snack bar.
  ///
273
  /// The [elevation] must be null or non-negative.
274
  const SnackBar({
275
    super.key,
276
    required this.content,
277
    this.backgroundColor,
278
    this.elevation,
279 280 281
    this.margin,
    this.padding,
    this.width,
282
    this.shape,
283
    this.hitTestBehavior,
284
    this.behavior,
285
    this.action,
286
    this.actionOverflowThreshold,
287 288
    this.showCloseIcon,
    this.closeIconColor,
289
    this.duration = _snackBarDisplayDuration,
290
    this.animation,
291
    this.onVisible,
292
    this.dismissDirection,
293
    this.clipBehavior = Clip.hardEdge,
294
  }) : assert(elevation == null || elevation >= 0.0),
295
       assert(width == null || margin == null,
296
         'Width and margin can not be used together',
297 298 299
       ),
       assert(actionOverflowThreshold == null || (actionOverflowThreshold >= 0 && actionOverflowThreshold <= 1),
        'Action overflow threshold must be between 0 and 1 inclusive');
300

301 302 303
  /// The primary content of the snack bar.
  ///
  /// Typically a [Text] widget.
304
  final Widget content;
305

306 307 308 309 310 311
  /// 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.
312
  final Color? backgroundColor;
313

314 315 316 317 318
  /// 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].
  ///
319 320 321
  /// If this property is null, then [SnackBarThemeData.elevation] of
  /// [ThemeData.snackBarTheme] is used, if that is also null, the default value
  /// is 6.0.
322
  final double? elevation;
323

324 325 326 327 328
  /// 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.
  ///
329 330
  /// If this property is null, then [SnackBarThemeData.insetPadding] of
  /// [ThemeData.snackBarTheme] is used. If that is also null, then the default is
331
  /// `EdgeInsets.fromLTRB(15.0, 5.0, 15.0, 10.0)`.
332 333
  ///
  /// If this property is not null and [hitTestBehavior] is null, then [hitTestBehavior] default is [HitTestBehavior.deferToChild].
334
  final EdgeInsetsGeometry? margin;
335 336 337 338

  /// The amount of padding to apply to the snack bar's content and optional
  /// action.
  ///
339
  /// If this property is null, the default padding values are as follows:
340 341 342 343
  ///
  /// * [content]
  ///     * Top and bottom paddings are 14.
  ///     * Left padding is 24 if [behavior] is [SnackBarBehavior.fixed],
344 345 346
  ///       16 if [behavior] is [SnackBarBehavior.floating].
  ///     * Right padding is same as start padding if there is no [action],
  ///       otherwise 0.
347
  /// * [action]
348
  ///     * Top and bottom paddings are 14.
349 350
  ///     * Left and right paddings are half of [content]'s left padding.
  ///
351
  /// If this property is not null, the padding is as follows:
352 353 354
  ///
  /// * [content]
  ///     * Left, top and bottom paddings are assigned normally.
355 356
  ///     * Right padding is assigned normally if there is no [action],
  ///       otherwise 0.
357
  /// * [action]
358
  ///     * Left padding is replaced with half the right padding.
359
  ///     * Top and bottom paddings are assigned normally.
360 361
  ///     * Right padding is replaced with one and a half times the
  ///       right padding.
362
  final EdgeInsetsGeometry? padding;
363 364 365 366 367 368 369

  /// 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.
  ///
370 371 372
  /// 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.
373
  final double? width;
374

375 376 377 378
  /// The shape of the snack bar's [Material].
  ///
  /// Defines the snack bar's [Material.shape].
  ///
379 380 381 382 383 384
  /// 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.
385
  final ShapeBorder? shape;
386

387 388 389 390 391 392 393
  /// 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;

394 395 396 397 398 399
  /// 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]
  ///
400 401 402
  /// If this property is null, then [SnackBarThemeData.behavior] of
  /// [ThemeData.snackBarTheme] is used. If that is null, then the default is
  /// [SnackBarBehavior.fixed].
403 404 405
  ///
  /// If this value is [SnackBarBehavior.floating], the length of the bar
  /// is defined by either [width] or [margin].
406
  final SnackBarBehavior? behavior;
407

408 409 410 411
  /// (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.
412 413
  ///
  /// The action should not be "dismiss" or "cancel".
414
  final SnackBarAction? action;
415

416 417 418 419 420 421 422 423 424 425 426 427
  /// (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;

428 429 430 431 432 433 434 435 436 437 438 439 440
  /// (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
441
  /// resolved against the set of [MaterialState]s that the action text
442 443 444 445
  /// is in, thus allowing for different colors for states such as pressed,
  /// hovered and others.
  final Color? closeIconColor;

446
  /// The amount of time the snack bar should be displayed.
447
  ///
jslavitz's avatar
jslavitz committed
448
  /// Defaults to 4.0s.
449 450 451
  ///
  /// See also:
  ///
452
  ///  * [ScaffoldMessengerState.removeCurrentSnackBar], which abruptly hides the
453 454
  ///    currently displayed snack bar, if any, and allows the next to be
  ///    displayed.
jslavitz's avatar
jslavitz committed
455
  ///  * <https://material.io/design/components/snackbars.html>
Hixie's avatar
Hixie committed
456
  final Duration duration;
457 458

  /// The animation driving the entrance and exit of the snack bar.
459
  final Animation<double>? animation;
460

461
  /// Called the first time that the snackbar is visible within a [Scaffold].
462
  final VoidCallback? onVisible;
463

464 465
  /// The direction in which the SnackBar can be dismissed.
  ///
466 467 468 469
  /// If this property is null, then [SnackBarThemeData.dismissDirection] of
  /// [ThemeData.snackBarTheme] is used. If that is null, then the default is
  /// [DismissDirection.down].
  final DismissDirection? dismissDirection;
470

471 472
  /// {@macro flutter.material.Material.clipBehavior}
  ///
473
  /// Defaults to [Clip.hardEdge].
474 475
  final Clip clipBehavior;

476
  // API for ScaffoldMessengerState.showSnackBar():
477 478

  /// Creates an animation controller useful for driving a snack bar's entrance and exit animation.
479
  static AnimationController createAnimationController({ required TickerProvider vsync }) {
480 481 482 483 484 485 486 487 488 489 490
    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.
491
  SnackBar withAnimation(Animation<double> newAnimation, { Key? fallbackKey }) {
492 493 494 495 496
    return SnackBar(
      key: key ?? fallbackKey,
      content: content,
      backgroundColor: backgroundColor,
      elevation: elevation,
497 498 499
      margin: margin,
      padding: padding,
      width: width,
500
      shape: shape,
501
      hitTestBehavior: hitTestBehavior,
502 503
      behavior: behavior,
      action: action,
504
      actionOverflowThreshold: actionOverflowThreshold,
505 506
      showCloseIcon: showCloseIcon,
      closeIconColor: closeIconColor,
507 508 509
      duration: duration,
      animation: newAnimation,
      onVisible: onVisible,
510
      dismissDirection: dismissDirection,
511
      clipBehavior: clipBehavior,
512 513 514 515 516 517 518 519 520 521 522 523 524
    );
  }

  @override
  State<SnackBar> createState() => _SnackBarState();
}

class _SnackBarState extends State<SnackBar> {
  bool _wasVisible = false;

  @override
  void initState() {
    super.initState();
525
    widget.animation!.addStatusListener(_onAnimationStatusChanged);
526 527 528 529
  }

  @override
  void didUpdateWidget(SnackBar oldWidget) {
530
    super.didUpdateWidget(oldWidget);
531
    if (widget.animation != oldWidget.animation) {
532 533
      oldWidget.animation!.removeStatusListener(_onAnimationStatusChanged);
      widget.animation!.addStatusListener(_onAnimationStatusChanged);
534 535 536 537 538
    }
  }

  @override
  void dispose() {
539
    widget.animation!.removeStatusListener(_onAnimationStatusChanged);
540 541 542 543 544 545 546 547 548 549 550
    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) {
551
          widget.onVisible!();
552 553 554 555 556
        }
        _wasVisible = true;
    }
  }

557
  @override
558
  Widget build(BuildContext context) {
559
    assert(debugCheckHasMediaQuery(context));
560
    final bool accessibleNavigation = MediaQuery.accessibleNavigationOf(context);
561
    assert(widget.animation != null);
562
    final ThemeData theme = Theme.of(context);
563
    final ColorScheme colorScheme = theme.colorScheme;
564
    final SnackBarThemeData snackBarTheme = theme.snackBarTheme;
565
    final bool isThemeDark = theme.brightness == Brightness.dark;
566 567 568 569
    final Color buttonColor =  isThemeDark ? colorScheme.primary : colorScheme.secondary;
    final SnackBarThemeData defaults = theme.useMaterial3
        ? _SnackbarDefaultsM3(context)
        : _SnackbarDefaultsM2(context);
570 571 572 573 574

    // SnackBar uses a theme that is the opposite brightness from
    // the surrounding theme.
    final Brightness brightness = isThemeDark ? Brightness.light : Brightness.dark;

Lioness100's avatar
Lioness100 committed
575
    // Invert the theme values for Material 2. Material 3 values are tokenized to pre-inverted values.
576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595
    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!;
596
    final double? width = widget.width ?? snackBarTheme.width;
597 598 599 600 601 602 603 604 605 606 607 608 609 610 611
    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'));
612
        assert(width == null, message('Width'));
613 614 615 616
      }
      return true;
    }());

617 618
    final bool showCloseIcon =  widget.showCloseIcon ?? snackBarTheme.showCloseIcon ?? defaults.showCloseIcon!;

619
    final bool isFloatingSnackBar = snackBarBehavior == SnackBarBehavior.floating;
620
    final double horizontalPadding = isFloatingSnackBar ? 16.0 : 24.0;
621 622 623 624 625 626
    final EdgeInsetsGeometry padding = widget.padding ??
        EdgeInsetsDirectional.only(
            start: horizontalPadding,
            end: widget.action != null || showCloseIcon
                ? 0
                : horizontalPadding);
627

628
    final double actionHorizontalMargin = (widget.padding?.resolve(TextDirection.ltr).right ?? horizontalPadding) / 2;
629
    final double iconHorizontalMargin = (widget.padding?.resolve(TextDirection.ltr).right ?? horizontalPadding) / 12.0;
630

631 632
    final CurvedAnimation heightAnimation = CurvedAnimation(parent: widget.animation!, curve: _snackBarHeightCurve);
    final CurvedAnimation fadeInAnimation = CurvedAnimation(parent: widget.animation!, curve: _snackBarFadeInCurve);
633 634
    final CurvedAnimation fadeInM3Animation = CurvedAnimation(parent: widget.animation!, curve: _snackBarM3FadeInCurve);

635
    final CurvedAnimation fadeOutAnimation = CurvedAnimation(
636
      parent: widget.animation!,
637 638 639
      curve: _snackBarFadeOutCurve,
      reverseCurve: const Threshold(0.0),
    );
640 641 642 643 644 645
    // 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),
    );
646

647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668

    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);
669
    actionTextPainter.dispose();
670 671 672

    final EdgeInsets margin = widget.margin?.resolve(TextDirection.ltr) ?? snackBarTheme.insetPadding ?? defaults.insetPadding!;

673
    final double snackBarWidth = widget.width ?? MediaQuery.sizeOf(context).width - (margin.left + margin.right);
674 675 676 677 678
    final double actionOverflowThreshold = widget.actionOverflowThreshold
      ?? snackBarTheme.actionOverflowThreshold
      ?? defaults.actionOverflowThreshold!;

    final bool willOverflowAction = actionAndIconWidth / snackBarWidth > actionOverflowThreshold;
679 680 681 682 683 684 685 686 687 688

    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),
689 690
              ),
            ),
691
            child: widget.action!,
692
          ),
693 694 695 696 697 698 699 700 701 702
        ),
      if (showCloseIcon)
        Padding(
          padding: EdgeInsets.symmetric(horizontal: iconHorizontalMargin),
          child: iconButton,
        ),
    ];

    Widget snackBar = Padding(
      padding: padding,
703
        child: Wrap(
704 705 706 707 708 709 710 711 712 713 714 715 716
          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,
                    ),
717
                  ),
718
                ),
719 720
                if (!willOverflowAction) ...maybeActionAndIcon,
                if (willOverflowAction) SizedBox(width: snackBarWidth * 0.4),
721 722
              ],
            ),
723 724 725 726 727 728 729
            if (willOverflowAction)
              Padding(
                padding: const EdgeInsets.only(bottom: _singleLineVerticalPadding),
                child: Row(mainAxisAlignment: MainAxisAlignment.end, children: maybeActionAndIcon),
              ),
            ],
        ),
730
    );
731

732 733 734 735 736 737 738
    if (!isFloatingSnackBar) {
      snackBar = SafeArea(
        top: false,
        child: snackBar,
      );
    }

739 740 741
    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);
742
    final DismissDirection dismissDirection = widget.dismissDirection ?? snackBarTheme.dismissDirection ?? DismissDirection.down;
743 744 745 746 747

    snackBar = Material(
      shape: shape,
      elevation: elevation,
      color: backgroundColor,
748
      clipBehavior: widget.clipBehavior,
749
      child: Theme(
750
        data: effectiveTheme,
751
        child: accessibleNavigation || theme.useMaterial3
752 753 754 755 756 757 758 759 760
            ? snackBar
            : FadeTransition(
                opacity: fadeOutAnimation,
                child: snackBar,
              ),
      ),
    );

    if (isFloatingSnackBar) {
761
      // If width is provided, do not include horizontal margins.
762
      if (width != null) {
763
        snackBar = Container(
764
          margin: EdgeInsets.only(top: margin.top, bottom: margin.bottom),
765
          width: width,
766 767 768 769
          child: snackBar,
        );
      } else {
        snackBar = Padding(
770
          padding: margin,
771 772 773 774 775 776
          child: snackBar,
        );
      }
      snackBar = SafeArea(
        top: false,
        bottom: false,
777 778 779 780 781
        child: snackBar,
      );
    }

    snackBar = Semantics(
782 783 784
      container: true,
      liveRegion: true,
      onDismiss: () {
785
        ScaffoldMessenger.of(context).removeCurrentSnackBar(reason: SnackBarClosedReason.dismiss);
786
      },
787
      child: Dismissible(
788
        key: const Key('dismissible'),
789
        direction: dismissDirection,
790
        resizeDuration: null,
791
        behavior: widget.hitTestBehavior ?? (widget.margin != null ? HitTestBehavior.deferToChild : HitTestBehavior.opaque),
792
        onDismissed: (DismissDirection direction) {
793
          ScaffoldMessenger.of(context).removeCurrentSnackBar(reason: SnackBarClosedReason.swipe);
794
        },
795
        child: snackBar,
796 797
      ),
    );
798

799
    final Widget snackBarTransition;
800
    if (accessibleNavigation) {
801
      snackBarTransition = snackBar;
802
    } else if (isFloatingSnackBar && !theme.useMaterial3) {
803 804 805 806
      snackBarTransition = FadeTransition(
        opacity: fadeInAnimation,
        child: snackBar,
      );
807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822
     // 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,
        ),
      );
823 824
    } else {
      snackBarTransition = AnimatedBuilder(
825
        animation: heightAnimation,
826
        builder: (BuildContext context, Widget? child) {
827
          return Align(
828
            alignment: AlignmentDirectional.topStart,
829
            heightFactor: heightAnimation.value,
830
            child: child,
831
          );
832
        },
833 834 835
        child: snackBar,
      );
    }
836

837 838
    return Hero(
      tag: '<SnackBar Hero tag - ${widget.content}>',
839
      transitionOnUserGestures: true,
840 841 842 843
      child: ClipRect(
        clipBehavior: widget.clipBehavior,
        child: snackBarTransition,
      ),
844
    );
845
  }
846
}
847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864

// 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(
865 866 867 868 869 870
    useMaterial3: _theme.useMaterial3,
    brightness: _theme.brightness == Brightness.light
      ? Brightness.dark
      : Brightness.light)
    .textTheme
    .titleMedium;
871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896

  @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;
897 898 899

  @override
  double get actionOverflowThreshold => 0.25;
900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960
}

// 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;
961 962 963 964 965 966

  @override
  Color? get closeIconColor => _colors.onInverseSurface;

  @override
  double get actionOverflowThreshold => 0.25;
967
}
968 969

// END GENERATED TOKEN PROPERTIES - Snackbar