mirror of
https://github.com/flutter/flutter
synced 2024-10-04 07:19:46 +00:00
M3 snackbar [re-land] (#116218)
* 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 * g3fix * g3fix * debug test fix
This commit is contained in:
parent
24b3c384c2
commit
29422d25fe
|
@ -44,6 +44,7 @@ import 'package:gen_defaults/progress_indicator_template.dart';
|
|||
import 'package:gen_defaults/radio_template.dart';
|
||||
import 'package:gen_defaults/segmented_button_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/switch_template.dart';
|
||||
import 'package:gen_defaults/tabs_template.dart';
|
||||
|
@ -162,6 +163,7 @@ Future<void> main(List<String> args) async {
|
|||
ProgressIndicatorTemplate('ProgressIndicator', '$materialLib/progress_indicator.dart', tokens).updateFile();
|
||||
RadioTemplate('Radio<T>', '$materialLib/radio.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();
|
||||
SurfaceTintTemplate('SurfaceTint', '$materialLib/elevation_overlay.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 'color_scheme.dart';
|
||||
import 'icon_button.dart';
|
||||
import 'icons.dart';
|
||||
import 'material.dart';
|
||||
import 'material_state.dart';
|
||||
import 'scaffold.dart';
|
||||
|
@ -19,17 +21,13 @@ import 'theme.dart';
|
|||
// late BuildContext context;
|
||||
|
||||
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 _snackBarDisplayDuration = Duration(milliseconds: 4000);
|
||||
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);
|
||||
|
||||
/// Specify how a [SnackBar] was closed.
|
||||
|
@ -97,6 +95,11 @@ class SnackBarAction extends StatefulWidget {
|
|||
|
||||
/// The button label color. If not provided, defaults to
|
||||
/// [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;
|
||||
|
||||
/// The button disabled label color. This color is shown after the
|
||||
|
@ -132,17 +135,36 @@ class _SnackBarActionState extends State<SnackBarAction> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Color? resolveForegroundColor(Set<MaterialState> states) {
|
||||
final SnackBarThemeData snackBarTheme = Theme.of(context).snackBarTheme;
|
||||
if (states.contains(MaterialState.disabled)) {
|
||||
return widget.disabledTextColor ?? snackBarTheme.disabledActionTextColor;
|
||||
final SnackBarThemeData defaults = Theme.of(context).useMaterial3
|
||||
? _SnackbarDefaultsM3(context)
|
||||
: _SnackbarDefaultsM2(context);
|
||||
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(
|
||||
style: ButtonStyle(
|
||||
foregroundColor: MaterialStateProperty.resolveWith<Color?>(resolveForegroundColor),
|
||||
foregroundColor: resolveForegroundColor(),
|
||||
),
|
||||
onPressed: _haveTriggeredAction ? null : _handlePressed,
|
||||
child: Text(widget.label),
|
||||
|
@ -213,6 +235,8 @@ class SnackBar extends StatefulWidget {
|
|||
this.shape,
|
||||
this.behavior,
|
||||
this.action,
|
||||
this.showCloseIcon,
|
||||
this.closeIconColor,
|
||||
this.duration = _snackBarDisplayDuration,
|
||||
this.animation,
|
||||
this.onVisible,
|
||||
|
@ -255,7 +279,8 @@ class SnackBar extends StatefulWidget {
|
|||
/// 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
|
||||
/// 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)`.
|
||||
final EdgeInsetsGeometry? margin;
|
||||
|
||||
|
@ -319,6 +344,9 @@ class SnackBar extends StatefulWidget {
|
|||
/// If this property is null, then [SnackBarThemeData.behavior] of
|
||||
/// [ThemeData.snackBarTheme] is used. If that is null, then the default is
|
||||
/// [SnackBarBehavior.fixed].
|
||||
///
|
||||
/// If this value is [SnackBarBehavior.floating], the length of the bar
|
||||
/// is defined by either [width] or [margin].
|
||||
final SnackBarBehavior? behavior;
|
||||
|
||||
/// (optional) An action that the user can take based on the snack bar.
|
||||
|
@ -329,6 +357,24 @@ class SnackBar extends StatefulWidget {
|
|||
/// The action should not be "dismiss" or "cancel".
|
||||
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.
|
||||
///
|
||||
/// Defaults to 4.0s.
|
||||
|
@ -384,6 +430,8 @@ class SnackBar extends StatefulWidget {
|
|||
shape: shape,
|
||||
behavior: behavior,
|
||||
action: action,
|
||||
showCloseIcon: showCloseIcon,
|
||||
closeIconColor: closeIconColor,
|
||||
duration: duration,
|
||||
animation: newAnimation,
|
||||
onVisible: onVisible,
|
||||
|
@ -443,34 +491,38 @@ class _SnackBarState extends State<SnackBar> {
|
|||
final ColorScheme colorScheme = theme.colorScheme;
|
||||
final SnackBarThemeData snackBarTheme = theme.snackBarTheme;
|
||||
final bool isThemeDark = theme.brightness == Brightness.dark;
|
||||
final Color buttonColor = isThemeDark ? colorScheme.primary : colorScheme.secondary;
|
||||
final Color buttonColor = isThemeDark ? colorScheme.primary : colorScheme.secondary;
|
||||
final SnackBarThemeData defaults = theme.useMaterial3
|
||||
? _SnackbarDefaultsM3(context)
|
||||
: _SnackbarDefaultsM2(context);
|
||||
|
||||
// SnackBar uses a theme that is the opposite brightness from
|
||||
// the surrounding theme.
|
||||
final Brightness brightness = isThemeDark ? Brightness.light : Brightness.dark;
|
||||
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;
|
||||
final SnackBarBehavior snackBarBehavior = widget.behavior ?? snackBarTheme.behavior ?? SnackBarBehavior.fixed;
|
||||
// Invert the theme values for Material 2. Material 3 values are tokenzied to pre-inverted values.
|
||||
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;
|
||||
assert((){
|
||||
// Whether the behavior is set through the constructor or the theme,
|
||||
|
@ -492,48 +544,119 @@ class _SnackBarState extends State<SnackBar> {
|
|||
return true;
|
||||
}());
|
||||
|
||||
final bool showCloseIcon = widget.showCloseIcon ?? snackBarTheme.showCloseIcon ?? defaults.showCloseIcon!;
|
||||
|
||||
final bool isFloatingSnackBar = snackBarBehavior == SnackBarBehavior.floating;
|
||||
final double horizontalPadding = isFloatingSnackBar ? 16.0 : 24.0;
|
||||
final EdgeInsetsGeometry padding = widget.padding
|
||||
?? EdgeInsetsDirectional.only(start: horizontalPadding, end: widget.action != null ? 0 : horizontalPadding);
|
||||
final EdgeInsetsGeometry padding = widget.padding ??
|
||||
EdgeInsetsDirectional.only(
|
||||
start: horizontalPadding,
|
||||
end: widget.action != null || showCloseIcon
|
||||
? 0
|
||||
: horizontalPadding);
|
||||
|
||||
final double actionHorizontalMargin = (widget.padding?.resolve(TextDirection.ltr).right ?? horizontalPadding) / 2;
|
||||
final double iconHorizontalMargin = (widget.padding?.resolve(TextDirection.ltr).right ?? horizontalPadding) / 12.0;
|
||||
|
||||
final CurvedAnimation heightAnimation = CurvedAnimation(parent: widget.animation!, curve: _snackBarHeightCurve);
|
||||
final CurvedAnimation fadeInAnimation = CurvedAnimation(parent: widget.animation!, curve: _snackBarFadeInCurve);
|
||||
final CurvedAnimation fadeInM3Animation = CurvedAnimation(parent: widget.animation!, curve: _snackBarM3FadeInCurve);
|
||||
|
||||
final CurvedAnimation fadeOutAnimation = CurvedAnimation(
|
||||
parent: widget.animation!,
|
||||
curve: _snackBarFadeOutCurve,
|
||||
reverseCurve: const Threshold(0.0),
|
||||
);
|
||||
// Material 3 Animation has a height animation on entry, but a direct fade out on exit.
|
||||
final CurvedAnimation heightM3Animation = CurvedAnimation(
|
||||
parent: widget.animation!,
|
||||
curve: _snackBarM3HeightCurve,
|
||||
reverseCurve: const Threshold(0.0),
|
||||
);
|
||||
|
||||
|
||||
final IconButton? iconButton = showCloseIcon
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
iconSize: 24.0,
|
||||
color: widget.closeIconColor ?? snackBarTheme.closeIconColor ?? defaults.closeIconColor,
|
||||
onPressed: () => ScaffoldMessenger.of(context).hideCurrentSnackBar(reason: SnackBarClosedReason.dismiss),
|
||||
)
|
||||
: null;
|
||||
|
||||
// Calculate combined width of Action, Icon, and their padding, if they are present.
|
||||
final TextPainter actionTextPainter = TextPainter(
|
||||
text: TextSpan(
|
||||
text: widget.action?.label ?? '',
|
||||
style: Theme.of(context).textTheme.labelLarge,
|
||||
),
|
||||
maxLines: 1,
|
||||
textDirection: TextDirection.ltr)
|
||||
..layout();
|
||||
final double actionAndIconWidth = actionTextPainter.size.width +
|
||||
(widget.action != null ? actionHorizontalMargin : 0) +
|
||||
(showCloseIcon ? (iconButton?.iconSize ?? 0 + iconHorizontalMargin) : 0);
|
||||
|
||||
final EdgeInsets margin = widget.margin?.resolve(TextDirection.ltr) ?? snackBarTheme.insetPadding ?? defaults.insetPadding!;
|
||||
|
||||
final double snackBarWidth = widget.width ?? 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(
|
||||
padding: padding,
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: widget.padding == null ? const EdgeInsets.symmetric(vertical: _singleLineVerticalPadding) : null,
|
||||
child: DefaultTextStyle(
|
||||
style: contentTextStyle!,
|
||||
child: widget.content,
|
||||
),
|
||||
),
|
||||
),
|
||||
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: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
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 +667,17 @@ class _SnackBarState extends State<SnackBar> {
|
|||
);
|
||||
}
|
||||
|
||||
final double elevation = widget.elevation ?? snackBarTheme.elevation ?? 6.0;
|
||||
final Color backgroundColor = widget.backgroundColor ?? snackBarTheme.backgroundColor ?? inverseTheme.colorScheme.background;
|
||||
final ShapeBorder? shape = widget.shape
|
||||
?? snackBarTheme.shape
|
||||
?? (isFloatingSnackBar ? const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0))) : null);
|
||||
final double elevation = widget.elevation ?? snackBarTheme.elevation ?? defaults.elevation!;
|
||||
final Color backgroundColor = widget.backgroundColor ?? snackBarTheme.backgroundColor ?? defaults.backgroundColor!;
|
||||
final ShapeBorder? shape = widget.shape ?? snackBarTheme.shape ?? (isFloatingSnackBar ? defaults.shape : null);
|
||||
|
||||
snackBar = Material(
|
||||
shape: shape,
|
||||
elevation: elevation,
|
||||
color: backgroundColor,
|
||||
child: Theme(
|
||||
data: inverseTheme,
|
||||
child: mediaQueryData.accessibleNavigation
|
||||
data: effectiveTheme,
|
||||
child: mediaQueryData.accessibleNavigation || theme.useMaterial3
|
||||
? snackBar
|
||||
: FadeTransition(
|
||||
opacity: fadeOutAnimation,
|
||||
|
@ -566,24 +687,16 @@ class _SnackBarState extends State<SnackBar> {
|
|||
);
|
||||
|
||||
if (isFloatingSnackBar) {
|
||||
const double topMargin = 5.0;
|
||||
const double bottomMargin = 10.0;
|
||||
// If width is provided, do not include horizontal margins.
|
||||
if (width != null) {
|
||||
snackBar = Container(
|
||||
margin: const EdgeInsets.only(top: topMargin, bottom: bottomMargin),
|
||||
margin: EdgeInsets.only(top: margin.top, bottom: margin.bottom),
|
||||
width: width,
|
||||
child: snackBar,
|
||||
);
|
||||
} else {
|
||||
const double horizontalMargin = 15.0;
|
||||
snackBar = Padding(
|
||||
padding: widget.margin ?? const EdgeInsets.fromLTRB(
|
||||
horizontalMargin,
|
||||
topMargin,
|
||||
horizontalMargin,
|
||||
bottomMargin,
|
||||
),
|
||||
padding: margin,
|
||||
child: snackBar,
|
||||
);
|
||||
}
|
||||
|
@ -614,11 +727,27 @@ class _SnackBarState extends State<SnackBar> {
|
|||
final Widget snackBarTransition;
|
||||
if (mediaQueryData.accessibleNavigation) {
|
||||
snackBarTransition = snackBar;
|
||||
} else if (isFloatingSnackBar) {
|
||||
} else if (isFloatingSnackBar && !theme.useMaterial3) {
|
||||
snackBarTransition = FadeTransition(
|
||||
opacity: fadeInAnimation,
|
||||
child: snackBar,
|
||||
);
|
||||
// Is Material 3 Floating Snack Bar.
|
||||
} else if (isFloatingSnackBar && theme.useMaterial3) {
|
||||
snackBarTransition = FadeTransition(
|
||||
opacity: fadeInM3Animation,
|
||||
child: AnimatedBuilder(
|
||||
animation: heightM3Animation,
|
||||
builder: (BuildContext context, Widget? child) {
|
||||
return Align(
|
||||
alignment: AlignmentDirectional.bottomStart,
|
||||
heightFactor: heightM3Animation.value,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: snackBar,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
snackBarTransition = AnimatedBuilder(
|
||||
animation: heightAnimation,
|
||||
|
@ -643,3 +772,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.behavior,
|
||||
this.width,
|
||||
this.insetPadding,
|
||||
this.showCloseIcon,
|
||||
this.closeIconColor,
|
||||
}) : assert(elevation == null || elevation >= 0.0),
|
||||
assert(
|
||||
width == null ||
|
||||
|
@ -115,6 +118,21 @@ class SnackBarThemeData with Diagnosticable {
|
|||
/// [SnackBarBehavior.floating].
|
||||
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
|
||||
/// new values.
|
||||
SnackBarThemeData copyWith({
|
||||
|
@ -126,6 +144,9 @@ class SnackBarThemeData with Diagnosticable {
|
|||
ShapeBorder? shape,
|
||||
SnackBarBehavior? behavior,
|
||||
double? width,
|
||||
EdgeInsets? insetPadding,
|
||||
bool? showCloseIcon,
|
||||
Color? closeIconColor,
|
||||
}) {
|
||||
return SnackBarThemeData(
|
||||
backgroundColor: backgroundColor ?? this.backgroundColor,
|
||||
|
@ -136,6 +157,9 @@ class SnackBarThemeData with Diagnosticable {
|
|||
shape: shape ?? this.shape,
|
||||
behavior: behavior ?? this.behavior,
|
||||
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),
|
||||
behavior: t < 0.5 ? a?.behavior : b?.behavior,
|
||||
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,
|
||||
behavior,
|
||||
width,
|
||||
insetPadding,
|
||||
showCloseIcon,
|
||||
closeIconColor,
|
||||
);
|
||||
|
||||
@override
|
||||
|
@ -186,7 +215,10 @@ class SnackBarThemeData with Diagnosticable {
|
|||
&& other.elevation == elevation
|
||||
&& other.shape == shape
|
||||
&& other.behavior == behavior
|
||||
&& other.width == width;
|
||||
&& other.width == width
|
||||
&& other.insetPadding == insetPadding
|
||||
&& other.showCloseIcon == showCloseIcon
|
||||
&& other.closeIconColor == closeIconColor;
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -200,5 +232,8 @@ class SnackBarThemeData with Diagnosticable {
|
|||
properties.add(DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: null));
|
||||
properties.add(DiagnosticsProperty<SnackBarBehavior>('behavior', behavior, 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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1339,6 +1339,7 @@ class ThemeData with Diagnosticable {
|
|||
/// * Navigation rail: [NavigationRail]
|
||||
/// * Progress indicators: [CircularProgressIndicator], [LinearProgressIndicator]
|
||||
/// * Radio button: [Radio]
|
||||
/// * Snack bar: [SnackBar]
|
||||
/// * Slider: [Slider]
|
||||
/// * Switch: [Switch]
|
||||
/// * Tabs: [TabBar]
|
||||
|
|
|
@ -316,6 +316,24 @@ void main() {
|
|||
' TextButtonTheme\n'
|
||||
' Padding\n'
|
||||
' Row\n'
|
||||
' Column\n'
|
||||
' _SingleChildViewport\n'
|
||||
' IgnorePointer-[GlobalKey#d48e8]\n'
|
||||
' Semantics\n'
|
||||
' Listener\n'
|
||||
' _GestureSemantics\n'
|
||||
' RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#0c3e0]\n'
|
||||
' Listener\n'
|
||||
' _ScrollableScope\n'
|
||||
' _ScrollSemantics-[GlobalKey#349b8]\n'
|
||||
' NotificationListener<ScrollMetricsNotification>\n'
|
||||
' RepaintBoundary\n'
|
||||
' CustomPaint\n'
|
||||
' RepaintBoundary\n'
|
||||
' NotificationListener<ScrollNotification>\n'
|
||||
' GlowingOverscrollIndicator\n'
|
||||
' Scrollable\n'
|
||||
' SingleChildScrollView\n'
|
||||
' Padding\n'
|
||||
' MediaQuery\n'
|
||||
' Padding\n'
|
||||
|
|
|
@ -1276,10 +1276,6 @@ void main() {
|
|||
await tester.pumpAndSettle();
|
||||
|
||||
expect(tester.getSemantics(find.text('snack')), matchesSemantics(
|
||||
isLiveRegion: true,
|
||||
hasDismissAction: true,
|
||||
hasScrollDownAction: true,
|
||||
hasScrollUpAction: true,
|
||||
label: 'snack',
|
||||
textDirection: TextDirection.ltr,
|
||||
));
|
||||
|
@ -2340,7 +2336,146 @@ void main() {
|
|||
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
|
||||
await tester.pumpWidget(const MaterialApp(
|
||||
home: Center(),
|
||||
|
|
|
@ -22,6 +22,9 @@ void main() {
|
|||
expect(snackBarTheme.shape, null);
|
||||
expect(snackBarTheme.behavior, null);
|
||||
expect(snackBarTheme.width, null);
|
||||
expect(snackBarTheme.insetPadding, null);
|
||||
expect(snackBarTheme.showCloseIcon, null);
|
||||
expect(snackBarTheme.closeIconColor, null);
|
||||
});
|
||||
|
||||
test(
|
||||
|
@ -59,6 +62,9 @@ void main() {
|
|||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(2.0))),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
width: 400.0,
|
||||
insetPadding: EdgeInsets.all(10.0),
|
||||
showCloseIcon: false,
|
||||
closeIconColor: Color(0xFF0000AA),
|
||||
).debugFillProperties(builder);
|
||||
|
||||
final List<String> description = builder.properties
|
||||
|
@ -75,6 +81,9 @@ void main() {
|
|||
'shape: RoundedRectangleBorder(BorderSide(width: 0.0, style: none), BorderRadius.circular(2.0))',
|
||||
'behavior: SnackBarBehavior.floating',
|
||||
'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 {
|
||||
const String text = 'I am a snack bar.';
|
||||
const String action = 'ACTION';
|
||||
final SnackBarThemeData snackBarTheme = _snackBarTheme();
|
||||
final SnackBarThemeData snackBarTheme = _snackBarTheme(showCloseIcon: true);
|
||||
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
theme: ThemeData(snackBarTheme: snackBarTheme),
|
||||
|
@ -144,12 +153,14 @@ void main() {
|
|||
final Material material = _getSnackBarMaterial(tester);
|
||||
final RenderParagraph button = _getSnackBarActionTextRenderObject(tester, action);
|
||||
final RenderParagraph content = _getSnackBarTextRenderObject(tester, text);
|
||||
final Icon icon = _getSnackBarIcon(tester);
|
||||
|
||||
expect(content.text.style, snackBarTheme.contentTextStyle);
|
||||
expect(material.color, snackBarTheme.backgroundColor);
|
||||
expect(material.elevation, snackBarTheme.elevation);
|
||||
expect(material.shape, snackBarTheme.shape);
|
||||
expect(button.text.style!.color, snackBarTheme.actionTextColor);
|
||||
expect(icon.icon, Icons.close);
|
||||
});
|
||||
|
||||
testWidgets('SnackBar widget properties take priority over theme', (WidgetTester tester) async {
|
||||
|
@ -163,7 +174,7 @@ void main() {
|
|||
const double snackBarWidth = 400.0;
|
||||
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
theme: ThemeData(snackBarTheme: _snackBarTheme()),
|
||||
theme: ThemeData(snackBarTheme: _snackBarTheme(showCloseIcon: true)),
|
||||
home: Scaffold(
|
||||
body: Builder(
|
||||
builder: (BuildContext context) {
|
||||
|
@ -182,6 +193,7 @@ void main() {
|
|||
label: action,
|
||||
onPressed: () {},
|
||||
),
|
||||
showCloseIcon: false,
|
||||
));
|
||||
},
|
||||
child: const Text('X'),
|
||||
|
@ -204,6 +216,7 @@ void main() {
|
|||
expect(material.elevation, elevation);
|
||||
expect(material.shape, shape);
|
||||
expect(button.text.style!.color, textColor);
|
||||
expect(_getSnackBarIconFinder(tester), findsNothing);
|
||||
// Assert width.
|
||||
final Offset snackBarBottomLeft = tester.getBottomLeft(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 {
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
theme: ThemeData(
|
||||
snackBarTheme: const SnackBarThemeData(behavior: SnackBarBehavior.floating),
|
||||
),
|
||||
snackBarTheme: const SnackBarThemeData(behavior: SnackBarBehavior.floating)),
|
||||
home: Scaffold(
|
||||
floatingActionButton: FloatingActionButton(
|
||||
child: const Icon(Icons.send),
|
||||
|
@ -389,13 +401,14 @@ void main() {
|
|||
});
|
||||
}
|
||||
|
||||
SnackBarThemeData _snackBarTheme() {
|
||||
return const SnackBarThemeData(
|
||||
SnackBarThemeData _snackBarTheme({bool? showCloseIcon}) {
|
||||
return SnackBarThemeData(
|
||||
backgroundColor: Colors.orange,
|
||||
actionTextColor: Colors.green,
|
||||
contentTextStyle: TextStyle(color: Colors.blue),
|
||||
contentTextStyle: const TextStyle(color: Colors.blue),
|
||||
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(
|
||||
of: find.byType(SnackBar),
|
||||
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) {
|
||||
return tester.renderObject(find.descendant(
|
||||
of: find.byType(SnackBar),
|
||||
|
|
Loading…
Reference in a new issue