snack_bar.dart 22.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 'material.dart';
11
import 'material_state.dart';
12
import 'scaffold.dart';
13
import 'snack_bar_theme.dart';
14 15
import 'text_button.dart';
import 'text_button_theme.dart';
16
import 'theme.dart';
Matt Perry's avatar
Matt Perry committed
17

18
const double _singleLineVerticalPadding = 14.0;
Matt Perry's avatar
Matt Perry committed
19

Hixie's avatar
Hixie committed
20 21
// TODO(ianh): We should check if the given text and actions are going to fit on
// one line or not, and if they are, use the single-line layout, and if not, use
22 23 24
// the multiline layout, https://github.com/flutter/flutter/issues/32782
// See https://material.io/components/snackbars#specs, 'Longer Action Text' does
// not match spec.
Hixie's avatar
Hixie committed
25

26 27
const Duration _snackBarTransitionDuration = Duration(milliseconds: 250);
const Duration _snackBarDisplayDuration = Duration(milliseconds: 4000);
28
const Curve _snackBarHeightCurve = Curves.fastOutSlowIn;
29 30
const Curve _snackBarFadeInCurve = Interval(0.45, 1.0, curve: Curves.fastOutSlowIn);
const Curve _snackBarFadeOutCurve = Interval(0.72, 1.0, curve: Curves.fastOutSlowIn);
Hixie's avatar
Hixie committed
31

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

52
  /// The snack bar was closed through a [SemanticsAction.dismiss].
53 54
  dismiss,

55 56 57 58
  /// The snack bar was closed by a user's swipe.
  swipe,

  /// The snack bar was closed by the [ScaffoldFeatureController] close callback
59
  /// or by calling [ScaffoldMessengerState.hideCurrentSnackBar] directly.
60 61
  hide,

62
  /// The snack bar was closed by an call to [ScaffoldMessengerState.removeCurrentSnackBar].
63 64 65 66 67 68
  remove,

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

69 70 71 72 73
/// A button for a [SnackBar], known as an "action".
///
/// Snack bar actions are always enabled. If you want to disable a snack bar
/// action, simply don't include it in the snack bar.
///
74 75
/// Snack bar actions can only be pressed once. Subsequent presses are ignored.
///
76 77 78
/// See also:
///
///  * [SnackBar]
jslavitz's avatar
jslavitz committed
79
///  * <https://material.io/design/components/snackbars.html>
80
class SnackBarAction extends StatefulWidget {
81 82 83
  /// Creates an action for a [SnackBar].
  ///
  /// The [label] and [onPressed] arguments must be non-null.
84
  const SnackBarAction({
85
    super.key,
jslavitz's avatar
jslavitz committed
86 87
    this.textColor,
    this.disabledTextColor,
88 89
    required this.label,
    required this.onPressed,
90
  }) : assert(label != null),
91
       assert(onPressed != null);
92

93 94
  /// The button label color. If not provided, defaults to
  /// [SnackBarThemeData.actionTextColor].
95
  final Color? textColor;
jslavitz's avatar
jslavitz committed
96 97

  /// The button disabled label color. This color is shown after the
98
  /// [SnackBarAction] is dismissed.
99
  final Color? disabledTextColor;
jslavitz's avatar
jslavitz committed
100

101
  /// The button label.
102
  final String label;
103

104
  /// The callback to be called when the button is pressed. Must not be null.
105
  ///
106
  /// This callback will be called at most once each time this action is
107
  /// displayed in a [SnackBar].
108
  final VoidCallback onPressed;
109

110
  @override
111
  State<SnackBarAction> createState() => _SnackBarActionState();
112 113 114 115 116 117
}

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

  void _handlePressed() {
118
    if (_haveTriggeredAction) {
119
      return;
120
    }
121 122 123
    setState(() {
      _haveTriggeredAction = true;
    });
124
    widget.onPressed();
125
    ScaffoldMessenger.of(context).hideCurrentSnackBar(reason: SnackBarClosedReason.action);
126 127
  }

