Unverified Commit efe0c5eb authored by Aayan's avatar Aayan Committed by GitHub

Integrate MaterialBanner with the ScaffoldMessenger API (#81706)

parent 734df6f5
...@@ -6,8 +6,44 @@ import 'package:flutter/widgets.dart'; ...@@ -6,8 +6,44 @@ import 'package:flutter/widgets.dart';
import 'banner_theme.dart'; import 'banner_theme.dart';
import 'divider.dart'; import 'divider.dart';
import 'scaffold.dart';
import 'theme.dart'; import 'theme.dart';
const Duration _materialBannerTransitionDuration = Duration(milliseconds: 250);
const Curve _materialBannerHeightCurve = Curves.fastOutSlowIn;
const Curve _materialBannerFadeOutCurve = Interval(0.72, 1.0, curve: Curves.fastOutSlowIn);
/// Specify how a [MaterialBanner] was closed.
///
/// The [ScaffoldMessengerState.showMaterialBanner] function returns a
/// [ScaffoldFeatureController]. The value of the controller's closed property
/// is a Future that resolves to a MaterialBannerClosedReason. Applications that need
/// to know how a material banner was closed can use this value.
///
/// Example:
///
/// ```dart
/// ScaffoldMessenger.of(context).showMaterialBanner(
/// MaterialBanner( ... )
/// ).closed.then((MaterialBannerClosedReason reason) {
/// ...
/// });
/// ```
enum MaterialBannerClosedReason {
/// The material banner was closed through a [SemanticsAction.dismiss].
dismiss,
/// The material banner was closed by a user's swipe.
swipe,
/// The material banner was closed by the [ScaffoldFeatureController] close callback
/// or by calling [ScaffoldMessengerState.hideCurrentMaterialBanner] directly.
hide,
/// The material banner was closed by a call to [ScaffoldMessengerState.removeCurrentMaterialBanner].
remove,
}
/// A Material Design banner. /// A Material Design banner.
/// ///
/// A banner displays an important, succinct message, and provides actions for /// A banner displays an important, succinct message, and provides actions for
...@@ -19,6 +55,9 @@ import 'theme.dart'; ...@@ -19,6 +55,9 @@ import 'theme.dart';
/// interact with them at any time. /// interact with them at any time.
/// ///
/// {@tool dartpad --template=stateless_widget_material} /// {@tool dartpad --template=stateless_widget_material}
///
/// Banners placed directly into the widget tree are static.
///
/// ```dart /// ```dart
/// Widget build(BuildContext context) { /// Widget build(BuildContext context) {
/// return Scaffold( /// return Scaffold(
...@@ -46,6 +85,41 @@ import 'theme.dart'; ...@@ -46,6 +85,41 @@ import 'theme.dart';
/// ``` /// ```
/// {@end-tool} /// {@end-tool}
/// ///
/// {@tool dartpad --template=stateless_widget_material}
///
/// MaterialBanner's can also be presented through a [ScaffoldMessenger].
/// Here is an example where ScaffoldMessengerState.showMaterialBanner() is used to show the MaterialBanner.
///
/// ```dart
/// Widget build(BuildContext context) {
/// return Scaffold(
/// appBar: AppBar(
/// title: const Text('The MaterialBanner is below'),
/// ),
/// body: Center(
/// child: ElevatedButton(
/// child: const Text('Show MaterialBanner'),
/// onPressed: () => ScaffoldMessenger.of(context).showMaterialBanner(
/// const MaterialBanner(
/// padding: EdgeInsets.all(20),
/// content: Text('Hello, I am a Material Banner'),
/// leading: Icon(Icons.agriculture_outlined),
/// backgroundColor: Colors.green,
/// actions: <Widget>[
/// TextButton(
/// child: Text('DISMISS'),
/// onPressed: null,
/// ),
/// ],
/// ),
/// ),
/// ),
/// ),
/// );
/// }
/// ```
/// {@end-tool}
///
/// The [actions] will be placed beside the [content] if there is only one. /// The [actions] will be placed beside the [content] if there is only one.
/// Otherwise, the [actions] will be placed below the [content]. Use /// Otherwise, the [actions] will be placed below the [content]. Use
/// [forceActionsBelow] to override this behavior. /// [forceActionsBelow] to override this behavior.
...@@ -59,7 +133,7 @@ import 'theme.dart'; ...@@ -59,7 +133,7 @@ import 'theme.dart';
/// [backgroundColor] can be provided to customize the banner. /// [backgroundColor] can be provided to customize the banner.
/// ///
/// This widget is unrelated to the widgets library [Banner] widget. /// This widget is unrelated to the widgets library [Banner] widget.
class MaterialBanner extends StatelessWidget { class MaterialBanner extends StatefulWidget {
/// Creates a [MaterialBanner]. /// Creates a [MaterialBanner].
/// ///
/// The [actions], [content], and [forceActionsBelow] must be non-null. /// The [actions], [content], and [forceActionsBelow] must be non-null.
...@@ -75,6 +149,8 @@ class MaterialBanner extends StatelessWidget { ...@@ -75,6 +149,8 @@ class MaterialBanner extends StatelessWidget {
this.leadingPadding, this.leadingPadding,
this.forceActionsBelow = false, this.forceActionsBelow = false,
this.overflowAlignment = OverflowBarAlignment.end, this.overflowAlignment = OverflowBarAlignment.end,
this.animation,
this.onVisible
}) : assert(content != null), }) : assert(content != null),
assert(actions != null), assert(actions != null),
assert(forceActionsBelow != null), assert(forceActionsBelow != null),
...@@ -138,18 +214,101 @@ class MaterialBanner extends StatelessWidget { ...@@ -138,18 +214,101 @@ class MaterialBanner extends StatelessWidget {
/// Defaults to [OverflowBarAlignment.end]. /// Defaults to [OverflowBarAlignment.end].
final OverflowBarAlignment overflowAlignment; final OverflowBarAlignment overflowAlignment;
/// The animation driving the entrance and exit of the material banner when presented by the [ScaffoldMessenger].
final Animation<double>? animation;
/// Called the first time that the material banner is visible within a [Scaffold] when presented by the [ScaffoldMessenger].
final VoidCallback? onVisible;
// API for ScaffoldMessengerState.showMaterialBanner():
/// Creates an animation controller useful for driving a material banner's entrance and exit animation.
static AnimationController createAnimationController({ required TickerProvider vsync }) {
return AnimationController(
duration: _materialBannerTransitionDuration,
debugLabel: 'MaterialBanner',
vsync: vsync,
);
}
/// Creates a copy of this material banner but with the animation replaced with the given animation.
///
/// If the original material banner lacks a key, the newly created material banner will
/// use the given fallback key.
MaterialBanner withAnimation(Animation<double> newAnimation, { Key? fallbackKey }) {
return MaterialBanner(
key: key ?? fallbackKey,
content: content,
contentTextStyle: contentTextStyle,
actions: actions,
leading: leading,
backgroundColor: backgroundColor,
padding: padding,
leadingPadding: leadingPadding,
forceActionsBelow: forceActionsBelow,
overflowAlignment: overflowAlignment,
animation: newAnimation,
onVisible: onVisible,
);
}
@override
State<MaterialBanner> createState() => _MaterialBannerState();
}
class _MaterialBannerState extends State<MaterialBanner> {
bool _wasVisible = false;
@override
void initState() {
super.initState();
widget.animation?.addStatusListener(_onAnimationStatusChanged);
}
@override
void didUpdateWidget(MaterialBanner oldWidget) {
if (widget.animation != oldWidget.animation) {
oldWidget.animation?.removeStatusListener(_onAnimationStatusChanged);
widget.animation?.addStatusListener(_onAnimationStatusChanged);
}
super.didUpdateWidget(oldWidget);
}
@override
void dispose() {
widget.animation?.removeStatusListener(_onAnimationStatusChanged);
super.dispose();
}
void _onAnimationStatusChanged(AnimationStatus animationStatus) {
switch (animationStatus) {
case AnimationStatus.dismissed:
case AnimationStatus.forward:
case AnimationStatus.reverse:
break;
case AnimationStatus.completed:
if (widget.onVisible != null && !_wasVisible) {
widget.onVisible!();
}
_wasVisible = true;
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(actions.isNotEmpty); assert(debugCheckHasMediaQuery(context));
final MediaQueryData mediaQueryData = MediaQuery.of(context);
assert(widget.actions.isNotEmpty);
final ThemeData theme = Theme.of(context); final ThemeData theme = Theme.of(context);
final MaterialBannerThemeData bannerTheme = MaterialBannerTheme.of(context); final MaterialBannerThemeData bannerTheme = MaterialBannerTheme.of(context);
final bool isSingleRow = actions.length == 1 && !forceActionsBelow; final bool isSingleRow = widget.actions.length == 1 && !widget.forceActionsBelow;
final EdgeInsetsGeometry padding = this.padding ?? bannerTheme.padding ?? (isSingleRow final EdgeInsetsGeometry padding = widget.padding ?? bannerTheme.padding ?? (isSingleRow
? const EdgeInsetsDirectional.only(start: 16.0, top: 2.0) ? const EdgeInsetsDirectional.only(start: 16.0, top: 2.0)
: const EdgeInsetsDirectional.only(start: 16.0, top: 24.0, end: 16.0, bottom: 4.0)); : const EdgeInsetsDirectional.only(start: 16.0, top: 24.0, end: 16.0, bottom: 4.0));
final EdgeInsetsGeometry leadingPadding = this.leadingPadding final EdgeInsetsGeometry leadingPadding = widget.leadingPadding
?? bannerTheme.leadingPadding ?? bannerTheme.leadingPadding
?? const EdgeInsetsDirectional.only(end: 16.0); ?? const EdgeInsetsDirectional.only(end: 16.0);
...@@ -158,36 +317,37 @@ class MaterialBanner extends StatelessWidget { ...@@ -158,36 +317,37 @@ class MaterialBanner extends StatelessWidget {
constraints: const BoxConstraints(minHeight: 52.0), constraints: const BoxConstraints(minHeight: 52.0),
padding: const EdgeInsets.symmetric(horizontal: 8), padding: const EdgeInsets.symmetric(horizontal: 8),
child: OverflowBar( child: OverflowBar(
overflowAlignment: overflowAlignment, overflowAlignment: widget.overflowAlignment,
spacing: 8, spacing: 8,
children: actions, children: widget.actions,
), ),
); );
final Color backgroundColor = this.backgroundColor final Color backgroundColor = widget.backgroundColor
?? bannerTheme.backgroundColor ?? bannerTheme.backgroundColor
?? theme.colorScheme.surface; ?? theme.colorScheme.surface;
final TextStyle? textStyle = contentTextStyle final TextStyle? textStyle = widget.contentTextStyle
?? bannerTheme.contentTextStyle ?? bannerTheme.contentTextStyle
?? theme.textTheme.bodyText2; ?? theme.textTheme.bodyText2;
return Container( Widget materialBanner = Container(
color: backgroundColor, color: backgroundColor,
child: Column( child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
Padding( Padding(
padding: padding, padding: padding,
child: Row( child: Row(
children: <Widget>[ children: <Widget>[
if (leading != null) if (widget.leading != null)
Padding( Padding(
padding: leadingPadding, padding: leadingPadding,
child: leading, child: widget.leading,
), ),
Expanded( Expanded(
child: DefaultTextStyle( child: DefaultTextStyle(
style: textStyle!, style: textStyle!,
child: content, child: widget.content,
), ),
), ),
if (isSingleRow) if (isSingleRow)
...@@ -201,5 +361,52 @@ class MaterialBanner extends StatelessWidget { ...@@ -201,5 +361,52 @@ class MaterialBanner extends StatelessWidget {
], ],
), ),
); );
// This provides a static banner for backwards compatibility.
if (widget.animation == null)
return materialBanner;
final CurvedAnimation heightAnimation = CurvedAnimation(parent: widget.animation!, curve: _materialBannerHeightCurve);
final CurvedAnimation fadeOutAnimation = CurvedAnimation(
parent: widget.animation!,
curve: _materialBannerFadeOutCurve,
reverseCurve: const Threshold(0.0),
);
materialBanner = Semantics(
container: true,
liveRegion: true,
onDismiss: () {
ScaffoldMessenger.of(context).removeCurrentMaterialBanner(reason: MaterialBannerClosedReason.dismiss);
},
child: mediaQueryData.accessibleNavigation
? materialBanner
: FadeTransition(
opacity: fadeOutAnimation,
child: materialBanner,
),
);
final Widget materialBannerTransition;
if (mediaQueryData.accessibleNavigation) {
materialBannerTransition = materialBanner;
} else {
materialBannerTransition = AnimatedBuilder(
animation: heightAnimation,
builder: (BuildContext context, Widget? child) {
return Align(
alignment: AlignmentDirectional.bottomStart,
heightFactor: heightAnimation.value,
child: child,
);
},
child: materialBanner,
);
}
return Hero(
child: ClipRect(child: materialBannerTransition),
tag: '<MaterialBanner Hero tag - ${widget.content}>',
);
} }
} }
...@@ -12,6 +12,7 @@ import 'package:flutter/gestures.dart' show DragStartBehavior; ...@@ -12,6 +12,7 @@ import 'package:flutter/gestures.dart' show DragStartBehavior;
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'app_bar.dart'; import 'app_bar.dart';
import 'banner.dart';
import 'bottom_sheet.dart'; import 'bottom_sheet.dart';
import 'colors.dart'; import 'colors.dart';
import 'curves.dart'; import 'curves.dart';
...@@ -49,6 +50,7 @@ enum _ScaffoldSlot { ...@@ -49,6 +50,7 @@ enum _ScaffoldSlot {
bodyScrim, bodyScrim,
bottomSheet, bottomSheet,
snackBar, snackBar,
materialBanner,
persistentFooter, persistentFooter,
bottomNavigationBar, bottomNavigationBar,
floatingActionButton, floatingActionButton,
...@@ -256,6 +258,8 @@ class ScaffoldMessenger extends StatefulWidget { ...@@ -256,6 +258,8 @@ class ScaffoldMessenger extends StatefulWidget {
/// Typically obtained via [ScaffoldMessenger.of]. /// Typically obtained via [ScaffoldMessenger.of].
class ScaffoldMessengerState extends State<ScaffoldMessenger> with TickerProviderStateMixin { class ScaffoldMessengerState extends State<ScaffoldMessenger> with TickerProviderStateMixin {
final LinkedHashSet<ScaffoldState> _scaffolds = LinkedHashSet<ScaffoldState>(); final LinkedHashSet<ScaffoldState> _scaffolds = LinkedHashSet<ScaffoldState>();
final Queue<ScaffoldFeatureController<MaterialBanner, MaterialBannerClosedReason>> _materialBanners = Queue<ScaffoldFeatureController<MaterialBanner, MaterialBannerClosedReason>>();
AnimationController? _materialBannerController;
final Queue<ScaffoldFeatureController<SnackBar, SnackBarClosedReason>> _snackBars = Queue<ScaffoldFeatureController<SnackBar, SnackBarClosedReason>>(); final Queue<ScaffoldFeatureController<SnackBar, SnackBarClosedReason>> _snackBars = Queue<ScaffoldFeatureController<SnackBar, SnackBarClosedReason>>();
AnimationController? _snackBarController; AnimationController? _snackBarController;
Timer? _snackBarTimer; Timer? _snackBarTimer;
...@@ -280,9 +284,16 @@ class ScaffoldMessengerState extends State<ScaffoldMessenger> with TickerProvide ...@@ -280,9 +284,16 @@ class ScaffoldMessengerState extends State<ScaffoldMessenger> with TickerProvide
void _register(ScaffoldState scaffold) { void _register(ScaffoldState scaffold) {
_scaffolds.add(scaffold); _scaffolds.add(scaffold);
if (_snackBars.isNotEmpty && _isRoot(scaffold)) {
if (_isRoot(scaffold)) {
if (_snackBars.isNotEmpty) {
scaffold._updateSnackBar(); scaffold._updateSnackBar();
} }
if (_materialBanners.isNotEmpty) {
scaffold._updateMaterialBanner();
}
}
} }
void _unregister(ScaffoldState scaffold) { void _unregister(ScaffoldState scaffold) {
...@@ -291,6 +302,24 @@ class ScaffoldMessengerState extends State<ScaffoldMessenger> with TickerProvide ...@@ -291,6 +302,24 @@ class ScaffoldMessengerState extends State<ScaffoldMessenger> with TickerProvide
assert(removed); assert(removed);
} }
void _updateScaffolds() {
for (final ScaffoldState scaffold in _scaffolds) {
if (_isRoot(scaffold)) {
scaffold._updateSnackBar();
scaffold._updateMaterialBanner();
}
}
}
// Nested Scaffolds are handled by the ScaffoldMessenger by only presenting a
// MaterialBanner or SnackBar in the root Scaffold of the nested set.
bool _isRoot(ScaffoldState scaffold) {
final ScaffoldState? parent = scaffold.context.findAncestorStateOfType<ScaffoldState>();
return parent == null || !_scaffolds.contains(parent);
}
// SNACKBAR API
/// Shows a [SnackBar] across all registered [Scaffold]s. /// Shows a [SnackBar] across all registered [Scaffold]s.
/// ///
/// A scaffold can show at most one snack bar at a time. If this function is /// A scaffold can show at most one snack bar at a time. If this function is
...@@ -329,7 +358,7 @@ class ScaffoldMessengerState extends State<ScaffoldMessenger> with TickerProvide ...@@ -329,7 +358,7 @@ class ScaffoldMessengerState extends State<ScaffoldMessenger> with TickerProvide
/// {@end-tool} /// {@end-tool}
ScaffoldFeatureController<SnackBar, SnackBarClosedReason> showSnackBar(SnackBar snackBar) { ScaffoldFeatureController<SnackBar, SnackBarClosedReason> showSnackBar(SnackBar snackBar) {
_snackBarController ??= SnackBar.createAnimationController(vsync: this) _snackBarController ??= SnackBar.createAnimationController(vsync: this)
..addStatusListener(_handleStatusChanged); ..addStatusListener(_handleSnackBarStatusChanged);
if (_snackBars.isEmpty) { if (_snackBars.isEmpty) {
assert(_snackBarController!.isDismissed); assert(_snackBarController!.isDismissed);
_snackBarController!.forward(); _snackBarController!.forward();
...@@ -354,7 +383,7 @@ class ScaffoldMessengerState extends State<ScaffoldMessenger> with TickerProvide ...@@ -354,7 +383,7 @@ class ScaffoldMessengerState extends State<ScaffoldMessenger> with TickerProvide
return controller; return controller;
} }
void _handleStatusChanged(AnimationStatus status) { void _handleSnackBarStatusChanged(AnimationStatus status) {
switch (status) { switch (status) {
case AnimationStatus.dismissed: case AnimationStatus.dismissed:
assert(_snackBars.isNotEmpty); assert(_snackBars.isNotEmpty);
...@@ -380,20 +409,6 @@ class ScaffoldMessengerState extends State<ScaffoldMessenger> with TickerProvide ...@@ -380,20 +409,6 @@ class ScaffoldMessengerState extends State<ScaffoldMessenger> with TickerProvide
} }
} }
void _updateScaffolds() {
for (final ScaffoldState scaffold in _scaffolds) {
if (_isRoot(scaffold))
scaffold._updateSnackBar();
}
}
// Nested Scaffolds are handled by the ScaffoldMessenger by only presenting a
// SnackBar in the root Scaffold of the nested set.
bool _isRoot(ScaffoldState scaffold) {
final ScaffoldState? parent = scaffold.context.findAncestorStateOfType<ScaffoldState>();
return parent == null || !_scaffolds.contains(parent);
}
/// Removes the current [SnackBar] (if any) immediately from registered /// Removes the current [SnackBar] (if any) immediately from registered
/// [Scaffold]s. /// [Scaffold]s.
/// ///
...@@ -445,6 +460,145 @@ class ScaffoldMessengerState extends State<ScaffoldMessenger> with TickerProvide ...@@ -445,6 +460,145 @@ class ScaffoldMessengerState extends State<ScaffoldMessenger> with TickerProvide
hideCurrentSnackBar(); hideCurrentSnackBar();
} }
// MATERIAL BANNER API
/// Shows a [MaterialBanner] across all registered [Scaffold]s.
///
/// A scaffold can show at most one material banner at a time. If this function is
/// called while another material banner is already visible, the given material banner
/// will be added to a queue and displayed after the earlier material banners have
/// closed.
///
/// To remove the [MaterialBanner] with an exit animation, use [hideCurrentMaterialBanner]
/// or call [ScaffoldFeatureController.close] on the returned
/// [ScaffoldFeatureController]. To remove a [MaterialBanner] suddenly (without an
/// animation), use [removeCurrentMaterialBanner].
///
/// See [ScaffoldMessenger.of] for information about how to obtain the
/// [ScaffoldMessengerState].
///
/// {@tool dartpad --template=stateless_widget_scaffold_center}
///
/// Here is an example of showing a [MaterialBanner] when the user presses a button.
///
/// ```dart
/// Widget build(BuildContext context) {
/// return OutlinedButton(
/// onPressed: () {
/// ScaffoldMessenger.of(context).showMaterialBanner(
/// const MaterialBanner(
/// content: Text('This is a MaterialBanner'),
/// actions: <Widget>[
/// TextButton(
/// child: Text('DISMISS'),
/// onPressed: null,
/// ),
/// ],
/// ),
/// );
/// },
/// child: const Text('Show MaterialBanner'),
/// );
/// }
/// ```
/// {@end-tool}
ScaffoldFeatureController<MaterialBanner, MaterialBannerClosedReason> showMaterialBanner(MaterialBanner materialBanner) {
_materialBannerController ??= MaterialBanner.createAnimationController(vsync: this)
..addStatusListener(_handleMaterialBannerStatusChanged);
if (_materialBanners.isEmpty) {
assert(_materialBannerController!.isDismissed);
_materialBannerController!.forward();
}
late ScaffoldFeatureController<MaterialBanner, MaterialBannerClosedReason> controller;
controller = ScaffoldFeatureController<MaterialBanner, MaterialBannerClosedReason>._(
// We provide a fallback key so that if back-to-back material banners happen to
// match in structure, material ink splashes and highlights don't survive
// from one to the next.
materialBanner.withAnimation(_materialBannerController!, fallbackKey: UniqueKey()),
Completer<MaterialBannerClosedReason>(),
() {
assert(_materialBanners.first == controller);
hideCurrentMaterialBanner(reason: MaterialBannerClosedReason.hide);
},
null, // MaterialBanner doesn't use a builder function so setState() wouldn't rebuild it
);
setState(() {
_materialBanners.addLast(controller);
});
_updateScaffolds();
return controller;
}
void _handleMaterialBannerStatusChanged(AnimationStatus status) {
switch (status) {
case AnimationStatus.dismissed:
assert(_materialBanners.isNotEmpty);
setState(() {
_materialBanners.removeFirst();
});
_updateScaffolds();
if (_materialBanners.isNotEmpty) {
_materialBannerController!.forward();
}
break;
case AnimationStatus.completed:
_updateScaffolds();
break;
case AnimationStatus.forward:
break;
case AnimationStatus.reverse:
break;
}
}
/// Removes the current [MaterialBanner] (if any) immediately from registered
/// [Scaffold]s.
///
/// The removed material banner does not run its normal exit animation. If there are
/// any queued material banners, they begin their entrance animation immediately.
void removeCurrentMaterialBanner({ MaterialBannerClosedReason reason = MaterialBannerClosedReason.remove }) {
assert(reason != null);
if (_materialBanners.isEmpty)
return;
final Completer<MaterialBannerClosedReason> completer = _materialBanners.first._completer;
if (!completer.isCompleted)
completer.complete(reason);
// This will trigger the animation's status callback.
_materialBannerController!.value = 0.0;
}
/// Removes the current [MaterialBanner] by running its normal exit animation.
///
/// The closed completer is called after the animation is complete.
void hideCurrentMaterialBanner({ MaterialBannerClosedReason reason = MaterialBannerClosedReason.hide }) {
assert(reason != null);
if (_materialBanners.isEmpty || _materialBannerController!.status == AnimationStatus.dismissed)
return;
final Completer<MaterialBannerClosedReason> completer = _materialBanners.first._completer;
if (_accessibleNavigation!) {
_materialBannerController!.value = 0.0;
completer.complete(reason);
} else {
_materialBannerController!.reverse().then<void>((void value) {
assert(mounted);
if (!completer.isCompleted)
completer.complete(reason);
});
}
}
/// Removes all the materialBanners currently in queue by clearing the queue
/// and running normal exit animation on the current materialBanner.
void clearMaterialBanners() {
if (_materialBanners.isEmpty || _materialBannerController!.status == AnimationStatus.dismissed)
return;
final ScaffoldFeatureController<MaterialBanner, MaterialBannerClosedReason> currentMaterialBanner = _materialBanners.first;
_materialBanners.clear();
_materialBanners.add(currentMaterialBanner);
hideCurrentMaterialBanner();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(debugCheckHasMediaQuery(context)); assert(debugCheckHasMediaQuery(context));
...@@ -522,6 +676,7 @@ class ScaffoldPrelayoutGeometry { ...@@ -522,6 +676,7 @@ class ScaffoldPrelayoutGeometry {
required this.minViewPadding, required this.minViewPadding,
required this.scaffoldSize, required this.scaffoldSize,
required this.snackBarSize, required this.snackBarSize,
required this.materialBannerSize,
required this.textDirection, required this.textDirection,
}); });
...@@ -605,6 +760,11 @@ class ScaffoldPrelayoutGeometry { ...@@ -605,6 +760,11 @@ class ScaffoldPrelayoutGeometry {
/// If the [Scaffold] is not showing a [SnackBar], this will be [Size.zero]. /// If the [Scaffold] is not showing a [SnackBar], this will be [Size.zero].
final Size snackBarSize; final Size snackBarSize;
/// The [Size] of the [Scaffold]'s [MaterialBanner].
///
/// If the [Scaffold] is not showing a [MaterialBanner], this will be [Size.zero].
final Size materialBannerSize;
/// The [TextDirection] of the [Scaffold]'s [BuildContext]. /// The [TextDirection] of the [Scaffold]'s [BuildContext].
final TextDirection textDirection; final TextDirection textDirection;
} }
...@@ -967,6 +1127,11 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate { ...@@ -967,6 +1127,11 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
snackBarSize = layoutChild(_ScaffoldSlot.snackBar, fullWidthConstraints); snackBarSize = layoutChild(_ScaffoldSlot.snackBar, fullWidthConstraints);
} }
Size materialBannerSize = Size.zero;
if (hasChild(_ScaffoldSlot.materialBanner)) {
materialBannerSize = layoutChild(_ScaffoldSlot.materialBanner, fullWidthConstraints);
}
if (hasChild(_ScaffoldSlot.bottomSheet)) { if (hasChild(_ScaffoldSlot.bottomSheet)) {
final BoxConstraints bottomSheetConstraints = BoxConstraints( final BoxConstraints bottomSheetConstraints = BoxConstraints(
maxWidth: fullWidthConstraints.maxWidth, maxWidth: fullWidthConstraints.maxWidth,
...@@ -992,6 +1157,7 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate { ...@@ -992,6 +1157,7 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
minInsets: minInsets, minInsets: minInsets,
scaffoldSize: size, scaffoldSize: size,
snackBarSize: snackBarSize, snackBarSize: snackBarSize,
materialBannerSize: materialBannerSize,
textDirection: textDirection, textDirection: textDirection,
minViewPadding: minViewPadding, minViewPadding: minViewPadding,
); );
...@@ -1034,6 +1200,17 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate { ...@@ -1034,6 +1200,17 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
positionChild(_ScaffoldSlot.snackBar, Offset(xOffset, snackBarYOffsetBase - snackBarSize.height)); positionChild(_ScaffoldSlot.snackBar, Offset(xOffset, snackBarYOffsetBase - snackBarSize.height));
} }
if (hasChild(_ScaffoldSlot.materialBanner)) {
if (materialBannerSize == Size.zero) {
materialBannerSize = layoutChild(
_ScaffoldSlot.materialBanner,
fullWidthConstraints,
);
}
positionChild(_ScaffoldSlot.materialBanner, Offset(0.0, appBarHeight));
}
if (hasChild(_ScaffoldSlot.statusBar)) { if (hasChild(_ScaffoldSlot.statusBar)) {
layoutChild(_ScaffoldSlot.statusBar, fullWidthConstraints.tighten(height: minInsets.top)); layoutChild(_ScaffoldSlot.statusBar, fullWidthConstraints.tighten(height: minInsets.top));
positionChild(_ScaffoldSlot.statusBar, Offset.zero); positionChild(_ScaffoldSlot.statusBar, Offset.zero);
...@@ -2194,12 +2371,14 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin, Resto ...@@ -2194,12 +2371,14 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin, Resto
_endDrawerKey.currentState?.open(); _endDrawerKey.currentState?.open();
} }
// Used for both the snackbar and material banner APIs
ScaffoldMessengerState? _scaffoldMessenger;
bool? _accessibleNavigation;
// SNACKBAR API // SNACKBAR API
final Queue<ScaffoldFeatureController<SnackBar, SnackBarClosedReason>> _snackBars = Queue<ScaffoldFeatureController<SnackBar, SnackBarClosedReason>>(); final Queue<ScaffoldFeatureController<SnackBar, SnackBarClosedReason>> _snackBars = Queue<ScaffoldFeatureController<SnackBar, SnackBarClosedReason>>();
AnimationController? _snackBarController; AnimationController? _snackBarController;
Timer? _snackBarTimer; Timer? _snackBarTimer;
bool? _accessibleNavigation;
ScaffoldMessengerState? _scaffoldMessenger;
/// [ScaffoldMessengerState.showSnackBar] shows a [SnackBar] at the bottom of /// [ScaffoldMessengerState.showSnackBar] shows a [SnackBar] at the bottom of
/// the scaffold. This method should not be used, and will be deprecated in /// the scaffold. This method should not be used, and will be deprecated in
...@@ -2401,12 +2580,35 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin, Resto ...@@ -2401,12 +2580,35 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin, Resto
// This is used to update the _messengerSnackBar by the ScaffoldMessenger. // This is used to update the _messengerSnackBar by the ScaffoldMessenger.
void _updateSnackBar() { void _updateSnackBar() {
setState(() { final ScaffoldFeatureController<SnackBar, SnackBarClosedReason>? messengerSnackBar = _scaffoldMessenger!._snackBars.isNotEmpty
_messengerSnackBar = _scaffoldMessenger!._snackBars.isNotEmpty
? _scaffoldMessenger!._snackBars.first ? _scaffoldMessenger!._snackBars.first
: null; : null;
if (_messengerSnackBar != messengerSnackBar) {
setState(() {
_messengerSnackBar = messengerSnackBar;
}); });
} }
}
// MATERIAL BANNER API
// The _messengerMaterialBanner represents the current MaterialBanner being managed by
// the ScaffoldMessenger, instead of the Scaffold.
ScaffoldFeatureController<MaterialBanner, MaterialBannerClosedReason>? _messengerMaterialBanner;
// This is used to update the _messengerMaterialBanner by the ScaffoldMessenger.
void _updateMaterialBanner() {
final ScaffoldFeatureController<MaterialBanner, MaterialBannerClosedReason>? messengerMaterialBanner = _scaffoldMessenger!._materialBanners.isNotEmpty
? _scaffoldMessenger!._materialBanners.first
: null;
if (_messengerMaterialBanner != messengerMaterialBanner) {
setState(() {
_messengerMaterialBanner = messengerMaterialBanner;
});
}
}
// PERSISTENT BOTTOM SHEET API // PERSISTENT BOTTOM SHEET API
...@@ -3136,6 +3338,20 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin, Resto ...@@ -3136,6 +3338,20 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin, Resto
); );
} }
// MaterialBanner set by ScaffoldMessenger
if (_messengerMaterialBanner != null) {
_addIfNonNull(
children,
_messengerMaterialBanner?._widget,
_ScaffoldSlot.materialBanner,
removeLeftPadding: false,
removeTopPadding: widget.appBar != null,
removeRightPadding: false,
removeBottomPadding: true,
maintainBottomViewPadding: !_resizeToAvoidBottomInset,
);
}
if (widget.persistentFooterButtons != null) { if (widget.persistentFooterButtons != null) {
_addIfNonNull( _addIfNonNull(
children, children,
...@@ -3288,7 +3504,7 @@ class ScaffoldFeatureController<T extends Widget, U> { ...@@ -3288,7 +3504,7 @@ class ScaffoldFeatureController<T extends Widget, U> {
/// Completes when the feature controlled by this object is no longer visible. /// Completes when the feature controlled by this object is no longer visible.
Future<U> get closed => _completer.future; Future<U> get closed => _completer.future;
/// Remove the feature (e.g., bottom sheet or snack bar) from the scaffold. /// Remove the feature (e.g., bottom sheet, snack bar, or material banner) from the scaffold.
final VoidCallback close; final VoidCallback close;
/// Mark the feature (e.g., bottom sheet or snack bar) as needing to rebuild. /// Mark the feature (e.g., bottom sheet or snack bar) as needing to rebuild.
......
...@@ -28,6 +28,44 @@ void main() { ...@@ -28,6 +28,44 @@ void main() {
expect(container.color, color); expect(container.color, color);
}); });
testWidgets('Custom background color respected when presented by ScaffoldMessenger', (WidgetTester tester) async {
const Color color = Colors.pink;
const String contentText = 'Content';
const Key tapTarget = Key('tap-target');
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: Builder(
builder: (BuildContext context) {
return GestureDetector(
key: tapTarget,
onTap: () {
ScaffoldMessenger.of(context).showMaterialBanner(MaterialBanner(
content: const Text(contentText),
backgroundColor: color,
actions: <Widget>[
TextButton(
child: const Text('DISMISS'),
onPressed: () => ScaffoldMessenger.of(context).hideCurrentMaterialBanner(),
),
],
));
},
behavior: HitTestBehavior.opaque,
child: const SizedBox(
height: 100.0,
width: 100.0,
),
);
},
),
),
));
await tester.tap(find.byKey(tapTarget));
await tester.pumpAndSettle();
expect(_getContainerFromText(tester, contentText).color, color);
});
testWidgets('Custom content TextStyle respected', (WidgetTester tester) async { testWidgets('Custom content TextStyle respected', (WidgetTester tester) async {
const String contentText = 'Content'; const String contentText = 'Content';
const TextStyle contentTextStyle = TextStyle(color: Colors.pink); const TextStyle contentTextStyle = TextStyle(color: Colors.pink);
...@@ -50,6 +88,45 @@ void main() { ...@@ -50,6 +88,45 @@ void main() {
expect(content.text.style, contentTextStyle); expect(content.text.style, contentTextStyle);
}); });
testWidgets('Custom content TextStyle respected when presented by ScaffoldMessenger', (WidgetTester tester) async {
const TextStyle contentTextStyle = TextStyle(color: Colors.pink);
const String contentText = 'Content';
const Key tapTarget = Key('tap-target');
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: Builder(
builder: (BuildContext context) {
return GestureDetector(
key: tapTarget,
onTap: () {
ScaffoldMessenger.of(context).showMaterialBanner(MaterialBanner(
content: const Text(contentText),
contentTextStyle: contentTextStyle,
actions: <Widget>[
TextButton(
child: const Text('DISMISS'),
onPressed: () => ScaffoldMessenger.of(context).hideCurrentMaterialBanner(),
),
],
));
},
behavior: HitTestBehavior.opaque,
child: const SizedBox(
height: 100.0,
width: 100.0,
),
);
},
),
),
));
await tester.tap(find.byKey(tapTarget));
await tester.pumpAndSettle();
final RenderParagraph content = _getTextRenderObjectFromDialog(tester, contentText);
expect(content.text.style, contentTextStyle);
});
testWidgets('Actions laid out below content if more than one action', (WidgetTester tester) async { testWidgets('Actions laid out below content if more than one action', (WidgetTester tester) async {
const String contentText = 'Content'; const String contentText = 'Content';
...@@ -77,6 +154,49 @@ void main() { ...@@ -77,6 +154,49 @@ void main() {
expect(contentBottomLeft.dx, lessThan(actionsTopLeft.dx)); expect(contentBottomLeft.dx, lessThan(actionsTopLeft.dx));
}); });
testWidgets('Actions laid out below content if more than one action when presented by ScaffoldMessenger', (WidgetTester tester) async {
const String contentText = 'Content';
const Key tapTarget = Key('tap-target');
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: Builder(
builder: (BuildContext context) {
return GestureDetector(
key: tapTarget,
onTap: () {
ScaffoldMessenger.of(context).showMaterialBanner(MaterialBanner(
content: const Text(contentText),
actions: <Widget>[
TextButton(
child: const Text('OK'),
onPressed: () => ScaffoldMessenger.of(context).hideCurrentMaterialBanner(),
),
TextButton(
child: const Text('DISMISS'),
onPressed: () => ScaffoldMessenger.of(context).hideCurrentMaterialBanner(),
),
],
));
},
behavior: HitTestBehavior.opaque,
child: const SizedBox(
height: 100.0,
width: 100.0,
),
);
},
),
),
));
await tester.tap(find.byKey(tapTarget));
await tester.pumpAndSettle();
final Offset contentBottomLeft = tester.getBottomLeft(find.text(contentText));
final Offset actionsTopLeft = tester.getTopLeft(find.byType(OverflowBar));
expect(contentBottomLeft.dy, lessThan(actionsTopLeft.dy));
expect(contentBottomLeft.dx, lessThan(actionsTopLeft.dx));
});
testWidgets('Actions laid out beside content if only one action', (WidgetTester tester) async { testWidgets('Actions laid out beside content if only one action', (WidgetTester tester) async {
const String contentText = 'Content'; const String contentText = 'Content';
...@@ -100,6 +220,236 @@ void main() { ...@@ -100,6 +220,236 @@ void main() {
expect(contentBottomLeft.dx, lessThan(actionsTopRight.dx)); expect(contentBottomLeft.dx, lessThan(actionsTopRight.dx));
}); });
testWidgets('Actions laid out beside content if only one action when presented by ScaffoldMessenger', (WidgetTester tester) async {
const String contentText = 'Content';
const Key tapTarget = Key('tap-target');
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: Builder(
builder: (BuildContext context) {
return GestureDetector(
key: tapTarget,
onTap: () {
ScaffoldMessenger.of(context).showMaterialBanner(MaterialBanner(
content: const Text(contentText),
actions: <Widget>[
TextButton(
child: const Text('DISMISS'),
onPressed: () => ScaffoldMessenger.of(context).hideCurrentMaterialBanner(),
),
],
));
},
behavior: HitTestBehavior.opaque,
child: const SizedBox(
height: 100.0,
width: 100.0,
),
);
},
),
),
));
await tester.tap(find.byKey(tapTarget));
await tester.pumpAndSettle();
final Offset contentBottomLeft = tester.getBottomLeft(find.text(contentText));
final Offset actionsTopRight = tester.getTopRight(find.byType(OverflowBar));
expect(contentBottomLeft.dy, greaterThan(actionsTopRight.dy));
expect(contentBottomLeft.dx, lessThan(actionsTopRight.dx));
});
testWidgets('MaterialBanner control test', (WidgetTester tester) async {
const String helloMaterialBanner = 'Hello MaterialBanner';
const Key tapTarget = Key('tap-target');
const Key dismissTarget = Key('dismiss-target');
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: Builder(
builder: (BuildContext context) {
return GestureDetector(
key: tapTarget,
onTap: () {
ScaffoldMessenger.of(context).showMaterialBanner(MaterialBanner(
content: const Text(helloMaterialBanner),
actions: <Widget>[
TextButton(
key: dismissTarget,
child: const Text('DISMISS'),
onPressed: () => ScaffoldMessenger.of(context).hideCurrentMaterialBanner(),
),
],
));
},
behavior: HitTestBehavior.opaque,
child: const SizedBox(
height: 100.0,
width: 100.0,
),
);
},
),
),
));
expect(find.text(helloMaterialBanner), findsNothing);
await tester.tap(find.byKey(tapTarget));
expect(find.text(helloMaterialBanner), findsNothing);
await tester.pump(); // schedule animation
expect(find.text(helloMaterialBanner), findsOneWidget);
await tester.pump(); // begin animation
expect(find.text(helloMaterialBanner), findsOneWidget);
await tester.pump(const Duration(milliseconds: 750)); // 0.75s // animation last frame; two second timer starts here
expect(find.text(helloMaterialBanner), findsOneWidget);
await tester.pump(const Duration(milliseconds: 750)); // 1.50s
expect(find.text(helloMaterialBanner), findsOneWidget);
await tester.pump(const Duration(milliseconds: 750)); // 2.25s
expect(find.text(helloMaterialBanner), findsOneWidget);
await tester.tap(find.byKey(dismissTarget));
await tester.pump(); // begin animation
expect(find.text(helloMaterialBanner), findsOneWidget); // frame 0 of dismiss animation
await tester.pumpAndSettle(); // 3.75s // last frame of animation, material banner removed from build
expect(find.text(helloMaterialBanner), findsNothing);
});
testWidgets('MaterialBanner twice test', (WidgetTester tester) async {
int materialBannerCount = 0;
const Key tapTarget = Key('tap-target');
const Key dismissTarget = Key('dismiss-target');
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: Builder(
builder: (BuildContext context) {
return GestureDetector(
key: tapTarget,
onTap: () {
materialBannerCount += 1;
ScaffoldMessenger.of(context).showMaterialBanner(MaterialBanner(
content: Text('banner$materialBannerCount'),
actions: <Widget>[
TextButton(
key: dismissTarget,
child: const Text('DISMISS'),
onPressed: () => ScaffoldMessenger.of(context).hideCurrentMaterialBanner(),
),
],
));
},
behavior: HitTestBehavior.opaque,
child: const SizedBox(
height: 100.0,
width: 100.0,
),
);
},
),
),
));
expect(find.text('banner1'), findsNothing);
expect(find.text('banner2'), findsNothing);
await tester.tap(find.byKey(tapTarget)); // queue banner1
await tester.tap(find.byKey(tapTarget)); // queue banner2
expect(find.text('banner1'), findsNothing);
expect(find.text('banner2'), findsNothing);
await tester.pump(); // schedule animation for banner1
expect(find.text('banner1'), findsOneWidget);
expect(find.text('banner2'), findsNothing);
await tester.pump(); // begin animation
expect(find.text('banner1'), findsOneWidget);
expect(find.text('banner2'), findsNothing);
await tester.pump(const Duration(milliseconds: 750)); // 0.75s // animation last frame
expect(find.text('banner1'), findsOneWidget);
expect(find.text('banner2'), findsNothing);
await tester.pump(const Duration(milliseconds: 750)); // 1.50s
expect(find.text('banner1'), findsOneWidget);
expect(find.text('banner2'), findsNothing);
await tester.pump(const Duration(milliseconds: 750)); // 2.25s
expect(find.text('banner1'), findsOneWidget);
expect(find.text('banner2'), findsNothing);
await tester.tap(find.byKey(dismissTarget));
await tester.pump(); // begin animation
expect(find.text('banner1'), findsOneWidget);
expect(find.text('banner2'), findsNothing);
await tester.pump(const Duration(milliseconds: 750)); // 3.75s // last frame of animation, material banner removed from build, new material banner put in its place
expect(find.text('banner1'), findsNothing);
expect(find.text('banner2'), findsOneWidget);
await tester.pump(); // begin animation
expect(find.text('banner1'), findsNothing);
expect(find.text('banner2'), findsOneWidget);
await tester.pump(const Duration(milliseconds: 750)); // 4.50s // animation last frame
expect(find.text('banner1'), findsNothing);
expect(find.text('banner2'), findsOneWidget);
await tester.pump(const Duration(milliseconds: 750)); // 5.25s
expect(find.text('banner1'), findsNothing);
expect(find.text('banner2'), findsOneWidget);
await tester.pump(const Duration(milliseconds: 750)); // 6.00s
expect(find.text('banner1'), findsNothing);
expect(find.text('banner2'), findsOneWidget);
await tester.tap(find.byKey(dismissTarget)); // reverse animation is scheduled
await tester.pump(); // begin animation
expect(find.text('banner1'), findsNothing);
expect(find.text('banner2'), findsOneWidget);
await tester.pump(const Duration(milliseconds: 750)); // 7.50s // last frame of animation, material banner removed from build
expect(find.text('banner1'), findsNothing);
expect(find.text('banner2'), findsNothing);
});
testWidgets('ScaffoldMessenger does not duplicate a MaterialBanner when presenting a SnackBar.', (WidgetTester tester) async {
const Key materialBannerTapTarget = Key('materialbanner-tap-target');
const Key snackBarTapTarget = Key('snackbar-tap-target');
const String snackBarText = 'SnackBar';
const String materialBannerText = 'MaterialBanner';
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: Builder(
builder: (BuildContext context) {
return Column(
children: <Widget>[
GestureDetector(
key: snackBarTapTarget,
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text(snackBarText),
));
},
behavior: HitTestBehavior.opaque,
child: const SizedBox(
height: 100.0,
width: 100.0,
),
),
GestureDetector(
key: materialBannerTapTarget,
onTap: () {
ScaffoldMessenger.of(context).showMaterialBanner(MaterialBanner(
content: const Text(materialBannerText),
actions: <Widget>[
TextButton(
child: const Text('DISMISS'),
onPressed: () => ScaffoldMessenger.of(context).hideCurrentMaterialBanner(),
),
],
));
},
behavior: HitTestBehavior.opaque,
child: const SizedBox(
height: 100.0,
width: 100.0,
),
),
],
);
},
),
),
));
await tester.tap(find.byKey(snackBarTapTarget));
await tester.tap(find.byKey(materialBannerTapTarget));
await tester.pumpAndSettle();
expect(find.text(snackBarText), findsOneWidget);
expect(find.text(materialBannerText), findsOneWidget);
});
// Regression test for https://github.com/flutter/flutter/issues/39574 // Regression test for https://github.com/flutter/flutter/issues/39574
testWidgets('Single action laid out beside content but aligned to the trailing edge', (WidgetTester tester) async { testWidgets('Single action laid out beside content but aligned to the trailing edge', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
...@@ -121,6 +471,44 @@ void main() { ...@@ -121,6 +471,44 @@ void main() {
expect(actionsTopRight.dx + 8, bannerTopRight.dx); // actions OverflowBar is padded by 8 expect(actionsTopRight.dx + 8, bannerTopRight.dx); // actions OverflowBar is padded by 8
}); });
// Regression test for https://github.com/flutter/flutter/issues/39574
testWidgets('Single action laid out beside content but aligned to the trailing edge when presented by ScaffoldMessenger', (WidgetTester tester) async {
const Key tapTarget = Key('tap-target');
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: Builder(
builder: (BuildContext context) {
return GestureDetector(
key: tapTarget,
onTap: () {
ScaffoldMessenger.of(context).showMaterialBanner(MaterialBanner(
content: const Text('Content'),
actions: <Widget>[
TextButton(
child: const Text('DISMISS'),
onPressed: () => ScaffoldMessenger.of(context).hideCurrentMaterialBanner(),
),
],
));
},
behavior: HitTestBehavior.opaque,
child: const SizedBox(
height: 100.0,
width: 100.0,
),
);
},
),
),
));
await tester.tap(find.byKey(tapTarget));
await tester.pumpAndSettle();
final Offset actionsTopRight = tester.getTopRight(find.byType(OverflowBar));
final Offset bannerTopRight = tester.getTopRight(find.byType(MaterialBanner));
expect(actionsTopRight.dx + 8, bannerTopRight.dx); // actions OverflowBar is padded by 8
});
// Regression test for https://github.com/flutter/flutter/issues/39574 // Regression test for https://github.com/flutter/flutter/issues/39574
testWidgets('Single action laid out beside content but aligned to the trailing edge - RTL', (WidgetTester tester) async { testWidgets('Single action laid out beside content but aligned to the trailing edge - RTL', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
...@@ -145,6 +533,46 @@ void main() { ...@@ -145,6 +533,46 @@ void main() {
expect(actionsTopLeft.dx - 8, bannerTopLeft.dx); // actions OverflowBar is padded by 8 expect(actionsTopLeft.dx - 8, bannerTopLeft.dx); // actions OverflowBar is padded by 8
}); });
testWidgets('Single action laid out beside content but aligned to the trailing edge when presented by ScaffoldMessenger - RTL', (WidgetTester tester) async {
const Key tapTarget = Key('tap-target');
await tester.pumpWidget(MaterialApp(
home: Directionality(
textDirection: TextDirection.rtl,
child: Scaffold(
body: Builder(
builder: (BuildContext context) {
return GestureDetector(
key: tapTarget,
onTap: () {
ScaffoldMessenger.of(context).showMaterialBanner(MaterialBanner(
content: const Text('Content'),
actions: <Widget>[
TextButton(
child: const Text('DISMISS'),
onPressed: () => ScaffoldMessenger.of(context).hideCurrentMaterialBanner(),
),
],
));
},
behavior: HitTestBehavior.opaque,
child: const SizedBox(
height: 100.0,
width: 100.0,
),
);
},
),
),
),
));
await tester.tap(find.byKey(tapTarget));
await tester.pumpAndSettle();
final Offset actionsTopLeft = tester.getTopLeft(find.byType(OverflowBar));
final Offset bannerTopLeft = tester.getTopLeft(find.byType(MaterialBanner));
expect(actionsTopLeft.dx - 8, bannerTopLeft.dx); // actions OverflowBar is padded by 8
});
testWidgets('Actions laid out below content if forced override', (WidgetTester tester) async { testWidgets('Actions laid out below content if forced override', (WidgetTester tester) async {
const String contentText = 'Content'; const String contentText = 'Content';
...@@ -169,10 +597,49 @@ void main() { ...@@ -169,10 +597,49 @@ void main() {
expect(contentBottomLeft.dx, lessThan(actionsTopLeft.dx)); expect(contentBottomLeft.dx, lessThan(actionsTopLeft.dx));
}); });
testWidgets('Actions laid out below content if forced override when presented by ScaffoldMessenger', (WidgetTester tester) async {
const String contentText = 'Content';
const Key tapTarget = Key('tap-target');
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: Builder(
builder: (BuildContext context) {
return GestureDetector(
key: tapTarget,
onTap: () {
ScaffoldMessenger.of(context).showMaterialBanner(MaterialBanner(
content: const Text(contentText),
forceActionsBelow: true,
actions: <Widget>[
TextButton(
child: const Text('DISMISS'),
onPressed: () => ScaffoldMessenger.of(context).hideCurrentMaterialBanner(),
),
],
));
},
behavior: HitTestBehavior.opaque,
child: const SizedBox(
height: 100.0,
width: 100.0,
),
);
},
),
),
));
await tester.tap(find.byKey(tapTarget));
await tester.pumpAndSettle();
final Offset contentBottomLeft = tester.getBottomLeft(find.text(contentText));
final Offset actionsTopLeft = tester.getTopLeft(find.byType(OverflowBar));
expect(contentBottomLeft.dy, lessThan(actionsTopLeft.dy));
expect(contentBottomLeft.dx, lessThan(actionsTopLeft.dx));
});
testWidgets('Action widgets layout', (WidgetTester tester) async { testWidgets('Action widgets layout', (WidgetTester tester) async {
// This regression test ensures that the action widgets layout matches what // This regression test ensures that the action widgets layout matches what
// it was, before ButtonBar was replaced by OverflowBar. // it was, before ButtonBar was replaced by OverflowBar.
Widget buildFrame(int actionCount, TextDirection textDirection) { Widget buildFrame(int actionCount, TextDirection textDirection) {
return MaterialApp( return MaterialApp(
home: Directionality( home: Directionality(
...@@ -191,6 +658,75 @@ void main() { ...@@ -191,6 +658,75 @@ void main() {
); );
} }
final Finder action0 = find.byKey(const ValueKey<int>(0));
final Finder action1 = find.byKey(const ValueKey<int>(1));
final Finder action2 = find.byKey(const ValueKey<int>(2));
// The action coordinates that follow were obtained by running
// the test code, before ButtonBar was replaced by OverflowBar.
await tester.pumpWidget(buildFrame(1, TextDirection.ltr));
expect(tester.getTopLeft(action0), const Offset(728, 28));
await tester.pumpWidget(buildFrame(1, TextDirection.rtl));
expect(tester.getTopLeft(action0), const Offset(8, 28));
await tester.pumpWidget(buildFrame(3, TextDirection.ltr));
expect(tester.getTopLeft(action0), const Offset(584, 130));
expect(tester.getTopLeft(action1), const Offset(656, 130));
expect(tester.getTopLeft(action2), const Offset(728, 130));
await tester.pumpWidget(buildFrame(3, TextDirection.rtl));
expect(tester.getTopLeft(action0), const Offset(152, 130));
expect(tester.getTopLeft(action1), const Offset(80, 130));
expect(tester.getTopLeft(action2), const Offset(8, 130));
});
testWidgets('Action widgets layout when presented by ScaffoldMessenger', (WidgetTester tester) async {
// This regression test ensures that the action widgets layout matches what
// it was, before ButtonBar was replaced by OverflowBar.
Widget buildFrame(int actionCount, TextDirection textDirection) {
return MaterialApp(
home: Directionality(
textDirection: textDirection,
child: Scaffold(
body: Builder(
builder: (BuildContext context) {
return GestureDetector(
key: const ValueKey<String>('tap-target'),
onTap: () {
ScaffoldMessenger.of(context).showMaterialBanner(MaterialBanner(
content: const SizedBox(width: 100, height: 100),
actions: List<Widget>.generate(actionCount, (int index) {
if (index == 0)
return SizedBox(
width: 64,
height: 48,
key: ValueKey<int>(index),
child: GestureDetector(
key: const ValueKey<String>('dismiss-target'),
onTap: () => ScaffoldMessenger.of(context).hideCurrentMaterialBanner(),
),
);
return SizedBox(
width: 64,
height: 48,
key: ValueKey<int>(index),
);
}),
));
},
);
}
),
),
),
);
}
final Finder tapTarget = find.byKey(const ValueKey<String>('tap-target'));
final Finder dismissTarget = find.byKey(const ValueKey<String>('dismiss-target'));
final Finder action0 = find.byKey(const ValueKey<int>(0)); final Finder action0 = find.byKey(const ValueKey<int>(0));
final Finder action1 = find.byKey(const ValueKey<int>(1)); final Finder action1 = find.byKey(const ValueKey<int>(1));
final Finder action2 = find.byKey(const ValueKey<int>(2)); final Finder action2 = find.byKey(const ValueKey<int>(2));
...@@ -199,26 +735,41 @@ void main() { ...@@ -199,26 +735,41 @@ void main() {
// the test code, before ButtonBar was replaced by OverflowBar. // the test code, before ButtonBar was replaced by OverflowBar.
await tester.pumpWidget(buildFrame(1, TextDirection.ltr)); await tester.pumpWidget(buildFrame(1, TextDirection.ltr));
await tester.tap(tapTarget);
await tester.pumpAndSettle();
expect(tester.getTopLeft(action0), const Offset(728, 28)); expect(tester.getTopLeft(action0), const Offset(728, 28));
await tester.tap(dismissTarget);
await tester.pumpAndSettle();
await tester.pumpWidget(buildFrame(1, TextDirection.rtl)); await tester.pumpWidget(buildFrame(1, TextDirection.rtl));
await tester.tap(tapTarget);
await tester.pumpAndSettle();
expect(tester.getTopLeft(action0), const Offset(8, 28)); expect(tester.getTopLeft(action0), const Offset(8, 28));
await tester.tap(dismissTarget);
await tester.pumpAndSettle();
await tester.pumpWidget(buildFrame(3, TextDirection.ltr)); await tester.pumpWidget(buildFrame(3, TextDirection.ltr));
await tester.tap(tapTarget);
await tester.pumpAndSettle();
expect(tester.getTopLeft(action0), const Offset(584, 130)); expect(tester.getTopLeft(action0), const Offset(584, 130));
expect(tester.getTopLeft(action1), const Offset(656, 130)); expect(tester.getTopLeft(action1), const Offset(656, 130));
expect(tester.getTopLeft(action2), const Offset(728, 130)); expect(tester.getTopLeft(action2), const Offset(728, 130));
await tester.tap(dismissTarget);
await tester.pumpAndSettle();
await tester.pumpWidget(buildFrame(3, TextDirection.rtl)); await tester.pumpWidget(buildFrame(3, TextDirection.rtl));
await tester.tap(tapTarget);
await tester.pumpAndSettle();
expect(tester.getTopLeft(action0), const Offset(152, 130)); expect(tester.getTopLeft(action0), const Offset(152, 130));
expect(tester.getTopLeft(action1), const Offset(80, 130)); expect(tester.getTopLeft(action1), const Offset(80, 130));
expect(tester.getTopLeft(action2), const Offset(8, 130)); expect(tester.getTopLeft(action2), const Offset(8, 130));
await tester.tap(dismissTarget);
await tester.pumpAndSettle();
}); });
testWidgets('Action widgets layout with overflow', (WidgetTester tester) async { testWidgets('Action widgets layout with overflow', (WidgetTester tester) async {
// This regression test ensures that the action widgets layout matches what // This regression test ensures that the action widgets layout matches what
// it was, before ButtonBar was replaced by OverflowBar. // it was, before ButtonBar was replaced by OverflowBar.
const int actionCount = 4; const int actionCount = 4;
Widget buildFrame(TextDirection textDirection) { Widget buildFrame(TextDirection textDirection) {
return MaterialApp( return MaterialApp(
...@@ -237,19 +788,88 @@ void main() { ...@@ -237,19 +788,88 @@ void main() {
), ),
); );
} }
// The action coordinates that follow were obtained by running
// the test code, before ButtonBar was replaced by OverflowBar.
await tester.pumpWidget(buildFrame(TextDirection.ltr));
for (int index = 0; index < actionCount; index += 1) {
expect(tester.getTopLeft(find.byKey(ValueKey<int>(index))), Offset(592, 134.0 + index * 10));
}
await tester.pumpWidget(buildFrame(TextDirection.rtl));
for (int index = 0; index < actionCount; index += 1) {
expect(tester.getTopLeft(find.byKey(ValueKey<int>(index))), Offset(8, 134.0 + index * 10));
}
});
testWidgets('Action widgets layout with overflow when presented by ScaffoldMessenger', (WidgetTester tester) async {
// This regression test ensures that the action widgets layout matches what
// it was, before ButtonBar was replaced by OverflowBar.
const int actionCount = 4;
Widget buildFrame(TextDirection textDirection) {
return MaterialApp(
home: Directionality(
textDirection: textDirection,
child: Scaffold(
body: Builder(
builder: (BuildContext context) {
return GestureDetector(
key: const ValueKey<String>('tap-target'),
onTap: () {
ScaffoldMessenger.of(context).showMaterialBanner(MaterialBanner(
content: const SizedBox(width: 100, height: 100),
actions: List<Widget>.generate(actionCount, (int index) {
if (index == 0)
return SizedBox(
width: 200,
height: 10,
key: ValueKey<int>(index),
child: GestureDetector(
key: const ValueKey<String>('dismiss-target'),
onTap: () => ScaffoldMessenger.of(context).hideCurrentMaterialBanner(),
),
);
return SizedBox(
width: 200,
height: 10,
key: ValueKey<int>(index),
);
}),
));
},
);
}
),
),
),
);
}
// The action coordinates that follow were obtained by running // The action coordinates that follow were obtained by running
// the test code, before ButtonBar was replaced by OverflowBar. // the test code, before ButtonBar was replaced by OverflowBar.
final Finder tapTarget = find.byKey(const ValueKey<String>('tap-target'));
final Finder dismissTarget = find.byKey(const ValueKey<String>('dismiss-target'));
await tester.pumpWidget(buildFrame(TextDirection.ltr)); await tester.pumpWidget(buildFrame(TextDirection.ltr));
await tester.tap(tapTarget);
await tester.pumpAndSettle();
for (int index = 0; index < actionCount; index += 1) { for (int index = 0; index < actionCount; index += 1) {
expect(tester.getTopLeft(find.byKey(ValueKey<int>(index))), Offset(592, 134.0 + index * 10)); expect(tester.getTopLeft(find.byKey(ValueKey<int>(index))), Offset(592, 134.0 + index * 10));
} }
await tester.tap(dismissTarget);
await tester.pumpAndSettle();
await tester.pumpWidget(buildFrame(TextDirection.rtl)); await tester.pumpWidget(buildFrame(TextDirection.rtl));
await tester.tap(tapTarget);
await tester.pumpAndSettle();
for (int index = 0; index < actionCount; index += 1) { for (int index = 0; index < actionCount; index += 1) {
expect(tester.getTopLeft(find.byKey(ValueKey<int>(index))), Offset(8, 134.0 + index * 10)); expect(tester.getTopLeft(find.byKey(ValueKey<int>(index))), Offset(8, 134.0 + index * 10));
} }
await tester.tap(dismissTarget);
await tester.pumpAndSettle();
}); });
testWidgets('[overflowAlignment] test', (WidgetTester tester) async { testWidgets('[overflowAlignment] test', (WidgetTester tester) async {
...@@ -288,12 +908,90 @@ void main() { ...@@ -288,12 +908,90 @@ void main() {
expect(tester.getTopLeft(find.byKey(ValueKey<int>(index))), Offset(592, 134.0 + index * 10)); expect(tester.getTopLeft(find.byKey(ValueKey<int>(index))), Offset(592, 134.0 + index * 10));
} }
}); });
testWidgets('[overflowAlignment] test when presented by ScaffoldMessenger', (WidgetTester tester) async {
const int actionCount = 4;
Widget buildFrame(TextDirection textDirection, OverflowBarAlignment overflowAlignment) {
return MaterialApp(
home: Directionality(
textDirection: textDirection,
child: Scaffold(
body: Builder(
builder: (BuildContext context) {
return GestureDetector(
key: const ValueKey<String>('tap-target'),
onTap: () {
ScaffoldMessenger.of(context).showMaterialBanner(MaterialBanner(
overflowAlignment: overflowAlignment,
content: const SizedBox(width: 100, height: 100),
actions: List<Widget>.generate(actionCount, (int index) {
if (index == 0)
return SizedBox(
width: 200,
height: 10,
key: ValueKey<int>(index),
child: GestureDetector(
key: const ValueKey<String>('dismiss-target'),
onTap: () => ScaffoldMessenger.of(context).hideCurrentMaterialBanner(),
),
);
return SizedBox(
width: 200,
height: 10,
key: ValueKey<int>(index),
);
}),
));
},
);
}
),
),
),
);
}
final Finder tapTarget = find.byKey(const ValueKey<String>('tap-target'));
final Finder dismissTarget = find.byKey(const ValueKey<String>('dismiss-target'));
await tester.pumpWidget(buildFrame(TextDirection.ltr, OverflowBarAlignment.start));
await tester.tap(tapTarget);
await tester.pumpAndSettle();
for (int index = 0; index < actionCount; index += 1) {
expect(tester.getTopLeft(find.byKey(ValueKey<int>(index))), Offset(8, 134.0 + index * 10));
}
await tester.tap(dismissTarget);
await tester.pumpAndSettle();
await tester.pumpWidget(buildFrame(TextDirection.ltr, OverflowBarAlignment.center));
await tester.tap(tapTarget);
await tester.pumpAndSettle();
for (int index = 0; index < actionCount; index += 1) {
expect(tester.getTopLeft(find.byKey(ValueKey<int>(index))), Offset(300, 134.0 + index * 10));
}
await tester.tap(dismissTarget);
await tester.pumpAndSettle();
await tester.pumpWidget(buildFrame(TextDirection.ltr, OverflowBarAlignment.end));
await tester.tap(tapTarget);
await tester.pumpAndSettle();
for (int index = 0; index < actionCount; index += 1) {
expect(tester.getTopLeft(find.byKey(ValueKey<int>(index))), Offset(592, 134.0 + index * 10));
}
await tester.tap(dismissTarget);
await tester.pumpAndSettle();
});
} }
Container _getContainerFromBanner(WidgetTester tester) { Container _getContainerFromBanner(WidgetTester tester) {
return tester.widget<Container>(find.descendant(of: find.byType(MaterialBanner), matching: find.byType(Container)).first); return tester.widget<Container>(find.descendant(of: find.byType(MaterialBanner), matching: find.byType(Container)).first);
} }
Container _getContainerFromText(WidgetTester tester, String text) {
return tester.widget<Container>(find.widgetWithText(Container, text).first);
}
RenderParagraph _getTextRenderObjectFromDialog(WidgetTester tester, String text) { RenderParagraph _getTextRenderObjectFromDialog(WidgetTester tester, String text) {
return tester.element<StatelessElement>(find.descendant(of: find.byType(MaterialBanner), matching: find.text(text))).renderObject! as RenderParagraph; return tester.element<StatelessElement>(find.descendant(of: find.byType(MaterialBanner), matching: find.text(text))).renderObject! as RenderParagraph;
} }
...@@ -64,7 +64,47 @@ void main() { ...@@ -64,7 +64,47 @@ void main() {
), ),
)); ));
final Container container = _getContainerFromBanner(tester); final Container container = _getContainerFromText(tester, contentText);
final RenderParagraph content = _getTextRenderObjectFromDialog(tester, contentText);
expect(container.color, const Color(0xffffffff));
// Default value for ThemeData.typography is Typography.material2014()
expect(content.text.style, Typography.material2014().englishLike.bodyText2!.merge(Typography.material2014().black.bodyText2));
});
testWidgets('Passing no MaterialBannerThemeData returns defaults when presented by ScaffoldMessenger', (WidgetTester tester) async {
const String contentText = 'Content';
const Key tapTarget = Key('tap-target');
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: Builder(
builder: (BuildContext context) {
return GestureDetector(
key: tapTarget,
onTap: () {
ScaffoldMessenger.of(context).showMaterialBanner(MaterialBanner(
content: const Text(contentText),
actions: <Widget>[
TextButton(
child: const Text('Action'),
onPressed: () { },
),
],
));
},
behavior: HitTestBehavior.opaque,
child: const SizedBox(
height: 100.0,
width: 100.0,
),
);
},
),
),
));
await tester.tap(find.byKey(tapTarget));
await tester.pumpAndSettle();
final Container container = _getContainerFromText(tester, contentText);
final RenderParagraph content = _getTextRenderObjectFromDialog(tester, contentText); final RenderParagraph content = _getTextRenderObjectFromDialog(tester, contentText);
expect(container.color, const Color(0xffffffff)); expect(container.color, const Color(0xffffffff));
// Default value for ThemeData.typography is Typography.material2014() // Default value for ThemeData.typography is Typography.material2014()
...@@ -90,7 +130,57 @@ void main() { ...@@ -90,7 +130,57 @@ void main() {
), ),
)); ));
final Container container = _getContainerFromBanner(tester); final Container container = _getContainerFromText(tester, contentText);
final RenderParagraph content = _getTextRenderObjectFromDialog(tester, contentText);
expect(container.color, bannerTheme.backgroundColor);
expect(content.text.style, bannerTheme.contentTextStyle);
final Offset contentTopLeft = tester.getTopLeft(_textFinder(contentText));
final Offset containerTopLeft = tester.getTopLeft(_containerFinder());
final Offset leadingTopLeft = tester.getTopLeft(find.byIcon(Icons.ac_unit));
expect(contentTopLeft.dy - containerTopLeft.dy, 24);
expect(contentTopLeft.dx - containerTopLeft.dx, 41);
expect(leadingTopLeft.dy - containerTopLeft.dy, 19);
expect(leadingTopLeft.dx - containerTopLeft.dx, 11);
});
testWidgets('MaterialBanner uses values from MaterialBannerThemeData when presented by ScaffoldMessenger', (WidgetTester tester) async {
final MaterialBannerThemeData bannerTheme = _bannerTheme();
const String contentText = 'Content';
const Key tapTarget = Key('tap-target');
await tester.pumpWidget(MaterialApp(
theme: ThemeData(bannerTheme: bannerTheme),
home: Scaffold(
body: Builder(
builder: (BuildContext context) {
return GestureDetector(
key: tapTarget,
onTap: () {
ScaffoldMessenger.of(context).showMaterialBanner(MaterialBanner(
leading: const Icon(Icons.ac_unit),
content: const Text(contentText),
actions: <Widget>[
TextButton(
child: const Text('Action'),
onPressed: () { },
),
],
));
},
behavior: HitTestBehavior.opaque,
child: const SizedBox(
height: 100.0,
width: 100.0,
),
);
},
),
),
));
await tester.tap(find.byKey(tapTarget));
await tester.pumpAndSettle();
final Container container = _getContainerFromText(tester, contentText);
final RenderParagraph content = _getTextRenderObjectFromDialog(tester, contentText); final RenderParagraph content = _getTextRenderObjectFromDialog(tester, contentText);
expect(container.color, bannerTheme.backgroundColor); expect(container.color, bannerTheme.backgroundColor);
expect(content.text.style, bannerTheme.contentTextStyle); expect(content.text.style, bannerTheme.contentTextStyle);
...@@ -129,7 +219,63 @@ void main() { ...@@ -129,7 +219,63 @@ void main() {
), ),
)); ));
final Container container = _getContainerFromBanner(tester); final Container container = _getContainerFromText(tester, contentText);
final RenderParagraph content = _getTextRenderObjectFromDialog(tester, contentText);
expect(container.color, backgroundColor);
expect(content.text.style, textStyle);
final Offset contentTopLeft = tester.getTopLeft(_textFinder(contentText));
final Offset containerTopLeft = tester.getTopLeft(_containerFinder());
final Offset leadingTopLeft = tester.getTopLeft(find.byIcon(Icons.ac_unit));
expect(contentTopLeft.dy - containerTopLeft.dy, 29);
expect(contentTopLeft.dx - containerTopLeft.dx, 58);
expect(leadingTopLeft.dy - containerTopLeft.dy, 24);
expect(leadingTopLeft.dx - containerTopLeft.dx, 22);
});
testWidgets('MaterialBanner widget properties take priority over theme when presented by ScaffoldMessenger', (WidgetTester tester) async {
const Color backgroundColor = Colors.purple;
const TextStyle textStyle = TextStyle(color: Colors.green);
final MaterialBannerThemeData bannerTheme = _bannerTheme();
const String contentText = 'Content';
const Key tapTarget = Key('tap-target');
await tester.pumpWidget(MaterialApp(
theme: ThemeData(bannerTheme: bannerTheme),
home: Scaffold(
body: Builder(
builder: (BuildContext context) {
return GestureDetector(
key: tapTarget,
onTap: () {
ScaffoldMessenger.of(context).showMaterialBanner(MaterialBanner(
backgroundColor: backgroundColor,
leading: const Icon(Icons.ac_unit),
contentTextStyle: textStyle,
content: const Text(contentText),
padding: const EdgeInsets.all(10),
leadingPadding: const EdgeInsets.all(12),
actions: <Widget>[
TextButton(
child: const Text('Action'),
onPressed: () { },
),
],
));
},
behavior: HitTestBehavior.opaque,
child: const SizedBox(
height: 100.0,
width: 100.0,
),
);
},
),
),
));
await tester.tap(find.byKey(tapTarget));
await tester.pumpAndSettle();
final Container container = _getContainerFromText(tester, contentText);
final RenderParagraph content = _getTextRenderObjectFromDialog(tester, contentText); final RenderParagraph content = _getTextRenderObjectFromDialog(tester, contentText);
expect(container.color, backgroundColor); expect(container.color, backgroundColor);
expect(content.text.style, textStyle); expect(content.text.style, textStyle);
...@@ -145,11 +291,12 @@ void main() { ...@@ -145,11 +291,12 @@ void main() {
testWidgets('MaterialBanner uses color scheme when necessary', (WidgetTester tester) async { testWidgets('MaterialBanner uses color scheme when necessary', (WidgetTester tester) async {
final ColorScheme colorScheme = const ColorScheme.light().copyWith(surface: Colors.purple); final ColorScheme colorScheme = const ColorScheme.light().copyWith(surface: Colors.purple);
const String contentText = 'Content';
await tester.pumpWidget(MaterialApp( await tester.pumpWidget(MaterialApp(
theme: ThemeData(colorScheme: colorScheme), theme: ThemeData(colorScheme: colorScheme),
home: Scaffold( home: Scaffold(
body: MaterialBanner( body: MaterialBanner(
content: const Text('Content'), content: const Text(contentText),
actions: <Widget>[ actions: <Widget>[
TextButton( TextButton(
child: const Text('Action'), child: const Text('Action'),
...@@ -160,7 +307,46 @@ void main() { ...@@ -160,7 +307,46 @@ void main() {
), ),
)); ));
final Container container = _getContainerFromBanner(tester); final Container container = _getContainerFromText(tester, contentText);
expect(container.color, colorScheme.surface);
});
testWidgets('MaterialBanner uses color scheme when necessary when presented by ScaffoldMessenger', (WidgetTester tester) async {
final ColorScheme colorScheme = const ColorScheme.light().copyWith(surface: Colors.purple);
const String contentText = 'Content';
const Key tapTarget = Key('tap-target');
await tester.pumpWidget(MaterialApp(
theme: ThemeData(colorScheme: colorScheme),
home: Scaffold(
body: Builder(
builder: (BuildContext context) {
return GestureDetector(
key: tapTarget,
onTap: () {
ScaffoldMessenger.of(context).showMaterialBanner(MaterialBanner(
content: const Text(contentText),
actions: <Widget>[
TextButton(
child: const Text('Action'),
onPressed: () { },
),
],
));
},
behavior: HitTestBehavior.opaque,
child: const SizedBox(
height: 100.0,
width: 100.0,
),
);
},
),
),
));
await tester.tap(find.byKey(tapTarget));
await tester.pumpAndSettle();
final Container container = _getContainerFromText(tester, contentText);
expect(container.color, colorScheme.surface); expect(container.color, colorScheme.surface);
}); });
} }
...@@ -174,8 +360,8 @@ MaterialBannerThemeData _bannerTheme() { ...@@ -174,8 +360,8 @@ MaterialBannerThemeData _bannerTheme() {
); );
} }
Container _getContainerFromBanner(WidgetTester tester) { Container _getContainerFromText(WidgetTester tester, String text) {
return tester.widget<Container>(_containerFinder()); return tester.widget<Container>(find.widgetWithText(Container, text).first);
} }
Finder _containerFinder() { Finder _containerFinder() {
......
...@@ -2282,6 +2282,63 @@ void main() { ...@@ -2282,6 +2282,63 @@ void main() {
await expectLater(find.byType(MaterialApp), matchesGoldenFile('snack_bar.goldenTest.workWithBottomSheet.png')); await expectLater(find.byType(MaterialApp), matchesGoldenFile('snack_bar.goldenTest.workWithBottomSheet.png'));
}); });
testWidgets('ScaffoldMessenger does not duplicate a SnackBar when presenting a MaterialBanner.', (WidgetTester tester) async {
const Key materialBannerTapTarget = Key('materialbanner-tap-target');
const Key snackBarTapTarget = Key('snackbar-tap-target');
const String snackBarText = 'SnackBar';
const String materialBannerText = 'MaterialBanner';
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: Builder(
builder: (BuildContext context) {
return Column(
children: <Widget>[
GestureDetector(
key: snackBarTapTarget,
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text(snackBarText),
));
},
behavior: HitTestBehavior.opaque,
child: const SizedBox(
height: 100.0,
width: 100.0,
),
),
GestureDetector(
key: materialBannerTapTarget,
onTap: () {
ScaffoldMessenger.of(context).showMaterialBanner(MaterialBanner(
content: const Text(materialBannerText),
actions: <Widget>[
TextButton(
child: const Text('DISMISS'),
onPressed: () => ScaffoldMessenger.of(context).hideCurrentMaterialBanner(),
),
],
));
},
behavior: HitTestBehavior.opaque,
child: const SizedBox(
height: 100.0,
width: 100.0,
),
),
],
);
},
),
),
));
await tester.tap(find.byKey(snackBarTapTarget));
await tester.tap(find.byKey(materialBannerTapTarget));
await tester.pumpAndSettle();
expect(find.text(snackBarText), findsOneWidget);
expect(find.text(materialBannerText), findsOneWidget);
});
testWidgets('ScaffoldMessenger presents SnackBars to only the root Scaffold when Scaffolds are nested.', (WidgetTester tester) async { testWidgets('ScaffoldMessenger presents SnackBars to only the root Scaffold when Scaffolds are nested.', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp( await tester.pumpWidget(MaterialApp(
home: Scaffold( home: Scaffold(
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment