snack_bar.dart 33.8 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 = DismissDirection.down,
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
  /// Defaults to [DismissDirection.down].
467 468
  final DismissDirection dismissDirection;

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

474
  // API for ScaffoldMessengerState.showSnackBar():
475 476

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

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

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

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

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

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

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

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

615 616
    final bool showCloseIcon =  widget.showCloseIcon ?? snackBarTheme.showCloseIcon ?? defaults.showCloseIcon!;

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

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

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

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

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

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

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

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

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

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

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

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

737 738 739
    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);
740 741 742 743 744

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

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

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

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

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

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

  @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;
894 895 896

  @override
  double get actionOverflowThreshold => 0.25;
897 898 899 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
}

// 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;
958 959 960 961 962 963

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

  @override
  double get actionOverflowThreshold => 0.25;
964
}
965 966

// END GENERATED TOKEN PROPERTIES - Snackbar