128
  @override
Hixie's avatar
Hixie committed
129
  Widget build(BuildContext context) {
130
    Color? resolveForegroundColor(Set<MaterialState> states) {
131
      final SnackBarThemeData snackBarTheme = Theme.of(context).snackBarTheme;
132
      if (states.contains(MaterialState.disabled)) {
133
        return widget.disabledTextColor ?? snackBarTheme.disabledActionTextColor;
134
      }
135 136
      return widget.textColor ?? snackBarTheme.actionTextColor;
    }
137

138 139
    return TextButton(
      style: ButtonStyle(
140
        foregroundColor: MaterialStateProperty.resolveWith<Color?>(resolveForegroundColor),
141
      ),
142
      onPressed: _haveTriggeredAction ? null : _handlePressed,
143
      child: Text(widget.label),
144 145 146
    );
  }
}
147

148 149 150
/// A lightweight message with an optional action which briefly displays at the
/// bottom of the screen.
///
151 152
/// {@youtube 560 315 https://www.youtube.com/watch?v=zpO6n_oZWw0}
///
153 154
/// To display a snack bar, call `ScaffoldMessenger.of(context).showSnackBar()`,
/// passing an instance of [SnackBar] that describes the message.
155 156
///
/// To control how long the [SnackBar] remains visible, specify a [duration].
157
///
158 159 160
/// A SnackBar with an action will not time out when TalkBack or VoiceOver are
/// enabled. This is controlled by [AccessibilityFeatures.accessibleNavigation].
///
161 162 163 164 165 166 167
/// 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.
///
168
/// {@tool dartpad}
169 170 171
/// Here is an example of a [SnackBar] with an [action] button implemented using
/// [SnackBarAction].
///
172
/// ** See code in examples/api/lib/material/snack_bar/snack_bar.0.dart **
173 174
/// {@end-tool}
///
175
/// {@tool dartpad}
176 177 178 179
/// 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.
///
180
/// ** See code in examples/api/lib/material/snack_bar/snack_bar.1.dart **
181 182
/// {@end-tool}
///
183
/// See also:
184
///
185 186 187 188 189
///  * [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.
190 191
///  * [SnackBarAction], which is used to specify an [action] button to show
///    on the snack bar.
192 193
///  * [SnackBarThemeData], to configure the default property values for
///    [SnackBar] widgets.
jslavitz's avatar
jslavitz committed
194
///  * <https://material.io/design/components/snackbars.html>
195
class SnackBar extends StatefulWidget {
196 197
  /// Creates a snack bar.
  ///
198 199
  /// The [content] argument must be non-null. The [elevation] must be null or
  /// non-negative.
200
  const SnackBar({
201
    super.key,
202
    required this.content,
203
    this.backgroundColor,
204
    this.elevation,
205 206 207
    this.margin,
    this.padding,
    this.width,
208 209
    this.shape,
    this.behavior,
210
    this.action,
211
    this.duration = _snackBarDisplayDuration,
212
    this.animation,
213
    this.onVisible,
214
    this.dismissDirection = DismissDirection.down,
215
    this.clipBehavior = Clip.hardEdge,
216 217
  }) : assert(elevation == null || elevation >= 0.0),
       assert(content != null),
218 219 220 221
       assert(
         width == null || margin == null,
         'Width and margin can not be used together',
       ),
222
       assert(duration != null),
223
       assert(clipBehavior != null);
224

225 226 227
  /// The primary content of the snack bar.
  ///
  /// Typically a [Text] widget.
228
  final Widget content;
229

230
  /// The snack bar's background color. If not specified it will use
231 232 233 234
  /// [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.
235
  final Color? backgroundColor;
236

237 238 239 240 241
  /// 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].
  ///
242 243 244
  /// If this property is null, then [SnackBarThemeData.elevation] of
  /// [ThemeData.snackBarTheme] is used, if that is also null, the default value
  /// is 6.0.
245
  final double? elevation;
246

247 248 249 250 251 252 253
  /// 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 the default is
  /// `EdgeInsets.fromLTRB(15.0, 5.0, 15.0, 10.0)`.
254
  final EdgeInsetsGeometry? margin;
255 256 257 258

