Integrate MaterialBanner with the ScaffoldMessenger API (#81706)
This commit is contained in:
parent
734df6f5c6
commit
efe0c5eb85
@ -6,8 +6,44 @@ import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'banner_theme.dart';
|
||||
import 'divider.dart';
|
||||
import 'scaffold.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 banner displays an important, succinct message, and provides actions for
|
||||
@ -19,6 +55,9 @@ import 'theme.dart';
|
||||
/// interact with them at any time.
|
||||
///
|
||||
/// {@tool dartpad --template=stateless_widget_material}
|
||||
///
|
||||
/// Banners placed directly into the widget tree are static.
|
||||
///
|
||||
/// ```dart
|
||||
/// Widget build(BuildContext context) {
|
||||
/// return Scaffold(
|
||||
@ -46,6 +85,41 @@ import 'theme.dart';
|
||||
/// ```
|
||||
/// {@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.
|
||||
/// Otherwise, the [actions] will be placed below the [content]. Use
|
||||
/// [forceActionsBelow] to override this behavior.
|
||||
@ -59,7 +133,7 @@ import 'theme.dart';
|
||||
/// [backgroundColor] can be provided to customize the banner.
|
||||
///
|
||||
/// This widget is unrelated to the widgets library [Banner] widget.
|
||||
class MaterialBanner extends StatelessWidget {
|
||||
class MaterialBanner extends StatefulWidget {
|
||||
/// Creates a [MaterialBanner].
|
||||
///
|
||||
/// The [actions], [content], and [forceActionsBelow] must be non-null.
|
||||
@ -75,6 +149,8 @@ class MaterialBanner extends StatelessWidget {
|
||||
this.leadingPadding,
|
||||
this.forceActionsBelow = false,
|
||||
this.overflowAlignment = OverflowBarAlignment.end,
|
||||
this.animation,
|
||||
this.onVisible
|
||||
}) : assert(content != null),
|
||||
assert(actions != null),
|
||||
assert(forceActionsBelow != null),
|
||||
@ -138,18 +214,101 @@ class MaterialBanner extends StatelessWidget {
|
||||
/// Defaults to [OverflowBarAlignment.end].
|
||||
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
|
||||
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 MaterialBannerThemeData bannerTheme = MaterialBannerTheme.of(context);
|
||||
|
||||
final bool isSingleRow = actions.length == 1 && !forceActionsBelow;
|
||||
final EdgeInsetsGeometry padding = this.padding ?? bannerTheme.padding ?? (isSingleRow
|
||||
final bool isSingleRow = widget.actions.length == 1 && !widget.forceActionsBelow;
|
||||
final EdgeInsetsGeometry padding = widget.padding ?? bannerTheme.padding ?? (isSingleRow
|
||||
? const EdgeInsetsDirectional.only(start: 16.0, top: 2.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
|
||||
?? const EdgeInsetsDirectional.only(end: 16.0);
|
||||
|
||||
@ -158,36 +317,37 @@ class MaterialBanner extends StatelessWidget {
|
||||
constraints: const BoxConstraints(minHeight: 52.0),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: OverflowBar(
|
||||
overflowAlignment: overflowAlignment,
|
||||
overflowAlignment: widget.overflowAlignment,
|
||||
spacing: 8,
|
||||
children: actions,
|
||||
children: widget.actions,
|
||||
),
|
||||
);
|
||||
|
||||
final Color backgroundColor = this.backgroundColor
|
||||
final Color backgroundColor = widget.backgroundColor
|
||||
?? bannerTheme.backgroundColor
|
||||
?? theme.colorScheme.surface;
|
||||
final TextStyle? textStyle = contentTextStyle
|
||||
final TextStyle? textStyle = widget.contentTextStyle
|
||||
?? bannerTheme.contentTextStyle
|
||||
?? theme.textTheme.bodyText2;
|
||||
|
||||
return Container(
|
||||
Widget materialBanner = Container(
|
||||
color: backgroundColor,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: padding,
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
if (leading != null)
|
||||
if (widget.leading != null)
|
||||
Padding(
|
||||
padding: leadingPadding,
|
||||
child: leading,
|
||||
child: widget.leading,
|
||||
),
|
||||
Expanded(
|
||||
child: DefaultTextStyle(
|
||||
style: textStyle!,
|
||||
child: content,
|
||||
child: widget.content,
|
||||
),
|
||||
),
|
||||
if (isSingleRow)
|
||||
@ -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;
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'app_bar.dart';
|
||||
import 'banner.dart';
|
||||
import 'bottom_sheet.dart';
|
||||
import 'colors.dart';
|
||||
import 'curves.dart';
|
||||
@ -49,6 +50,7 @@ enum _ScaffoldSlot {
|
||||
bodyScrim,
|
||||
bottomSheet,
|
||||
snackBar,
|
||||
materialBanner,
|
||||
persistentFooter,
|
||||
bottomNavigationBar,
|
||||
floatingActionButton,
|
||||
@ -256,6 +258,8 @@ class ScaffoldMessenger extends StatefulWidget {
|
||||
/// Typically obtained via [ScaffoldMessenger.of].
|
||||
class ScaffoldMessengerState extends State<ScaffoldMessenger> with TickerProviderStateMixin {
|
||||
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>>();
|
||||
AnimationController? _snackBarController;
|
||||
Timer? _snackBarTimer;
|
||||
@ -280,8 +284,15 @@ class ScaffoldMessengerState extends State<ScaffoldMessenger> with TickerProvide
|
||||
|
||||
void _register(ScaffoldState scaffold) {
|
||||
_scaffolds.add(scaffold);
|
||||
if (_snackBars.isNotEmpty && _isRoot(scaffold)) {
|
||||
scaffold._updateSnackBar();
|
||||
|
||||
if (_isRoot(scaffold)) {
|
||||
if (_snackBars.isNotEmpty) {
|
||||
scaffold._updateSnackBar();
|
||||
}
|
||||
|
||||
if (_materialBanners.isNotEmpty) {
|
||||
scaffold._updateMaterialBanner();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -291,6 +302,24 @@ class ScaffoldMessengerState extends State<ScaffoldMessenger> with TickerProvide
|
||||
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.
|
||||
///
|
||||
/// 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
|
||||
/// {@end-tool}
|
||||
ScaffoldFeatureController<SnackBar, SnackBarClosedReason> showSnackBar(SnackBar snackBar) {
|
||||
_snackBarController ??= SnackBar.createAnimationController(vsync: this)
|
||||
..addStatusListener(_handleStatusChanged);
|
||||
..addStatusListener(_handleSnackBarStatusChanged);
|
||||
if (_snackBars.isEmpty) {
|
||||
assert(_snackBarController!.isDismissed);
|
||||
_snackBarController!.forward();
|
||||
@ -354,7 +383,7 @@ class ScaffoldMessengerState extends State<ScaffoldMessenger> with TickerProvide
|
||||
return controller;
|
||||
}
|
||||
|
||||
void _handleStatusChanged(AnimationStatus status) {
|
||||
void _handleSnackBarStatusChanged(AnimationStatus status) {
|
||||
switch (status) {
|
||||
case AnimationStatus.dismissed:
|
||||
assert(_snackBars.isNotEmpty);
|
||||
@ -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
|
||||
/// [Scaffold]s.
|
||||
///
|
||||
@ -445,6 +460,145 @@ class ScaffoldMessengerState extends State<ScaffoldMessenger> with TickerProvide
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasMediaQuery(context));
|
||||
@ -522,6 +676,7 @@ class ScaffoldPrelayoutGeometry {
|
||||
required this.minViewPadding,
|
||||
required this.scaffoldSize,
|
||||
required this.snackBarSize,
|
||||
required this.materialBannerSize,
|
||||
required this.textDirection,
|
||||
});
|
||||
|
||||
@ -605,6 +760,11 @@ class ScaffoldPrelayoutGeometry {
|
||||
/// If the [Scaffold] is not showing a [SnackBar], this will be [Size.zero].
|
||||
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].
|
||||
final TextDirection textDirection;
|
||||
}
|
||||
@ -967,6 +1127,11 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
|
||||
snackBarSize = layoutChild(_ScaffoldSlot.snackBar, fullWidthConstraints);
|
||||
}
|
||||
|
||||
Size materialBannerSize = Size.zero;
|
||||
if (hasChild(_ScaffoldSlot.materialBanner)) {
|
||||
materialBannerSize = layoutChild(_ScaffoldSlot.materialBanner, fullWidthConstraints);
|
||||
}
|
||||
|
||||
if (hasChild(_ScaffoldSlot.bottomSheet)) {
|
||||
final BoxConstraints bottomSheetConstraints = BoxConstraints(
|
||||
maxWidth: fullWidthConstraints.maxWidth,
|
||||
@ -992,6 +1157,7 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
|
||||
minInsets: minInsets,
|
||||
scaffoldSize: size,
|
||||
snackBarSize: snackBarSize,
|
||||
materialBannerSize: materialBannerSize,
|
||||
textDirection: textDirection,
|
||||
minViewPadding: minViewPadding,
|
||||
);
|
||||
@ -1034,6 +1200,17 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
|
||||
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)) {
|
||||
layoutChild(_ScaffoldSlot.statusBar, fullWidthConstraints.tighten(height: minInsets.top));
|
||||
positionChild(_ScaffoldSlot.statusBar, Offset.zero);
|
||||
@ -2194,12 +2371,14 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin, Resto
|
||||
_endDrawerKey.currentState?.open();
|
||||
}
|
||||
|
||||
// Used for both the snackbar and material banner APIs
|
||||
ScaffoldMessengerState? _scaffoldMessenger;
|
||||
bool? _accessibleNavigation;
|
||||
|
||||
// SNACKBAR API
|
||||
final Queue<ScaffoldFeatureController<SnackBar, SnackBarClosedReason>> _snackBars = Queue<ScaffoldFeatureController<SnackBar, SnackBarClosedReason>>();
|
||||
AnimationController? _snackBarController;
|
||||
Timer? _snackBarTimer;
|
||||
bool? _accessibleNavigation;
|
||||
ScaffoldMessengerState? _scaffoldMessenger;
|
||||
|
||||
/// [ScaffoldMessengerState.showSnackBar] shows a [SnackBar] at the bottom of
|
||||
/// the scaffold. This method should not be used, and will be deprecated in
|
||||
@ -2401,11 +2580,34 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin, Resto
|
||||
|
||||
// This is used to update the _messengerSnackBar by the ScaffoldMessenger.
|
||||
void _updateSnackBar() {
|
||||
setState(() {
|
||||
_messengerSnackBar = _scaffoldMessenger!._snackBars.isNotEmpty
|
||||
? _scaffoldMessenger!._snackBars.first
|
||||
: null;
|
||||
});
|
||||
final ScaffoldFeatureController<SnackBar, SnackBarClosedReason>? messengerSnackBar = _scaffoldMessenger!._snackBars.isNotEmpty
|
||||
? _scaffoldMessenger!._snackBars.first
|
||||
: 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
|
||||
@ -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) {
|
||||
_addIfNonNull(
|
||||
children,
|
||||
@ -3288,7 +3504,7 @@ class ScaffoldFeatureController<T extends Widget, U> {
|
||||
/// Completes when the feature controlled by this object is no longer visible.
|
||||
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;
|
||||
|
||||
/// Mark the feature (e.g., bottom sheet or snack bar) as needing to rebuild.
|
||||
|
@ -28,6 +28,44 @@ void main() {
|
||||
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 {
|
||||
const String contentText = 'Content';
|
||||
const TextStyle contentTextStyle = TextStyle(color: Colors.pink);
|
||||
@ -50,6 +88,45 @@ void main() {
|
||||
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 {
|
||||
const String contentText = 'Content';
|
||||
|
||||
@ -77,6 +154,49 @@ void main() {
|
||||
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 {
|
||||
const String contentText = 'Content';
|
||||
|
||||
@ -100,6 +220,236 @@ void main() {
|
||||
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
|
||||
testWidgets('Single action laid out beside content but aligned to the trailing edge', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
@ -121,12 +471,50 @@ void main() {
|
||||
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
|
||||
testWidgets('Single action laid out beside content but aligned to the trailing edge - RTL', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
textDirection: TextDirection.rtl,
|
||||
child: MaterialBanner(
|
||||
content: const Text('Content'),
|
||||
actions: <Widget>[
|
||||
@ -145,6 +533,46 @@ void main() {
|
||||
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 {
|
||||
const String contentText = 'Content';
|
||||
|
||||
@ -169,10 +597,49 @@ void main() {
|
||||
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 {
|
||||
// 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(
|
||||
@ -194,7 +661,6 @@ 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.
|
||||
|
||||
@ -215,10 +681,95 @@ void main() {
|
||||
expect(tester.getTopLeft(action2), const Offset(8, 130));
|
||||
});
|
||||
|
||||
testWidgets('Action widgets layout with overflow', (WidgetTester tester) async {
|
||||
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 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));
|
||||
await tester.tap(tapTarget);
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.getTopLeft(action0), const Offset(728, 28));
|
||||
await tester.tap(dismissTarget);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.pumpWidget(buildFrame(1, TextDirection.rtl));
|
||||
await tester.tap(tapTarget);
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.getTopLeft(action0), const Offset(8, 28));
|
||||
await tester.tap(dismissTarget);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
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(action1), const Offset(656, 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.tap(tapTarget);
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.getTopLeft(action0), const Offset(152, 130));
|
||||
expect(tester.getTopLeft(action1), const Offset(80, 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 {
|
||||
// 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(
|
||||
@ -237,7 +788,6 @@ void main() {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// The action coordinates that follow were obtained by running
|
||||
// the test code, before ButtonBar was replaced by OverflowBar.
|
||||
|
||||
@ -252,6 +802,76 @@ void main() {
|
||||
}
|
||||
});
|
||||
|
||||
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 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.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();
|
||||
|
||||
await tester.pumpWidget(buildFrame(TextDirection.rtl));
|
||||
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();
|
||||
});
|
||||
|
||||
testWidgets('[overflowAlignment] test', (WidgetTester tester) async {
|
||||
const int actionCount = 4;
|
||||
Widget buildFrame(TextDirection textDirection, OverflowBarAlignment overflowAlignment) {
|
||||
@ -288,12 +908,90 @@ void main() {
|
||||
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) {
|
||||
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) {
|
||||
return tester.element<StatelessElement>(find.descendant(of: find.byType(MaterialBanner), matching: find.text(text))).renderObject! as RenderParagraph;
|
||||
}
|
||||
|
@ -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);
|
||||
expect(container.color, const Color(0xffffffff));
|
||||
// Default value for ThemeData.typography is Typography.material2014()
|
||||
@ -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);
|
||||
expect(container.color, bannerTheme.backgroundColor);
|
||||
expect(content.text.style, bannerTheme.contentTextStyle);
|
||||
@ -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);
|
||||
expect(container.color, backgroundColor);
|
||||
expect(content.text.style, textStyle);
|
||||
@ -145,11 +291,12 @@ void main() {
|
||||
|
||||
testWidgets('MaterialBanner uses color scheme when necessary', (WidgetTester tester) async {
|
||||
final ColorScheme colorScheme = const ColorScheme.light().copyWith(surface: Colors.purple);
|
||||
const String contentText = 'Content';
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
theme: ThemeData(colorScheme: colorScheme),
|
||||
home: Scaffold(
|
||||
body: MaterialBanner(
|
||||
content: const Text('Content'),
|
||||
content: const Text(contentText),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
child: const Text('Action'),
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
@ -174,8 +360,8 @@ MaterialBannerThemeData _bannerTheme() {
|
||||
);
|
||||
}
|
||||
|
||||
Container _getContainerFromBanner(WidgetTester tester) {
|
||||
return tester.widget<Container>(_containerFinder());
|
||||
Container _getContainerFromText(WidgetTester tester, String text) {
|
||||
return tester.widget<Container>(find.widgetWithText(Container, text).first);
|
||||
}
|
||||
|
||||
Finder _containerFinder() {
|
||||
|
@ -2282,6 +2282,63 @@ void main() {
|
||||
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 {
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: Scaffold(
|
||||
|
Loading…
x
Reference in New Issue
Block a user