mirror of
https://github.com/flutter/flutter
synced 2024-09-13 21:32:11 +00:00
Update SnackBar to support Material 3 (#115750)
* Add M2 defaults and template skeleton * add MaterialStateColor functionality to ActionTextColor (issue #110402) * Add M2 defaults and template skeleton * updated material 3 tokens * Updated snackbar demo * add theme tests * add gen defaults * formatting * more whitespace fixes * add widget type * update docs * code review changes * Add line overflow functionality * whitespace fixes * update M3 animation * whitespace fixes * add insetPadding param * Modifed icon parameter to showCloseIcon * white space fixes * test fixes * rename iconColor to closeIconColor * debug test fix * de-britishification
This commit is contained in:
parent
17c1dbc473
commit
d58855c499
|
@ -44,6 +44,7 @@ import 'package:gen_defaults/progress_indicator_template.dart';
|
||||||
import 'package:gen_defaults/radio_template.dart';
|
import 'package:gen_defaults/radio_template.dart';
|
||||||
import 'package:gen_defaults/segmented_button_template.dart';
|
import 'package:gen_defaults/segmented_button_template.dart';
|
||||||
import 'package:gen_defaults/slider_template.dart';
|
import 'package:gen_defaults/slider_template.dart';
|
||||||
|
import 'package:gen_defaults/snackbar_template.dart';
|
||||||
import 'package:gen_defaults/surface_tint.dart';
|
import 'package:gen_defaults/surface_tint.dart';
|
||||||
import 'package:gen_defaults/switch_template.dart';
|
import 'package:gen_defaults/switch_template.dart';
|
||||||
import 'package:gen_defaults/text_field_template.dart';
|
import 'package:gen_defaults/text_field_template.dart';
|
||||||
|
@ -161,6 +162,7 @@ Future<void> main(List<String> args) async {
|
||||||
ProgressIndicatorTemplate('ProgressIndicator', '$materialLib/progress_indicator.dart', tokens).updateFile();
|
ProgressIndicatorTemplate('ProgressIndicator', '$materialLib/progress_indicator.dart', tokens).updateFile();
|
||||||
RadioTemplate('Radio<T>', '$materialLib/radio.dart', tokens).updateFile();
|
RadioTemplate('Radio<T>', '$materialLib/radio.dart', tokens).updateFile();
|
||||||
SegmentedButtonTemplate('SegmentedButton', '$materialLib/segmented_button.dart', tokens).updateFile();
|
SegmentedButtonTemplate('SegmentedButton', '$materialLib/segmented_button.dart', tokens).updateFile();
|
||||||
|
SnackbarTemplate('md.comp.snackbar', 'Snackbar', '$materialLib/snack_bar.dart', tokens).updateFile();
|
||||||
SliderTemplate('md.comp.slider', 'Slider', '$materialLib/slider.dart', tokens).updateFile();
|
SliderTemplate('md.comp.slider', 'Slider', '$materialLib/slider.dart', tokens).updateFile();
|
||||||
SurfaceTintTemplate('SurfaceTint', '$materialLib/elevation_overlay.dart', tokens).updateFile();
|
SurfaceTintTemplate('SurfaceTint', '$materialLib/elevation_overlay.dart', tokens).updateFile();
|
||||||
SwitchTemplate('Switch', '$materialLib/switch.dart', tokens).updateFile();
|
SwitchTemplate('Switch', '$materialLib/switch.dart', tokens).updateFile();
|
||||||
|
|
76
dev/tools/gen_defaults/lib/snackbar_template.dart
Normal file
76
dev/tools/gen_defaults/lib/snackbar_template.dart
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'template.dart';
|
||||||
|
|
||||||
|
class SnackbarTemplate extends TokenTemplate {
|
||||||
|
const SnackbarTemplate(
|
||||||
|
this.tokenGroup, super.blockName, super.fileName, super.tokens, {
|
||||||
|
super.colorSchemePrefix = '_colors.'
|
||||||
|
});
|
||||||
|
|
||||||
|
final String tokenGroup;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String generate() => '''
|
||||||
|
class _${blockName}DefaultsM3 extends SnackBarThemeData {
|
||||||
|
_${blockName}DefaultsM3(this.context);
|
||||||
|
|
||||||
|
final BuildContext context;
|
||||||
|
late final ThemeData _theme = Theme.of(context);
|
||||||
|
|
||||||
|
late final ColorScheme _colors = _theme.colorScheme;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Color get backgroundColor => ${componentColor("$tokenGroup.container")};
|
||||||
|
|
||||||
|
@override
|
||||||
|
Color get actionTextColor => MaterialStateColor.resolveWith((Set<MaterialState> states) {
|
||||||
|
if (states.contains(MaterialState.disabled)) {
|
||||||
|
return ${componentColor("$tokenGroup.action.pressed.label-text")};
|
||||||
|
}
|
||||||
|
if (states.contains(MaterialState.pressed)) {
|
||||||
|
return ${componentColor("$tokenGroup.action.pressed.label-text")};
|
||||||
|
}
|
||||||
|
if (states.contains(MaterialState.hovered)) {
|
||||||
|
return ${componentColor("$tokenGroup.action.hover.label-text")};
|
||||||
|
}
|
||||||
|
if (states.contains(MaterialState.focused)) {
|
||||||
|
return ${componentColor("$tokenGroup.action.focus.label-text")};
|
||||||
|
}
|
||||||
|
return ${componentColor("$tokenGroup.action.label-text")};
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Color get disabledActionTextColor =>
|
||||||
|
${componentColor("$tokenGroup.action.pressed.label-text")};
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
TextStyle get contentTextStyle =>
|
||||||
|
${textStyle("$tokenGroup.supporting-text")}!.copyWith
|
||||||
|
(color: ${componentColor("$tokenGroup.supporting-text")},
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
double get elevation => ${elevation("$tokenGroup.container")};
|
||||||
|
|
||||||
|
@override
|
||||||
|
ShapeBorder get shape => ${shape("$tokenGroup.container")};
|
||||||
|
|
||||||
|
@override
|
||||||
|
SnackBarBehavior get behavior => SnackBarBehavior.fixed;
|
||||||
|
|
||||||
|
@override
|
||||||
|
EdgeInsets get insetPadding => const EdgeInsets.fromLTRB(15.0, 5.0, 15.0, 10.0);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get showCloseIcon => false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Color get iconColor => _colors.onInverseSurface;
|
||||||
|
}
|
||||||
|
|
||||||
|
''';
|
||||||
|
}
|
168
examples/api/lib/material/snack_bar/snack_bar.2.dart
Normal file
168
examples/api/lib/material/snack_bar/snack_bar.2.dart
Normal file
|
@ -0,0 +1,168 @@
|
||||||
|
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
/// Flutter code sample for [SnackBar] with Material 3 specifications.
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
void main() => runApp(const MyApp());
|
||||||
|
|
||||||
|
// A Material 3 [SnackBar] demonstrating an optional icon, in either floating
|
||||||
|
// or fixed format.
|
||||||
|
class MyApp extends StatelessWidget {
|
||||||
|
const MyApp({super.key});
|
||||||
|
|
||||||
|
static const String _title = 'Flutter Code Sample';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp(
|
||||||
|
title: _title,
|
||||||
|
theme: ThemeData(useMaterial3: true),
|
||||||
|
home: Scaffold(
|
||||||
|
appBar: AppBar(title: const Text(_title)),
|
||||||
|
body: const Center(
|
||||||
|
child: SnackBarExample(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SnackBarExample extends StatefulWidget {
|
||||||
|
const SnackBarExample({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SnackBarExample> createState() => _SnackBarExampleState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SnackBarExampleState extends State<SnackBarExample> {
|
||||||
|
SnackBarBehavior? _snackBarBehavior = SnackBarBehavior.floating;
|
||||||
|
bool _withIcon = true;
|
||||||
|
bool _withAction = true;
|
||||||
|
bool _multiLine = false;
|
||||||
|
bool _longActionLabel = false;
|
||||||
|
|
||||||
|
Padding _configRow(List<Widget> children) => Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0), child: Row(children: children));
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(padding: const EdgeInsets.only(left: 50.0), child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
_configRow(<Widget>[
|
||||||
|
Text('Snack Bar configuration',
|
||||||
|
style: Theme.of(context).textTheme.bodyLarge),
|
||||||
|
]),
|
||||||
|
_configRow(
|
||||||
|
<Widget>[
|
||||||
|
const Text('Fixed'),
|
||||||
|
Radio<SnackBarBehavior>(
|
||||||
|
value: SnackBarBehavior.fixed,
|
||||||
|
groupValue: _snackBarBehavior,
|
||||||
|
onChanged: (SnackBarBehavior? value) {
|
||||||
|
setState(() {
|
||||||
|
_snackBarBehavior = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const Text('Floating'),
|
||||||
|
Radio<SnackBarBehavior>(
|
||||||
|
value: SnackBarBehavior.floating,
|
||||||
|
groupValue: _snackBarBehavior,
|
||||||
|
onChanged: (SnackBarBehavior? value) {
|
||||||
|
setState(() {
|
||||||
|
_snackBarBehavior = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
_configRow(
|
||||||
|
<Widget>[
|
||||||
|
const Text('Include Icon '),
|
||||||
|
Switch(
|
||||||
|
value: _withIcon,
|
||||||
|
onChanged: (bool value) {
|
||||||
|
setState(() {
|
||||||
|
_withIcon = !_withIcon;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
_configRow(
|
||||||
|
<Widget>[
|
||||||
|
const Text('Include Action '),
|
||||||
|
Switch(
|
||||||
|
value: _withAction,
|
||||||
|
onChanged: (bool value) {
|
||||||
|
setState(() {
|
||||||
|
_withAction = !_withAction;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16.0),
|
||||||
|
const Text('Long Action Label '),
|
||||||
|
Switch(
|
||||||
|
value: _longActionLabel,
|
||||||
|
onChanged: !_withAction
|
||||||
|
? null
|
||||||
|
: (bool value) {
|
||||||
|
setState(() {
|
||||||
|
_longActionLabel = !_longActionLabel;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
_configRow(
|
||||||
|
<Widget>[
|
||||||
|
const Text('Multi Line Text'),
|
||||||
|
Switch(
|
||||||
|
value: _multiLine,
|
||||||
|
onChanged: _snackBarBehavior == SnackBarBehavior.fixed ? null : (bool value) {
|
||||||
|
setState(() {
|
||||||
|
_multiLine = !_multiLine;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16.0),
|
||||||
|
ElevatedButton(
|
||||||
|
child: const Text('Show Snackbar'),
|
||||||
|
onPressed: () {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(_snackBar());
|
||||||
|
}
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SnackBar _snackBar() {
|
||||||
|
final SnackBarAction? action = _withAction
|
||||||
|
? SnackBarAction(
|
||||||
|
label: _longActionLabel ? 'Long Action Text' : 'Action',
|
||||||
|
onPressed: () {
|
||||||
|
// Code to execute.
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
final double? width =
|
||||||
|
_snackBarBehavior == SnackBarBehavior.floating && _multiLine ? 400.0 : null;
|
||||||
|
final String label = _multiLine
|
||||||
|
? 'A Snack Bar with quite a lot of text which spans across multiple lines'
|
||||||
|
: 'Single Line Snack Bar';
|
||||||
|
return SnackBar(
|
||||||
|
content: Text(label),
|
||||||
|
showCloseIcon: _withIcon,
|
||||||
|
width: width,
|
||||||
|
behavior: _snackBarBehavior,
|
||||||
|
action: action,
|
||||||
|
duration: const Duration(seconds: 3),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,6 +7,8 @@ import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
import 'button_style.dart';
|
import 'button_style.dart';
|
||||||
import 'color_scheme.dart';
|
import 'color_scheme.dart';
|
||||||
|
import 'icon_button.dart';
|
||||||
|
import 'icons.dart';
|
||||||
import 'material.dart';
|
import 'material.dart';
|
||||||
import 'material_state.dart';
|
import 'material_state.dart';
|
||||||
import 'scaffold.dart';
|
import 'scaffold.dart';
|
||||||
|
@ -19,17 +21,13 @@ import 'theme.dart';
|
||||||
// late BuildContext context;
|
// late BuildContext context;
|
||||||
|
|
||||||
const double _singleLineVerticalPadding = 14.0;
|
const double _singleLineVerticalPadding = 14.0;
|
||||||
|
|
||||||
// TODO(ianh): We should check if the given text and actions are going to fit on
|
|
||||||
// one line or not, and if they are, use the single-line layout, and if not, use
|
|
||||||
// the multiline layout, https://github.com/flutter/flutter/issues/32782
|
|
||||||
// See https://material.io/components/snackbars#specs, 'Longer Action Text' does
|
|
||||||
// not match spec.
|
|
||||||
|
|
||||||
const Duration _snackBarTransitionDuration = Duration(milliseconds: 250);
|
const Duration _snackBarTransitionDuration = Duration(milliseconds: 250);
|
||||||
const Duration _snackBarDisplayDuration = Duration(milliseconds: 4000);
|
const Duration _snackBarDisplayDuration = Duration(milliseconds: 4000);
|
||||||
const Curve _snackBarHeightCurve = Curves.fastOutSlowIn;
|
const Curve _snackBarHeightCurve = Curves.fastOutSlowIn;
|
||||||
const Curve _snackBarFadeInCurve = Interval(0.45, 1.0, curve: Curves.fastOutSlowIn);
|
const Curve _snackBarM3HeightCurve = Curves.easeInOutQuart;
|
||||||
|
|
||||||
|
const Curve _snackBarFadeInCurve = Interval(0.4, 1.0);
|
||||||
|
const Curve _snackBarM3FadeInCurve = Interval(0.4, 0.6, curve: Curves.easeInCirc);
|
||||||
const Curve _snackBarFadeOutCurve = Interval(0.72, 1.0, curve: Curves.fastOutSlowIn);
|
const Curve _snackBarFadeOutCurve = Interval(0.72, 1.0, curve: Curves.fastOutSlowIn);
|
||||||
|
|
||||||
/// Specify how a [SnackBar] was closed.
|
/// Specify how a [SnackBar] was closed.
|
||||||
|
@ -97,6 +95,11 @@ class SnackBarAction extends StatefulWidget {
|
||||||
|
|
||||||
/// The button label color. If not provided, defaults to
|
/// The button label color. If not provided, defaults to
|
||||||
/// [SnackBarThemeData.actionTextColor].
|
/// [SnackBarThemeData.actionTextColor].
|
||||||
|
///
|
||||||
|
/// If [textColor] is a [MaterialStateColor], then the text color will be
|
||||||
|
/// be resolved against the set of [MaterialState]s that the action text
|
||||||
|
/// is in, thus allowing for different colors for states such as pressed,
|
||||||
|
/// hovered and others.
|
||||||
final Color? textColor;
|
final Color? textColor;
|
||||||
|
|
||||||
/// The button disabled label color. This color is shown after the
|
/// The button disabled label color. This color is shown after the
|
||||||
|
@ -132,17 +135,36 @@ class _SnackBarActionState extends State<SnackBarAction> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Color? resolveForegroundColor(Set<MaterialState> states) {
|
final SnackBarThemeData defaults = Theme.of(context).useMaterial3
|
||||||
final SnackBarThemeData snackBarTheme = Theme.of(context).snackBarTheme;
|
? _SnackbarDefaultsM3(context)
|
||||||
if (states.contains(MaterialState.disabled)) {
|
: _SnackbarDefaultsM2(context);
|
||||||
return widget.disabledTextColor ?? snackBarTheme.disabledActionTextColor;
|
final SnackBarThemeData snackBarTheme = Theme.of(context).snackBarTheme;
|
||||||
|
|
||||||
|
MaterialStateColor resolveForegroundColor() {
|
||||||
|
if (widget.textColor is MaterialStateColor) {
|
||||||
|
return widget.textColor! as MaterialStateColor;
|
||||||
}
|
}
|
||||||
return widget.textColor ?? snackBarTheme.actionTextColor;
|
if (snackBarTheme.actionTextColor is MaterialStateColor) {
|
||||||
|
return snackBarTheme.actionTextColor! as MaterialStateColor;
|
||||||
|
}
|
||||||
|
if (defaults.actionTextColor is MaterialStateColor) {
|
||||||
|
return defaults.actionTextColor! as MaterialStateColor;
|
||||||
|
}
|
||||||
|
return MaterialStateColor.resolveWith((Set<MaterialState> states) {
|
||||||
|
if (states.contains(MaterialState.disabled)) {
|
||||||
|
return widget.disabledTextColor ??
|
||||||
|
snackBarTheme.disabledActionTextColor ??
|
||||||
|
defaults.disabledActionTextColor!;
|
||||||
|
}
|
||||||
|
return widget.textColor ??
|
||||||
|
snackBarTheme.actionTextColor ??
|
||||||
|
defaults.actionTextColor!;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return TextButton(
|
return TextButton(
|
||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
foregroundColor: MaterialStateProperty.resolveWith<Color?>(resolveForegroundColor),
|
foregroundColor: resolveForegroundColor(),
|
||||||
),
|
),
|
||||||
onPressed: _haveTriggeredAction ? null : _handlePressed,
|
onPressed: _haveTriggeredAction ? null : _handlePressed,
|
||||||
child: Text(widget.label),
|
child: Text(widget.label),
|
||||||
|
@ -213,6 +235,8 @@ class SnackBar extends StatefulWidget {
|
||||||
this.shape,
|
this.shape,
|
||||||
this.behavior,
|
this.behavior,
|
||||||
this.action,
|
this.action,
|
||||||
|
this.showCloseIcon,
|
||||||
|
this.closeIconColor,
|
||||||
this.duration = _snackBarDisplayDuration,
|
this.duration = _snackBarDisplayDuration,
|
||||||
this.animation,
|
this.animation,
|
||||||
this.onVisible,
|
this.onVisible,
|
||||||
|
@ -255,7 +279,8 @@ class SnackBar extends StatefulWidget {
|
||||||
/// This property is only used when [behavior] is [SnackBarBehavior.floating].
|
/// This property is only used when [behavior] is [SnackBarBehavior.floating].
|
||||||
/// It can not be used if [width] is specified.
|
/// It can not be used if [width] is specified.
|
||||||
///
|
///
|
||||||
/// If this property is null, then the default is
|
/// If this property is null, then [SnackBarThemeData.insetPadding] of
|
||||||
|
/// [ThemeData.snackBarTheme] is used. If that is also null, then the default is
|
||||||
/// `EdgeInsets.fromLTRB(15.0, 5.0, 15.0, 10.0)`.
|
/// `EdgeInsets.fromLTRB(15.0, 5.0, 15.0, 10.0)`.
|
||||||
final EdgeInsetsGeometry? margin;
|
final EdgeInsetsGeometry? margin;
|
||||||
|
|
||||||
|
@ -319,6 +344,9 @@ class SnackBar extends StatefulWidget {
|
||||||
/// If this property is null, then [SnackBarThemeData.behavior] of
|
/// If this property is null, then [SnackBarThemeData.behavior] of
|
||||||
/// [ThemeData.snackBarTheme] is used. If that is null, then the default is
|
/// [ThemeData.snackBarTheme] is used. If that is null, then the default is
|
||||||
/// [SnackBarBehavior.fixed].
|
/// [SnackBarBehavior.fixed].
|
||||||
|
///
|
||||||
|
/// If this value is [SnackBarBehavior.floating], the length of the bar
|
||||||
|
/// is defined by either [width] or [margin].
|
||||||
final SnackBarBehavior? behavior;
|
final SnackBarBehavior? behavior;
|
||||||
|
|
||||||
/// (optional) An action that the user can take based on the snack bar.
|
/// (optional) An action that the user can take based on the snack bar.
|
||||||
|
@ -329,6 +357,24 @@ class SnackBar extends StatefulWidget {
|
||||||
/// The action should not be "dismiss" or "cancel".
|
/// The action should not be "dismiss" or "cancel".
|
||||||
final SnackBarAction? action;
|
final SnackBarAction? action;
|
||||||
|
|
||||||
|
/// (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
|
||||||
|
/// be resolved against the set of [MaterialState]s that the action text
|
||||||
|
/// is in, thus allowing for different colors for states such as pressed,
|
||||||
|
/// hovered and others.
|
||||||
|
final Color? closeIconColor;
|
||||||
|
|
||||||
/// The amount of time the snack bar should be displayed.
|
/// The amount of time the snack bar should be displayed.
|
||||||
///
|
///
|
||||||
/// Defaults to 4.0s.
|
/// Defaults to 4.0s.
|
||||||
|
@ -384,6 +430,8 @@ class SnackBar extends StatefulWidget {
|
||||||
shape: shape,
|
shape: shape,
|
||||||
behavior: behavior,
|
behavior: behavior,
|
||||||
action: action,
|
action: action,
|
||||||
|
showCloseIcon: showCloseIcon,
|
||||||
|
closeIconColor: closeIconColor,
|
||||||
duration: duration,
|
duration: duration,
|
||||||
animation: newAnimation,
|
animation: newAnimation,
|
||||||
onVisible: onVisible,
|
onVisible: onVisible,
|
||||||
|
@ -443,34 +491,38 @@ class _SnackBarState extends State<SnackBar> {
|
||||||
final ColorScheme colorScheme = theme.colorScheme;
|
final ColorScheme colorScheme = theme.colorScheme;
|
||||||
final SnackBarThemeData snackBarTheme = theme.snackBarTheme;
|
final SnackBarThemeData snackBarTheme = theme.snackBarTheme;
|
||||||
final bool isThemeDark = theme.brightness == Brightness.dark;
|
final bool isThemeDark = theme.brightness == Brightness.dark;
|
||||||
final Color buttonColor = isThemeDark ? colorScheme.primary : colorScheme.secondary;
|
final Color buttonColor = isThemeDark ? colorScheme.primary : colorScheme.secondary;
|
||||||
|
final SnackBarThemeData defaults = theme.useMaterial3
|
||||||
|
? _SnackbarDefaultsM3(context)
|
||||||
|
: _SnackbarDefaultsM2(context);
|
||||||
|
|
||||||
// SnackBar uses a theme that is the opposite brightness from
|
// SnackBar uses a theme that is the opposite brightness from
|
||||||
// the surrounding theme.
|
// the surrounding theme.
|
||||||
final Brightness brightness = isThemeDark ? Brightness.light : Brightness.dark;
|
final Brightness brightness = isThemeDark ? Brightness.light : Brightness.dark;
|
||||||
final Color themeBackgroundColor = isThemeDark
|
|
||||||
? colorScheme.onSurface
|
|
||||||
: Color.alphaBlend(colorScheme.onSurface.withOpacity(0.80), colorScheme.surface);
|
|
||||||
final ThemeData inverseTheme = theme.copyWith(
|
|
||||||
colorScheme: ColorScheme(
|
|
||||||
primary: colorScheme.onPrimary,
|
|
||||||
primaryVariant: colorScheme.onPrimary,
|
|
||||||
secondary: buttonColor,
|
|
||||||
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,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
final TextStyle? contentTextStyle = snackBarTheme.contentTextStyle ?? ThemeData(brightness: brightness).textTheme.titleMedium;
|
// Invert the theme values for Material 2. Material 3 values are tokenzied to pre-inverted values.
|
||||||
final SnackBarBehavior snackBarBehavior = widget.behavior ?? snackBarTheme.behavior ?? SnackBarBehavior.fixed;
|
final ThemeData effectiveTheme = theme.useMaterial3
|
||||||
|
? theme
|
||||||
|
: theme.copyWith(
|
||||||
|
colorScheme: ColorScheme(
|
||||||
|
primary: colorScheme.onPrimary,
|
||||||
|
primaryVariant: colorScheme.onPrimary,
|
||||||
|
secondary: buttonColor,
|
||||||
|
secondaryVariant: colorScheme.onSecondary,
|
||||||
|
surface: colorScheme.onSurface,
|
||||||
|
background: defaults.backgroundColor!,
|
||||||
|
error: colorScheme.onError,
|
||||||
|
onPrimary: colorScheme.primary,
|
||||||
|
onSecondary: colorScheme.secondary,
|
||||||
|
onSurface: colorScheme.surface,
|
||||||
|
onBackground: colorScheme.background,
|
||||||
|
onError: colorScheme.error,
|
||||||
|
brightness: brightness,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final TextStyle? contentTextStyle = snackBarTheme.contentTextStyle ?? defaults.contentTextStyle;
|
||||||
|
final SnackBarBehavior snackBarBehavior = widget.behavior ?? snackBarTheme.behavior ?? defaults.behavior!;
|
||||||
final double? width = widget.width ?? snackBarTheme.width;
|
final double? width = widget.width ?? snackBarTheme.width;
|
||||||
assert((){
|
assert((){
|
||||||
// Whether the behavior is set through the constructor or the theme,
|
// Whether the behavior is set through the constructor or the theme,
|
||||||
|
@ -492,47 +544,116 @@ class _SnackBarState extends State<SnackBar> {
|
||||||
return true;
|
return true;
|
||||||
}());
|
}());
|
||||||
|
|
||||||
|
final bool showCloseIcon = widget.showCloseIcon ?? snackBarTheme.showCloseIcon ?? defaults.showCloseIcon!;
|
||||||
|
|
||||||
final bool isFloatingSnackBar = snackBarBehavior == SnackBarBehavior.floating;
|
final bool isFloatingSnackBar = snackBarBehavior == SnackBarBehavior.floating;
|
||||||
final double horizontalPadding = isFloatingSnackBar ? 16.0 : 24.0;
|
final double horizontalPadding = isFloatingSnackBar ? 16.0 : 24.0;
|
||||||
final EdgeInsetsGeometry padding = widget.padding
|
final EdgeInsetsGeometry padding = widget.padding ??
|
||||||
?? EdgeInsetsDirectional.only(start: horizontalPadding, end: widget.action != null ? 0 : horizontalPadding);
|
EdgeInsetsDirectional.only(
|
||||||
|
start: horizontalPadding,
|
||||||
|
end: widget.action != null || showCloseIcon
|
||||||
|
? 0
|
||||||
|
: horizontalPadding);
|
||||||
|
|
||||||
final double actionHorizontalMargin = (widget.padding?.resolve(TextDirection.ltr).right ?? horizontalPadding) / 2;
|
final double actionHorizontalMargin = (widget.padding?.resolve(TextDirection.ltr).right ?? horizontalPadding) / 2;
|
||||||
|
final double iconHorizontalMargin = (widget.padding?.resolve(TextDirection.ltr).right ?? horizontalPadding) / 12.0;
|
||||||
|
|
||||||
final CurvedAnimation heightAnimation = CurvedAnimation(parent: widget.animation!, curve: _snackBarHeightCurve);
|
final CurvedAnimation heightAnimation = CurvedAnimation(parent: widget.animation!, curve: _snackBarHeightCurve);
|
||||||
final CurvedAnimation fadeInAnimation = CurvedAnimation(parent: widget.animation!, curve: _snackBarFadeInCurve);
|
final CurvedAnimation fadeInAnimation = CurvedAnimation(parent: widget.animation!, curve: _snackBarFadeInCurve);
|
||||||
|
final CurvedAnimation fadeInM3Animation = CurvedAnimation(parent: widget.animation!, curve: _snackBarM3FadeInCurve);
|
||||||
|
|
||||||
final CurvedAnimation fadeOutAnimation = CurvedAnimation(
|
final CurvedAnimation fadeOutAnimation = CurvedAnimation(
|
||||||
parent: widget.animation!,
|
parent: widget.animation!,
|
||||||
curve: _snackBarFadeOutCurve,
|
curve: _snackBarFadeOutCurve,
|
||||||
reverseCurve: const Threshold(0.0),
|
reverseCurve: const Threshold(0.0),
|
||||||
);
|
);
|
||||||
|
// Material 3 Animation has a height animation on entry, but a direct fade out on exit.
|
||||||
|
final CurvedAnimation heightM3Animation = CurvedAnimation(
|
||||||
|
parent: widget.animation!,
|
||||||
|
curve: _snackBarM3HeightCurve,
|
||||||
|
reverseCurve: const Threshold(0.0),
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
final IconButton? iconButton = showCloseIcon
|
||||||
|
? IconButton(
|
||||||
|
icon: const Icon(Icons.close),
|
||||||
|
iconSize: 24.0,
|
||||||
|
color: widget.closeIconColor ?? snackBarTheme.closeIconColor ?? defaults.closeIconColor,
|
||||||
|
onPressed: () => ScaffoldMessenger.of(context).hideCurrentSnackBar(reason: SnackBarClosedReason.dismiss),
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// Calculate combined width of Action, Icon, and their padding, if they are present.
|
||||||
|
final TextPainter actionTextPainter = TextPainter(
|
||||||
|
text: TextSpan(
|
||||||
|
text: widget.action?.label ?? '',
|
||||||
|
style: Theme.of(context).textTheme.labelLarge,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
textDirection: TextDirection.ltr)
|
||||||
|
..layout();
|
||||||
|
final double actionAndIconWidth = actionTextPainter.size.width +
|
||||||
|
(widget.action != null ? actionHorizontalMargin : 0) +
|
||||||
|
(showCloseIcon ? (iconButton?.iconSize ?? 0 + iconHorizontalMargin) : 0);
|
||||||
|
|
||||||
|
final EdgeInsets margin = widget.margin?.resolve(TextDirection.ltr) ?? snackBarTheme.insetPadding ?? defaults.insetPadding!;
|
||||||
|
|
||||||
|
final double snackBarWidth = widget.width ?? mediaQueryData.size.width - (margin.left + margin.right);
|
||||||
|
// Action and Icon will overflow to a new line if their width is greater
|
||||||
|
// than one quarter of the total Snack Bar width.
|
||||||
|
final bool actionLineOverflow =
|
||||||
|
actionAndIconWidth / snackBarWidth > 0.25;
|
||||||
|
|
||||||
|
final List<Widget> maybeActionAndIcon = <Widget>[
|
||||||
|
if (widget.action != null)
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: actionHorizontalMargin),
|
||||||
|
child: TextButtonTheme(
|
||||||
|
data: TextButtonThemeData(
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
foregroundColor: buttonColor,
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: horizontalPadding),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: widget.action!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (showCloseIcon)
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: iconHorizontalMargin),
|
||||||
|
child: iconButton,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
Widget snackBar = Padding(
|
Widget snackBar = Padding(
|
||||||
padding: padding,
|
padding: padding,
|
||||||
child: Row(
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Expanded(
|
Row(
|
||||||
child: Container(
|
children: <Widget>[
|
||||||
padding: widget.padding == null ? const EdgeInsets.symmetric(vertical: _singleLineVerticalPadding) : null,
|
Expanded(
|
||||||
child: DefaultTextStyle(
|
child: Container(
|
||||||
style: contentTextStyle!,
|
padding: widget.padding == null
|
||||||
child: widget.content,
|
? const EdgeInsets.symmetric(
|
||||||
),
|
vertical: _singleLineVerticalPadding)
|
||||||
),
|
: null,
|
||||||
),
|
child: DefaultTextStyle(
|
||||||
if (widget.action != null)
|
style: contentTextStyle!,
|
||||||
Padding(
|
child: widget.content,
|
||||||
padding: EdgeInsets.symmetric(horizontal: actionHorizontalMargin),
|
|
||||||
child: TextButtonTheme(
|
|
||||||
data: TextButtonThemeData(
|
|
||||||
style: TextButton.styleFrom(
|
|
||||||
foregroundColor: buttonColor,
|
|
||||||
padding: EdgeInsets.symmetric(horizontal: horizontalPadding),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: widget.action!,
|
|
||||||
),
|
),
|
||||||
),
|
if(!actionLineOverflow) ...maybeActionAndIcon,
|
||||||
|
if(actionLineOverflow) SizedBox(width: snackBarWidth*0.4),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if(actionLineOverflow) Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: _singleLineVerticalPadding),
|
||||||
|
child: Row(mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: maybeActionAndIcon),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -544,19 +665,17 @@ class _SnackBarState extends State<SnackBar> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final double elevation = widget.elevation ?? snackBarTheme.elevation ?? 6.0;
|
final double elevation = widget.elevation ?? snackBarTheme.elevation ?? defaults.elevation!;
|
||||||
final Color backgroundColor = widget.backgroundColor ?? snackBarTheme.backgroundColor ?? inverseTheme.colorScheme.background;
|
final Color backgroundColor = widget.backgroundColor ?? snackBarTheme.backgroundColor ?? defaults.backgroundColor!;
|
||||||
final ShapeBorder? shape = widget.shape
|
final ShapeBorder? shape = widget.shape ?? snackBarTheme.shape ?? (isFloatingSnackBar ? defaults.shape : null);
|
||||||
?? snackBarTheme.shape
|
|
||||||
?? (isFloatingSnackBar ? const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0))) : null);
|
|
||||||
|
|
||||||
snackBar = Material(
|
snackBar = Material(
|
||||||
shape: shape,
|
shape: shape,
|
||||||
elevation: elevation,
|
elevation: elevation,
|
||||||
color: backgroundColor,
|
color: backgroundColor,
|
||||||
child: Theme(
|
child: Theme(
|
||||||
data: inverseTheme,
|
data: effectiveTheme,
|
||||||
child: mediaQueryData.accessibleNavigation
|
child: mediaQueryData.accessibleNavigation || theme.useMaterial3
|
||||||
? snackBar
|
? snackBar
|
||||||
: FadeTransition(
|
: FadeTransition(
|
||||||
opacity: fadeOutAnimation,
|
opacity: fadeOutAnimation,
|
||||||
|
@ -566,24 +685,16 @@ class _SnackBarState extends State<SnackBar> {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isFloatingSnackBar) {
|
if (isFloatingSnackBar) {
|
||||||
const double topMargin = 5.0;
|
|
||||||
const double bottomMargin = 10.0;
|
|
||||||
// If width is provided, do not include horizontal margins.
|
// If width is provided, do not include horizontal margins.
|
||||||
if (width != null) {
|
if (width != null) {
|
||||||
snackBar = Container(
|
snackBar = Container(
|
||||||
margin: const EdgeInsets.only(top: topMargin, bottom: bottomMargin),
|
margin: EdgeInsets.only(top: margin.top, bottom: margin.bottom),
|
||||||
width: width,
|
width: width,
|
||||||
child: snackBar,
|
child: snackBar,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const double horizontalMargin = 15.0;
|
|
||||||
snackBar = Padding(
|
snackBar = Padding(
|
||||||
padding: widget.margin ?? const EdgeInsets.fromLTRB(
|
padding: margin,
|
||||||
horizontalMargin,
|
|
||||||
topMargin,
|
|
||||||
horizontalMargin,
|
|
||||||
bottomMargin,
|
|
||||||
),
|
|
||||||
child: snackBar,
|
child: snackBar,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -614,11 +725,27 @@ class _SnackBarState extends State<SnackBar> {
|
||||||
final Widget snackBarTransition;
|
final Widget snackBarTransition;
|
||||||
if (mediaQueryData.accessibleNavigation) {
|
if (mediaQueryData.accessibleNavigation) {
|
||||||
snackBarTransition = snackBar;
|
snackBarTransition = snackBar;
|
||||||
} else if (isFloatingSnackBar) {
|
} else if (isFloatingSnackBar && !theme.useMaterial3) {
|
||||||
snackBarTransition = FadeTransition(
|
snackBarTransition = FadeTransition(
|
||||||
opacity: fadeInAnimation,
|
opacity: fadeInAnimation,
|
||||||
child: snackBar,
|
child: snackBar,
|
||||||
);
|
);
|
||||||
|
// Is Material 3 Floating Snack Bar.
|
||||||
|
} else if (isFloatingSnackBar && theme.useMaterial3) {
|
||||||
|
snackBarTransition = FadeTransition(
|
||||||
|
opacity: fadeInM3Animation,
|
||||||
|
child: AnimatedBuilder(
|
||||||
|
animation: heightM3Animation,
|
||||||
|
builder: (BuildContext context, Widget? child) {
|
||||||
|
return Align(
|
||||||
|
alignment: AlignmentDirectional.bottomStart,
|
||||||
|
heightFactor: heightM3Animation.value,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: snackBar,
|
||||||
|
),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
snackBarTransition = AnimatedBuilder(
|
snackBarTransition = AnimatedBuilder(
|
||||||
animation: heightAnimation,
|
animation: heightAnimation,
|
||||||
|
@ -643,3 +770,123 @@ class _SnackBarState extends State<SnackBar> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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(
|
||||||
|
brightness: _theme.brightness == Brightness.light
|
||||||
|
? Brightness.dark
|
||||||
|
: Brightness.light)
|
||||||
|
.textTheme
|
||||||
|
.titleMedium;
|
||||||
|
|
||||||
|
@override
|
||||||
|
SnackBarBehavior get behavior => SnackBarBehavior.fixed;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Color get actionTextColor => _colors.secondary;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Color get disabledActionTextColor => _colors.onSurface
|
||||||
|
.withOpacity(_theme.brightness == Brightness.light ? 0.38 : 0.3);
|
||||||
|
|
||||||
|
@override
|
||||||
|
ShapeBorder get shape => const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(4.0),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
EdgeInsets get insetPadding => const EdgeInsets.fromLTRB(15.0, 5.0, 15.0, 10.0);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get showCloseIcon => false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Color get closeIconColor => _colors.onSurface;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
// Token database version: v0_143
|
||||||
|
|
||||||
|
class _SnackbarDefaultsM3 extends SnackBarThemeData {
|
||||||
|
_SnackbarDefaultsM3(this.context);
|
||||||
|
|
||||||
|
final BuildContext context;
|
||||||
|
late final ThemeData _theme = Theme.of(context);
|
||||||
|
|
||||||
|
late final ColorScheme _colors = _theme.colorScheme;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Color get backgroundColor => _colors.inverseSurface;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Color get actionTextColor => MaterialStateColor.resolveWith((Set<MaterialState> states) {
|
||||||
|
if (states.contains(MaterialState.disabled)) {
|
||||||
|
return _colors.inversePrimary;
|
||||||
|
}
|
||||||
|
if (states.contains(MaterialState.pressed)) {
|
||||||
|
return _colors.inversePrimary;
|
||||||
|
}
|
||||||
|
if (states.contains(MaterialState.hovered)) {
|
||||||
|
return _colors.inversePrimary;
|
||||||
|
}
|
||||||
|
if (states.contains(MaterialState.focused)) {
|
||||||
|
return _colors.inversePrimary;
|
||||||
|
}
|
||||||
|
return _colors.inversePrimary;
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Color get disabledActionTextColor =>
|
||||||
|
_colors.inversePrimary;
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
TextStyle get contentTextStyle =>
|
||||||
|
Theme.of(context).textTheme.bodyMedium!.copyWith
|
||||||
|
(color: _colors.onInverseSurface,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
double get elevation => 6.0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
ShapeBorder get shape => const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0)));
|
||||||
|
|
||||||
|
@override
|
||||||
|
SnackBarBehavior get behavior => SnackBarBehavior.fixed;
|
||||||
|
|
||||||
|
@override
|
||||||
|
EdgeInsets get insetPadding => const EdgeInsets.fromLTRB(15.0, 5.0, 15.0, 10.0);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get showCloseIcon => false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Color get closeIconColor => _colors.onInverseSurface;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// END GENERATED TOKEN PROPERTIES - Snackbar
|
||||||
|
|
|
@ -61,6 +61,9 @@ class SnackBarThemeData with Diagnosticable {
|
||||||
this.shape,
|
this.shape,
|
||||||
this.behavior,
|
this.behavior,
|
||||||
this.width,
|
this.width,
|
||||||
|
this.insetPadding,
|
||||||
|
this.showCloseIcon,
|
||||||
|
this.closeIconColor,
|
||||||
}) : assert(elevation == null || elevation >= 0.0),
|
}) : assert(elevation == null || elevation >= 0.0),
|
||||||
assert(
|
assert(
|
||||||
width == null ||
|
width == null ||
|
||||||
|
@ -115,6 +118,21 @@ class SnackBarThemeData with Diagnosticable {
|
||||||
/// [SnackBarBehavior.floating].
|
/// [SnackBarBehavior.floating].
|
||||||
final double? width;
|
final double? width;
|
||||||
|
|
||||||
|
/// Overrides the default value for [SnackBar.margin].
|
||||||
|
///
|
||||||
|
/// This value is only used when [behavior] is [SnackBarBehavior.floating].
|
||||||
|
final EdgeInsets? insetPadding;
|
||||||
|
|
||||||
|
/// Overrides the default value for [SnackBar.showCloseIcon].
|
||||||
|
///
|
||||||
|
/// Whether to show an optional "Close" icon.
|
||||||
|
final bool? showCloseIcon;
|
||||||
|
|
||||||
|
/// Overrides the default value for [SnackBar.closeIconColor].
|
||||||
|
///
|
||||||
|
/// This value is only used if [showCloseIcon] is true.
|
||||||
|
final Color? closeIconColor;
|
||||||
|
|
||||||
/// Creates a copy of this object with the given fields replaced with the
|
/// Creates a copy of this object with the given fields replaced with the
|
||||||
/// new values.
|
/// new values.
|
||||||
SnackBarThemeData copyWith({
|
SnackBarThemeData copyWith({
|
||||||
|
@ -126,6 +144,9 @@ class SnackBarThemeData with Diagnosticable {
|
||||||
ShapeBorder? shape,
|
ShapeBorder? shape,
|
||||||
SnackBarBehavior? behavior,
|
SnackBarBehavior? behavior,
|
||||||
double? width,
|
double? width,
|
||||||
|
EdgeInsets? insetPadding,
|
||||||
|
bool? showCloseIcon,
|
||||||
|
Color? closeIconColor,
|
||||||
}) {
|
}) {
|
||||||
return SnackBarThemeData(
|
return SnackBarThemeData(
|
||||||
backgroundColor: backgroundColor ?? this.backgroundColor,
|
backgroundColor: backgroundColor ?? this.backgroundColor,
|
||||||
|
@ -136,6 +157,9 @@ class SnackBarThemeData with Diagnosticable {
|
||||||
shape: shape ?? this.shape,
|
shape: shape ?? this.shape,
|
||||||
behavior: behavior ?? this.behavior,
|
behavior: behavior ?? this.behavior,
|
||||||
width: width ?? this.width,
|
width: width ?? this.width,
|
||||||
|
insetPadding: insetPadding ?? this.insetPadding,
|
||||||
|
showCloseIcon: showCloseIcon ?? this.showCloseIcon,
|
||||||
|
closeIconColor: closeIconColor ?? this.closeIconColor,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -155,6 +179,8 @@ class SnackBarThemeData with Diagnosticable {
|
||||||
shape: ShapeBorder.lerp(a?.shape, b?.shape, t),
|
shape: ShapeBorder.lerp(a?.shape, b?.shape, t),
|
||||||
behavior: t < 0.5 ? a?.behavior : b?.behavior,
|
behavior: t < 0.5 ? a?.behavior : b?.behavior,
|
||||||
width: lerpDouble(a?.width, b?.width, t),
|
width: lerpDouble(a?.width, b?.width, t),
|
||||||
|
insetPadding: EdgeInsets.lerp(a?.insetPadding, b?.insetPadding, t),
|
||||||
|
closeIconColor: Color.lerp(a?.closeIconColor, b?.closeIconColor, t),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -168,6 +194,9 @@ class SnackBarThemeData with Diagnosticable {
|
||||||
shape,
|
shape,
|
||||||
behavior,
|
behavior,
|
||||||
width,
|
width,
|
||||||
|
insetPadding,
|
||||||
|
showCloseIcon,
|
||||||
|
closeIconColor,
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -186,7 +215,10 @@ class SnackBarThemeData with Diagnosticable {
|
||||||
&& other.elevation == elevation
|
&& other.elevation == elevation
|
||||||
&& other.shape == shape
|
&& other.shape == shape
|
||||||
&& other.behavior == behavior
|
&& other.behavior == behavior
|
||||||
&& other.width == width;
|
&& other.width == width
|
||||||
|
&& other.insetPadding == insetPadding
|
||||||
|
&& other.showCloseIcon == showCloseIcon
|
||||||
|
&& other.closeIconColor == closeIconColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -200,5 +232,8 @@ class SnackBarThemeData with Diagnosticable {
|
||||||
properties.add(DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: null));
|
properties.add(DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: null));
|
||||||
properties.add(DiagnosticsProperty<SnackBarBehavior>('behavior', behavior, defaultValue: null));
|
properties.add(DiagnosticsProperty<SnackBarBehavior>('behavior', behavior, defaultValue: null));
|
||||||
properties.add(DoubleProperty('width', width, defaultValue: null));
|
properties.add(DoubleProperty('width', width, defaultValue: null));
|
||||||
|
properties.add(DiagnosticsProperty<EdgeInsets>('insetPadding', insetPadding, defaultValue: null));
|
||||||
|
properties.add(DiagnosticsProperty<bool>('showCloseIcon', showCloseIcon, defaultValue: null));
|
||||||
|
properties.add(ColorProperty('closeIconColor', closeIconColor, defaultValue: null));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1321,6 +1321,7 @@ class ThemeData with Diagnosticable {
|
||||||
/// * [Navigation rail](https://m3.material.io/components/navigation-rail): [NavigationRail]
|
/// * [Navigation rail](https://m3.material.io/components/navigation-rail): [NavigationRail]
|
||||||
/// * Progress indicators: [CircularProgressIndicator], [LinearProgressIndicator]
|
/// * Progress indicators: [CircularProgressIndicator], [LinearProgressIndicator]
|
||||||
/// * Radio button: [Radio]
|
/// * Radio button: [Radio]
|
||||||
|
/// * Snack bar: [SnackBar]
|
||||||
/// * Switch: [Switch]
|
/// * Switch: [Switch]
|
||||||
/// * Top app bar: [AppBar]
|
/// * Top app bar: [AppBar]
|
||||||
///
|
///
|
||||||
|
|
|
@ -316,6 +316,7 @@ void main() {
|
||||||
' TextButtonTheme\n'
|
' TextButtonTheme\n'
|
||||||
' Padding\n'
|
' Padding\n'
|
||||||
' Row\n'
|
' Row\n'
|
||||||
|
' Column\n'
|
||||||
' Padding\n'
|
' Padding\n'
|
||||||
' MediaQuery\n'
|
' MediaQuery\n'
|
||||||
' Padding\n'
|
' Padding\n'
|
||||||
|
|
|
@ -2340,7 +2340,146 @@ void main() {
|
||||||
await expectLater(find.byType(MaterialApp), matchesGoldenFile('snack_bar.goldenTest.backdropFilter.png'));
|
await expectLater(find.byType(MaterialApp), matchesGoldenFile('snack_bar.goldenTest.backdropFilter.png'));
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('ScaffoldMessenger will alert for snackbars that cannot be presented', (WidgetTester tester) async {
|
testWidgets('Floating snackbar can display optional icon', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(const MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
bottomSheet: SizedBox(
|
||||||
|
width: 200,
|
||||||
|
height: 50,
|
||||||
|
child: ColoredBox(
|
||||||
|
color: Colors.pink,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
final ScaffoldMessengerState scaffoldMessengerState = tester.state(find.byType(ScaffoldMessenger));
|
||||||
|
scaffoldMessengerState.showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: const Text('Feeling snackish'),
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
action: SnackBarAction(label: 'ACTION', onPressed: () {}),
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
showCloseIcon: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle(); // Have the SnackBar fully animate out.
|
||||||
|
|
||||||
|
await expectLater(
|
||||||
|
find.byType(MaterialApp),
|
||||||
|
matchesGoldenFile(
|
||||||
|
'snack_bar.goldenTest.floatingWithActionWithIcon.png'));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Fixed width snackbar can display optional icon', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(const MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
bottomSheet: SizedBox(
|
||||||
|
width: 200,
|
||||||
|
height: 50,
|
||||||
|
child: ColoredBox(
|
||||||
|
color: Colors.pink,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
final ScaffoldMessengerState scaffoldMessengerState = tester.state(find.byType(ScaffoldMessenger));
|
||||||
|
scaffoldMessengerState.showSnackBar(SnackBar(
|
||||||
|
content: const Text('Go get a snack'),
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
action: SnackBarAction(label: 'ACTION', onPressed: () {}),
|
||||||
|
showCloseIcon: true,
|
||||||
|
behavior: SnackBarBehavior.fixed,
|
||||||
|
));
|
||||||
|
await tester.pumpAndSettle(); // Have the SnackBar fully animate out.
|
||||||
|
|
||||||
|
await expectLater(find.byType(MaterialApp), matchesGoldenFile('snack_bar.goldenTest.fixedWithActionWithIcon.png'));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Fixed snackbar can display optional icon without action', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(const MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
bottomSheet: SizedBox(
|
||||||
|
width: 200,
|
||||||
|
height: 50,
|
||||||
|
child: ColoredBox(
|
||||||
|
color: Colors.pink,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
final ScaffoldMessengerState scaffoldMessengerState = tester.state(find.byType(ScaffoldMessenger));
|
||||||
|
scaffoldMessengerState.showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('I wonder if there are snacks nearby?'),
|
||||||
|
duration: Duration(seconds: 2),
|
||||||
|
behavior: SnackBarBehavior.fixed,
|
||||||
|
showCloseIcon: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle(); // Have the SnackBar fully animate out.
|
||||||
|
|
||||||
|
await expectLater(find.byType(MaterialApp), matchesGoldenFile('snack_bar.goldenTest.fixedWithIcon.png'));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'Floating width snackbar can display optional icon without action', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(const MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
bottomSheet: SizedBox(
|
||||||
|
width: 200,
|
||||||
|
height: 50,
|
||||||
|
child: ColoredBox(
|
||||||
|
color: Colors.pink,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
final ScaffoldMessengerState scaffoldMessengerState = tester.state(find.byType(ScaffoldMessenger));
|
||||||
|
scaffoldMessengerState.showSnackBar(const SnackBar(
|
||||||
|
content: Text('Must go get a snack!'),
|
||||||
|
duration: Duration(seconds: 2),
|
||||||
|
showCloseIcon: true,
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
));
|
||||||
|
await tester.pumpAndSettle(); // Have the SnackBar fully animate out.
|
||||||
|
|
||||||
|
await expectLater(find.byType(MaterialApp),
|
||||||
|
matchesGoldenFile('snack_bar.goldenTest.floatingWithIcon.png'));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Fixed multi-line snackbar with icon is aligned correctly', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(const MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
bottomSheet: SizedBox(
|
||||||
|
width: 200,
|
||||||
|
height: 50,
|
||||||
|
child: ColoredBox(
|
||||||
|
color: Colors.pink,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
final ScaffoldMessengerState scaffoldMessengerState = tester.state(find.byType(ScaffoldMessenger));
|
||||||
|
scaffoldMessengerState.showSnackBar(const SnackBar(
|
||||||
|
content: Text(
|
||||||
|
'This is a really long snackbar message. So long, it spans across more than one line!'),
|
||||||
|
duration: Duration(seconds: 2),
|
||||||
|
showCloseIcon: true,
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
));
|
||||||
|
await tester.pumpAndSettle(); // Have the SnackBar fully animate out.
|
||||||
|
|
||||||
|
await expectLater(find.byType(MaterialApp),
|
||||||
|
matchesGoldenFile('snack_bar.goldenTest.multiLineWithIcon.png'));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'ScaffoldMessenger will alert for snackbars that cannot be presented', (WidgetTester tester) async {
|
||||||
// Regression test for https://github.com/flutter/flutter/issues/103004
|
// Regression test for https://github.com/flutter/flutter/issues/103004
|
||||||
await tester.pumpWidget(const MaterialApp(
|
await tester.pumpWidget(const MaterialApp(
|
||||||
home: Center(),
|
home: Center(),
|
||||||
|
|
|
@ -22,6 +22,9 @@ void main() {
|
||||||
expect(snackBarTheme.shape, null);
|
expect(snackBarTheme.shape, null);
|
||||||
expect(snackBarTheme.behavior, null);
|
expect(snackBarTheme.behavior, null);
|
||||||
expect(snackBarTheme.width, null);
|
expect(snackBarTheme.width, null);
|
||||||
|
expect(snackBarTheme.insetPadding, null);
|
||||||
|
expect(snackBarTheme.showCloseIcon, null);
|
||||||
|
expect(snackBarTheme.closeIconColor, null);
|
||||||
});
|
});
|
||||||
|
|
||||||
test(
|
test(
|
||||||
|
@ -59,6 +62,9 @@ void main() {
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(2.0))),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(2.0))),
|
||||||
behavior: SnackBarBehavior.floating,
|
behavior: SnackBarBehavior.floating,
|
||||||
width: 400.0,
|
width: 400.0,
|
||||||
|
insetPadding: EdgeInsets.all(10.0),
|
||||||
|
showCloseIcon: false,
|
||||||
|
closeIconColor: Color(0xFF0000AA),
|
||||||
).debugFillProperties(builder);
|
).debugFillProperties(builder);
|
||||||
|
|
||||||
final List<String> description = builder.properties
|
final List<String> description = builder.properties
|
||||||
|
@ -75,6 +81,9 @@ void main() {
|
||||||
'shape: RoundedRectangleBorder(BorderSide(width: 0.0, style: none), BorderRadius.circular(2.0))',
|
'shape: RoundedRectangleBorder(BorderSide(width: 0.0, style: none), BorderRadius.circular(2.0))',
|
||||||
'behavior: SnackBarBehavior.floating',
|
'behavior: SnackBarBehavior.floating',
|
||||||
'width: 400.0',
|
'width: 400.0',
|
||||||
|
'insetPadding: EdgeInsets.all(10.0)',
|
||||||
|
'showCloseIcon: false',
|
||||||
|
'closeIconColor: Color(0xff0000aa)',
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -115,7 +124,7 @@ void main() {
|
||||||
testWidgets('SnackBar uses values from SnackBarThemeData', (WidgetTester tester) async {
|
testWidgets('SnackBar uses values from SnackBarThemeData', (WidgetTester tester) async {
|
||||||
const String text = 'I am a snack bar.';
|
const String text = 'I am a snack bar.';
|
||||||
const String action = 'ACTION';
|
const String action = 'ACTION';
|
||||||
final SnackBarThemeData snackBarTheme = _snackBarTheme();
|
final SnackBarThemeData snackBarTheme = _snackBarTheme(showCloseIcon: true);
|
||||||
|
|
||||||
await tester.pumpWidget(MaterialApp(
|
await tester.pumpWidget(MaterialApp(
|
||||||
theme: ThemeData(snackBarTheme: snackBarTheme),
|
theme: ThemeData(snackBarTheme: snackBarTheme),
|
||||||
|
@ -144,12 +153,14 @@ void main() {
|
||||||
final Material material = _getSnackBarMaterial(tester);
|
final Material material = _getSnackBarMaterial(tester);
|
||||||
final RenderParagraph button = _getSnackBarActionTextRenderObject(tester, action);
|
final RenderParagraph button = _getSnackBarActionTextRenderObject(tester, action);
|
||||||
final RenderParagraph content = _getSnackBarTextRenderObject(tester, text);
|
final RenderParagraph content = _getSnackBarTextRenderObject(tester, text);
|
||||||
|
final Icon icon = _getSnackBarIcon(tester);
|
||||||
|
|
||||||
expect(content.text.style, snackBarTheme.contentTextStyle);
|
expect(content.text.style, snackBarTheme.contentTextStyle);
|
||||||
expect(material.color, snackBarTheme.backgroundColor);
|
expect(material.color, snackBarTheme.backgroundColor);
|
||||||
expect(material.elevation, snackBarTheme.elevation);
|
expect(material.elevation, snackBarTheme.elevation);
|
||||||
expect(material.shape, snackBarTheme.shape);
|
expect(material.shape, snackBarTheme.shape);
|
||||||
expect(button.text.style!.color, snackBarTheme.actionTextColor);
|
expect(button.text.style!.color, snackBarTheme.actionTextColor);
|
||||||
|
expect(icon.icon, Icons.close);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('SnackBar widget properties take priority over theme', (WidgetTester tester) async {
|
testWidgets('SnackBar widget properties take priority over theme', (WidgetTester tester) async {
|
||||||
|
@ -163,7 +174,7 @@ void main() {
|
||||||
const double snackBarWidth = 400.0;
|
const double snackBarWidth = 400.0;
|
||||||
|
|
||||||
await tester.pumpWidget(MaterialApp(
|
await tester.pumpWidget(MaterialApp(
|
||||||
theme: ThemeData(snackBarTheme: _snackBarTheme()),
|
theme: ThemeData(snackBarTheme: _snackBarTheme(showCloseIcon: true)),
|
||||||
home: Scaffold(
|
home: Scaffold(
|
||||||
body: Builder(
|
body: Builder(
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
|
@ -182,6 +193,7 @@ void main() {
|
||||||
label: action,
|
label: action,
|
||||||
onPressed: () {},
|
onPressed: () {},
|
||||||
),
|
),
|
||||||
|
showCloseIcon: false,
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
child: const Text('X'),
|
child: const Text('X'),
|
||||||
|
@ -204,6 +216,7 @@ void main() {
|
||||||
expect(material.elevation, elevation);
|
expect(material.elevation, elevation);
|
||||||
expect(material.shape, shape);
|
expect(material.shape, shape);
|
||||||
expect(button.text.style!.color, textColor);
|
expect(button.text.style!.color, textColor);
|
||||||
|
expect(_getSnackBarIconFinder(tester), findsNothing);
|
||||||
// Assert width.
|
// Assert width.
|
||||||
final Offset snackBarBottomLeft = tester.getBottomLeft(materialFinder.first);
|
final Offset snackBarBottomLeft = tester.getBottomLeft(materialFinder.first);
|
||||||
final Offset snackBarBottomRight = tester.getBottomRight(materialFinder.first);
|
final Offset snackBarBottomRight = tester.getBottomRight(materialFinder.first);
|
||||||
|
@ -214,8 +227,7 @@ void main() {
|
||||||
testWidgets('SnackBar theme behavior is correct for floating', (WidgetTester tester) async {
|
testWidgets('SnackBar theme behavior is correct for floating', (WidgetTester tester) async {
|
||||||
await tester.pumpWidget(MaterialApp(
|
await tester.pumpWidget(MaterialApp(
|
||||||
theme: ThemeData(
|
theme: ThemeData(
|
||||||
snackBarTheme: const SnackBarThemeData(behavior: SnackBarBehavior.floating),
|
snackBarTheme: const SnackBarThemeData(behavior: SnackBarBehavior.floating)),
|
||||||
),
|
|
||||||
home: Scaffold(
|
home: Scaffold(
|
||||||
floatingActionButton: FloatingActionButton(
|
floatingActionButton: FloatingActionButton(
|
||||||
child: const Icon(Icons.send),
|
child: const Icon(Icons.send),
|
||||||
|
@ -389,13 +401,14 @@ void main() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
SnackBarThemeData _snackBarTheme() {
|
SnackBarThemeData _snackBarTheme({bool? showCloseIcon}) {
|
||||||
return const SnackBarThemeData(
|
return SnackBarThemeData(
|
||||||
backgroundColor: Colors.orange,
|
backgroundColor: Colors.orange,
|
||||||
actionTextColor: Colors.green,
|
actionTextColor: Colors.green,
|
||||||
contentTextStyle: TextStyle(color: Colors.blue),
|
contentTextStyle: const TextStyle(color: Colors.blue),
|
||||||
elevation: 12.0,
|
elevation: 12.0,
|
||||||
shape: BeveledRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12))),
|
showCloseIcon: showCloseIcon,
|
||||||
|
shape: const BeveledRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -409,7 +422,6 @@ Finder _getSnackBarMaterialFinder(WidgetTester tester) {
|
||||||
return find.descendant(
|
return find.descendant(
|
||||||
of: find.byType(SnackBar),
|
of: find.byType(SnackBar),
|
||||||
matching: find.byType(Material),
|
matching: find.byType(Material),
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -420,6 +432,17 @@ RenderParagraph _getSnackBarActionTextRenderObject(WidgetTester tester, String t
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Icon _getSnackBarIcon(WidgetTester tester) {
|
||||||
|
return tester.widget<Icon>(_getSnackBarIconFinder(tester));
|
||||||
|
}
|
||||||
|
|
||||||
|
Finder _getSnackBarIconFinder(WidgetTester tester) {
|
||||||
|
return find.descendant(
|
||||||
|
of: find.byType(SnackBar),
|
||||||
|
matching: find.byIcon(Icons.close),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
RenderParagraph _getSnackBarTextRenderObject(WidgetTester tester, String text) {
|
RenderParagraph _getSnackBarTextRenderObject(WidgetTester tester, String text) {
|
||||||
return tester.renderObject(find.descendant(
|
return tester.renderObject(find.descendant(
|
||||||
of: find.byType(SnackBar),
|
of: find.byType(SnackBar),
|
||||||
|
|
Loading…
Reference in a new issue