  /// The amount of padding to apply to the snack bar's content and optional
  /// action.
  ///
259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281
  /// If this property is null, the default padding values for:
  ///
  /// * [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 assignment for:
  ///
  /// * [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 value of right padding.
  ///     * Top and bottom paddings are assigned normally.
  ///     * Right padding has an additional half value of right padding.
  ///       ```dart
  ///       right + (right / 2)
  ///       ```
282
  final EdgeInsetsGeometry? padding;
283 284 285 286 287 288 289 290 291

  /// 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 the snack bar will take up the full device
  /// width less the margin.
292
  final double? width;
293

294 295 296 297
  /// The shape of the snack bar's [Material].
  ///
  /// Defines the snack bar's [Material.shape].
  ///
298 299 300 301 302 303
  /// 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.
304
  final ShapeBorder? shape;
305 306 307 308 309 310 311

  /// 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]
  ///
312 313 314
  /// If this property is null, then [SnackBarThemeData.behavior] of
  /// [ThemeData.snackBarTheme] is used. If that is null, then the default is
  /// [SnackBarBehavior.fixed].
315
  final SnackBarBehavior? behavior;
316

317 318 319 320
  /// (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.
321 322
  ///
  /// The action should not be "dismiss" or "cancel".
323
  final SnackBarAction? action;
324 325

  /// The amount of time the snack bar should be displayed.
326
  ///
jslavitz's avatar
jslavitz committed
327
  /// Defaults to 4.0s.
328 329 330
  ///
  /// See also:
  ///
331
  ///  * [ScaffoldMessengerState.removeCurrentSnackBar], which abruptly hides the
332 333
  ///    currently displayed snack bar, if any, and allows the next to be
  ///    displayed.
jslavitz's avatar
jslavitz committed
334
  ///  * <https://material.io/design/components/snackbars.html>
Hixie's avatar
Hixie committed
335
  final Duration duration;
336 337

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

340
  /// Called the first time that the snackbar is visible within a [Scaffold].
341
  final VoidCallback? onVisible;
342

343 344 345 346 347
  /// The direction in which the SnackBar can be dismissed.
  ///
  /// Cannot be null, defaults to [DismissDirection.down].
  final DismissDirection dismissDirection;

348 349 350 351 352
  /// {@macro flutter.material.Material.clipBehavior}
  ///
  /// Defaults to [Clip.hardEdge], and must not be null.
  final Clip clipBehavior;

353
  // API for ScaffoldMessengerState.showSnackBar():
354 355

  /// Creates an animation controller useful for driving a snack bar's entrance and exit animation.
356
  static AnimationController createAnimationController({ required TickerProvider vsync }) {
357 358 359 360 361 362 363 364 365 366 367
    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.
368
  SnackBar withAnimation(Animation<double> newAnimation, { Key? fallbackKey }) {
369 370 371 372 373
    return SnackBar(
      key: key ?? fallbackKey,
      content: content,
      backgroundColor: backgroundColor,
      elevation: elevation,
374 375 376
      margin: margin,
      padding: padding,
      width: width,
377 378 379 380 381 382
      shape: shape,
      behavior: behavior,
      action: action,
      duration: duration,
      animation: newAnimation,
      onVisible: onVisible,
383
      dismissDirection: dismissDirection,
384
      clipBehavior: clipBehavior,
385 386 387 388 389 390 391 392 393 394 395 396 397
    );
  }

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

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

  @override
  void initState() {
    super.initState();
398
    widget.animation!.addStatusListener(_onAnimationStatusChanged);
399 400 401 402
  }

  @override
  void didUpdateWidget(SnackBar oldWidget) {
403
    super.didUpdateWidget(oldWidget);
404
    if (widget.animation != oldWidget.animation) {
405 406
      oldWidget.animation!.removeStatusListener(_onAnimationStatusChanged);
      widget.animation!.addStatusListener(_onAnimationStatusChanged);
407 408 409 410 411
    }
  }

  @override
  void dispose() {
412
    widget.animation!.removeStatusListener(_onAnimationStatusChanged);
413 414 415 416 417 418 419 420 421 422 423
    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) {
424
          widget.onVisible!();
425 426 427 428 429
        }
        _wasVisible = true;
    }
  }

430
  @override
431
  Widget build(BuildContext context) {
432
    assert(debugCheckHasMediaQuery(context));
433
    final MediaQueryData mediaQueryData = MediaQuery.of(context);
434
    assert(widget.animation != null);
435
    final ThemeData theme = Theme.of(context);
436
    final ColorScheme colorScheme = theme.colorScheme;
437
    final SnackBarThemeData snackBarTheme = theme.snackBarTheme;
438
    final bool isThemeDark = theme.brightness == Brightness.dark;
439
    final Color buttonColor = isThemeDark ? colorScheme.primary : colorScheme.secondary;
440 441 442 443 444 445 446

    // SnackBar uses a theme that is the opposite brightness from
    // the surrounding theme.
    final Brightness brightness = isThemeDark ? Brightness.light : Brightness.dark;
    final Color themeBackgroundColor = isThemeDark
      ? colorScheme.onSurface
      : Color.alphaBlend(colorScheme.onSurface.withOpacity(0.80), colorScheme.surface);
447
    final ThemeData inverseTheme = theme.copyWith(
448 449 450
      colorScheme: ColorScheme(
        primary: colorScheme.onPrimary,
        primaryVariant: colorScheme.onPrimary,
451
        secondary: buttonColor,
452 453 454 455 456 457 458 459 460 461 462
        secondaryVariant: colorScheme.onSecondary,
        surface: colorScheme.onSurface,
        background: themeBackgroundColor,
        error: colorScheme.onError,
        onPrimary: colorScheme.primary,
        onSecondary: colorScheme.secondary,
        onSurface: colorScheme.surface,
        onBackground: colorScheme.background,
        onError: colorScheme.error,
        brightness: brightness,
      ),
463
    );
464

465
    final TextStyle? contentTextStyle = snackBarTheme.contentTextStyle ?? ThemeData(brightness: brightness).textTheme.subtitle1;
466
    final SnackBarBehavior snackBarBehavior = widget.behavior ?? snackBarTheme.behavior ?? SnackBarBehavior.fixed;
467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486
    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(widget.width == null, message('Width'));
      }
      return true;
    }());

487
    final bool isFloatingSnackBar = snackBarBehavior == SnackBarBehavior.floating;
488 489 490
    final double horizontalPadding = isFloatingSnackBar ? 16.0 : 24.0;
    final EdgeInsetsGeometry padding = widget.padding
      ?? EdgeInsetsDirectional.only(start: horizontalPadding, end: widget.action != null ? 0 : horizontalPadding);
491

492 493
    final double actionHorizontalMargin = (widget.padding?.resolve(TextDirection.ltr).right ?? horizontalPadding) / 2;

494 495
    final CurvedAnimation heightAnimation = CurvedAnimation(parent: widget.animation!, curve: _snackBarHeightCurve);
    final CurvedAnimation fadeInAnimation = CurvedAnimation(parent: widget.animation!, curve: _snackBarFadeInCurve);
496
    final CurvedAnimation fadeOutAnimation = CurvedAnimation(
497
      parent: widget.animation!,
498 499 500 501
      curve: _snackBarFadeOutCurve,
      reverseCurve: const Threshold(0.0),
    );

502 503
    Widget snackBar = Padding(
      padding: padding,
504
      child: Row(
505 506 507
        children: <Widget>[
          Expanded(
            child: Container(
508
              padding: widget.padding == null ? const EdgeInsets.symmetric(vertical: _singleLineVerticalPadding) : null,
509
              child: DefaultTextStyle(
510
                style: contentTextStyle!,
511
                child: widget.content,
512 513 514
              ),
            ),
          ),
515
          if (widget.action != null)
516 517 518 519 520 521 522 523
            Padding(
              padding: EdgeInsets.symmetric(horizontal: actionHorizontalMargin),
              child: TextButtonTheme(
                data: TextButtonThemeData(
                  style: TextButton.styleFrom(
                    primary: buttonColor,
                    padding: EdgeInsets.symmetric(horizontal: horizontalPadding),
                  ),
524
                ),
525
                child: widget.action!,
526
              ),
527
            ),
528
        ],
529 530
      ),
    );
531

532 533 534 535 536 537 538
    if (!isFloatingSnackBar) {
      snackBar = SafeArea(
        top: false,
        child: snackBar,
      );
    }

539
    final double elevation = widget.elevation ?? snackBarTheme.elevation ?? 6.0;
540
    final Color backgroundColor = widget.backgroundColor ?? snackBarTheme.backgroundColor ?? inverseTheme.colorScheme.background;
541
    final ShapeBorder? shape = widget.shape
542
      ?? snackBarTheme.shape
543
      ?? (isFloatingSnackBar ? const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0))) : null);
544 545 546 547 548 549

    snackBar = Material(
      shape: shape,
      elevation: elevation,
      color: backgroundColor,
      child: Theme(
550
        data: inverseTheme,
551 552 553 554 555 556 557 558 559 560
        child: mediaQueryData.accessibleNavigation
            ? snackBar
            : FadeTransition(
                opacity: fadeOutAnimation,
                child: snackBar,
              ),
      ),
    );

    if (isFloatingSnackBar) {
561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584
      const double topMargin = 5.0;
      const double bottomMargin = 10.0;
      // If width is provided, do not include horizontal margins.
      if (widget.width != null) {
        snackBar = Container(
          margin: const EdgeInsets.only(top: topMargin, bottom: bottomMargin),
          width: widget.width,
          child: snackBar,
        );
      } else {
        const double horizontalMargin = 15.0;
        snackBar = Padding(
          padding: widget.margin ?? const EdgeInsets.fromLTRB(
            horizontalMargin,
            topMargin,
            horizontalMargin,
            bottomMargin,
          ),
          child: snackBar,
        );
      }
      snackBar = SafeArea(
        top: false,
        bottom: false,
585 586 587 588 589
        child: snackBar,
      );
    }

    snackBar = Semantics(
590 591 592
      container: true,
      liveRegion: true,
      onDismiss: () {
593
        ScaffoldMessenger.of(context).removeCurrentSnackBar(reason: SnackBarClosedReason.dismiss);
594
      },
595
      child: Dismissible(
596
        key: const Key('dismissible'),
597
        direction: widget.dismissDirection,
598 599
        resizeDuration: null,
        onDismissed: (DismissDirection direction) {
600
          ScaffoldMessenger.of(context).removeCurrentSnackBar(reason: SnackBarClosedReason.swipe);
601
        },
602
        child: snackBar,
603 604
      ),
    );
605

606
    final Widget snackBarTransition;
607 608 609 610 611 612 613 614 615
    if (mediaQueryData.accessibleNavigation) {
      snackBarTransition = snackBar;
    } else if (isFloatingSnackBar) {
      snackBarTransition = FadeTransition(
        opacity: fadeInAnimation,
        child: snackBar,
      );
    } else {
      snackBarTransition = AnimatedBuilder(
616
        animation: heightAnimation,
617
        builder: (BuildContext context, Widget? child) {
618
          return Align(
619
            alignment: AlignmentDirectional.topStart,
620
            heightFactor: heightAnimation.value,
621
            child: child,
622
          );
623
        },
624 625 626
        child: snackBar,
      );
    }
627

628 629
    return Hero(
      tag: '<SnackBar Hero tag - ${widget.content}>',
630
      transitionOnUserGestures: true,
631 632 633 634
      child: ClipRect(
        clipBehavior: widget.clipBehavior,
        child: snackBarTransition,
      ),
635
    );
636
  }
637
}