[Material] Allow for customizing Snack bar margin, padding, and width (#61180)
This commit is contained in:
@@ -409,6 +409,7 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
|
||||
@required this.floatingActionButtonMoveAnimationProgress,
|
||||
@required this.floatingActionButtonMotionAnimator,
|
||||
@required this.isSnackBarFloating,
|
||||
@required this.snackBarWidth,
|
||||
@required this.extendBody,
|
||||
@required this.extendBodyBehindAppBar,
|
||||
}) : assert(minInsets != null),
|
||||
@@ -432,6 +433,7 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
|
||||
final FloatingActionButtonAnimator floatingActionButtonMotionAnimator;
|
||||
|
||||
final bool isSnackBarFloating;
|
||||
final double snackBarWidth;
|
||||
|
||||
@override
|
||||
void performLayout(Size size) {
|
||||
@@ -563,8 +565,12 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
|
||||
}
|
||||
|
||||
if (hasChild(_ScaffoldSlot.snackBar)) {
|
||||
final bool hasCustomWidth = snackBarWidth != null && snackBarWidth < size.width;
|
||||
if (snackBarSize == Size.zero) {
|
||||
snackBarSize = layoutChild(_ScaffoldSlot.snackBar, fullWidthConstraints);
|
||||
snackBarSize = layoutChild(
|
||||
_ScaffoldSlot.snackBar,
|
||||
hasCustomWidth ? looseConstraints : fullWidthConstraints,
|
||||
);
|
||||
}
|
||||
|
||||
double snackBarYOffsetBase;
|
||||
@@ -574,7 +580,8 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
|
||||
snackBarYOffsetBase = contentBottom;
|
||||
}
|
||||
|
||||
positionChild(_ScaffoldSlot.snackBar, Offset(0.0, snackBarYOffsetBase - snackBarSize.height));
|
||||
final double xOffset = hasCustomWidth ? (size.width - snackBarWidth) / 2 : 0.0;
|
||||
positionChild(_ScaffoldSlot.snackBar, Offset(xOffset, snackBarYOffsetBase - snackBarSize.height));
|
||||
}
|
||||
|
||||
if (hasChild(_ScaffoldSlot.statusBar)) {
|
||||
@@ -2381,11 +2388,13 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
|
||||
}
|
||||
|
||||
bool isSnackBarFloating = false;
|
||||
double snackBarWidth;
|
||||
if (_snackBars.isNotEmpty) {
|
||||
final SnackBarBehavior snackBarBehavior = _snackBars.first._widget.behavior
|
||||
?? themeData.snackBarTheme.behavior
|
||||
?? SnackBarBehavior.fixed;
|
||||
isSnackBarFloating = snackBarBehavior == SnackBarBehavior.floating;
|
||||
snackBarWidth = _snackBars.first._widget.width;
|
||||
|
||||
_addIfNonNull(
|
||||
children,
|
||||
@@ -2541,6 +2550,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
|
||||
previousFloatingActionButtonLocation: _previousFloatingActionButtonLocation,
|
||||
textDirection: textDirection,
|
||||
isSnackBarFloating: isSnackBarFloating,
|
||||
snackBarWidth: snackBarWidth,
|
||||
),
|
||||
);
|
||||
}),
|
||||
|
||||
@@ -173,6 +173,9 @@ class SnackBar extends StatefulWidget {
|
||||
@required this.content,
|
||||
this.backgroundColor,
|
||||
this.elevation,
|
||||
this.margin,
|
||||
this.padding,
|
||||
this.width,
|
||||
this.shape,
|
||||
this.behavior,
|
||||
this.action,
|
||||
@@ -181,6 +184,18 @@ class SnackBar extends StatefulWidget {
|
||||
this.onVisible,
|
||||
}) : assert(elevation == null || elevation >= 0.0),
|
||||
assert(content != null),
|
||||
assert(
|
||||
margin == null || behavior == SnackBarBehavior.floating,
|
||||
'Margin can only be used with floating behavior',
|
||||
),
|
||||
assert(
|
||||
width == null || behavior == SnackBarBehavior.floating,
|
||||
'Width can only be used with floating behavior',
|
||||
),
|
||||
assert(
|
||||
width == null || margin == null,
|
||||
'Width and margin can not be used together',
|
||||
),
|
||||
assert(duration != null),
|
||||
super(key: key);
|
||||
|
||||
@@ -189,7 +204,7 @@ class SnackBar extends StatefulWidget {
|
||||
/// Typically a [Text] widget.
|
||||
final Widget content;
|
||||
|
||||
/// The Snackbar's background color. If not specified it will use
|
||||
/// The snack bar's background color. If not specified it will use
|
||||
/// [ThemeData.snackBarTheme.backgroundColor]. If that is not specified
|
||||
/// it will default to a dark variation of [ColorScheme.surface] for light
|
||||
/// themes, or [ColorScheme.onSurface] for dark themes.
|
||||
@@ -204,6 +219,34 @@ class SnackBar extends StatefulWidget {
|
||||
/// used, if that is also null, the default value is 6.0.
|
||||
final double elevation;
|
||||
|
||||
/// Empty space to surround the snack bar.
|
||||
///
|
||||
/// This property is only used when [behavior] is [SnackBarBehavior.floating].
|
||||
/// It can not be used if [width] is specified.
|
||||
///
|
||||
/// If this property is null, then the default is
|
||||
/// `EdgeInsets.fromLTRB(15.0, 5.0, 15.0, 10.0)`.
|
||||
final EdgeInsetsGeometry margin;
|
||||
|
||||
/// The amount of padding to apply to the snack bar's content and optional
|
||||
/// action.
|
||||
///
|
||||
/// If this property is null, then the default depends on the [behavior] and
|
||||
/// the presence of an [action]. The start padding is 24 if [behavior] is
|
||||
/// [SnackBarBehavior.fixed] and 16 if it is [SnackBarBehavior.floating]. If
|
||||
/// there is no [action], the same padding is added to the end.
|
||||
final EdgeInsetsGeometry padding;
|
||||
|
||||
/// The width of the snack bar.
|
||||
///
|
||||
/// If width is specified, the snack bar will be centered horizontally in the
|
||||
/// available space. This property is only used when [behavior] is
|
||||
/// [SnackBarBehavior.floating]. It can not be used if [margin] is specified.
|
||||
///
|
||||
/// If this property is null, then the snack bar will take up the full device
|
||||
/// width less the margin.
|
||||
final double width;
|
||||
|
||||
/// The shape of the snack bar's [Material].
|
||||
///
|
||||
/// Defines the snack bar's [Material.shape].
|
||||
@@ -272,6 +315,9 @@ class SnackBar extends StatefulWidget {
|
||||
content: content,
|
||||
backgroundColor: backgroundColor,
|
||||
elevation: elevation,
|
||||
margin: margin,
|
||||
padding: padding,
|
||||
width: width,
|
||||
shape: shape,
|
||||
behavior: behavior,
|
||||
action: action,
|
||||
@@ -365,7 +411,9 @@ class _SnackBarState extends State<SnackBar> {
|
||||
final TextStyle contentTextStyle = snackBarTheme.contentTextStyle ?? inverseTheme.textTheme.subtitle1;
|
||||
final SnackBarBehavior snackBarBehavior = widget.behavior ?? snackBarTheme.behavior ?? SnackBarBehavior.fixed;
|
||||
final bool isFloatingSnackBar = snackBarBehavior == SnackBarBehavior.floating;
|
||||
final double snackBarPadding = isFloatingSnackBar ? 16.0 : 24.0;
|
||||
final double horizontalPadding = isFloatingSnackBar ? 16.0 : 24.0;
|
||||
final EdgeInsetsGeometry padding = widget.padding
|
||||
?? EdgeInsetsDirectional.only(start: horizontalPadding, end: widget.action != null ? 0 : horizontalPadding);
|
||||
|
||||
final CurvedAnimation heightAnimation = CurvedAnimation(parent: widget.animation, curve: _snackBarHeightCurve);
|
||||
final CurvedAnimation fadeInAnimation = CurvedAnimation(parent: widget.animation, curve: _snackBarFadeInCurve);
|
||||
@@ -375,13 +423,11 @@ class _SnackBarState extends State<SnackBar> {
|
||||
reverseCurve: const Threshold(0.0),
|
||||
);
|
||||
|
||||
Widget snackBar = SafeArea(
|
||||
top: false,
|
||||
bottom: !isFloatingSnackBar,
|
||||
Widget snackBar = Padding(
|
||||
padding: padding,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
SizedBox(width: snackBarPadding),
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: _singleLineVerticalPadding),
|
||||
@@ -395,15 +441,20 @@ class _SnackBarState extends State<SnackBar> {
|
||||
ButtonTheme(
|
||||
textTheme: ButtonTextTheme.accent,
|
||||
minWidth: 64.0,
|
||||
padding: EdgeInsets.symmetric(horizontal: snackBarPadding),
|
||||
padding: EdgeInsets.symmetric(horizontal: horizontalPadding),
|
||||
child: widget.action,
|
||||
)
|
||||
else
|
||||
SizedBox(width: snackBarPadding),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (!isFloatingSnackBar) {
|
||||
snackBar = SafeArea(
|
||||
top: false,
|
||||
child: snackBar,
|
||||
);
|
||||
}
|
||||
|
||||
final double elevation = widget.elevation ?? snackBarTheme.elevation ?? 6.0;
|
||||
final Color backgroundColor = widget.backgroundColor ?? snackBarTheme.backgroundColor ?? inverseTheme.backgroundColor;
|
||||
final ShapeBorder shape = widget.shape
|
||||
@@ -426,8 +477,30 @@ class _SnackBarState extends State<SnackBar> {
|
||||
);
|
||||
|
||||
if (isFloatingSnackBar) {
|
||||
snackBar = Padding(
|
||||
padding: const EdgeInsets.fromLTRB(15.0, 5.0, 15.0, 10.0),
|
||||
const double topMargin = 5.0;
|
||||
const double bottomMargin = 10.0;
|
||||
// If width is provided, do not include horizontal margins.
|
||||
if (widget.width != null) {
|
||||
snackBar = Container(
|
||||
margin: const EdgeInsets.only(top: topMargin, bottom: bottomMargin),
|
||||
width: widget.width,
|
||||
child: snackBar,
|
||||
);
|
||||
} else {
|
||||
const double horizontalMargin = 15.0;
|
||||
snackBar = Padding(
|
||||
padding: widget.margin ?? const EdgeInsets.fromLTRB(
|
||||
horizontalMargin,
|
||||
topMargin,
|
||||
horizontalMargin,
|
||||
bottomMargin,
|
||||
),
|
||||
child: snackBar,
|
||||
);
|
||||
}
|
||||
snackBar = SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
child: snackBar,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -380,6 +380,129 @@ void main() {
|
||||
expect(renderModel.color, equals(darkTheme.colorScheme.onSurface));
|
||||
});
|
||||
|
||||
testWidgets('Snackbar margin can be customized', (WidgetTester tester) async {
|
||||
const double padding = 20.0;
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Builder(
|
||||
builder: (BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
Scaffold.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: const Text('I am a snack bar.'),
|
||||
margin: const EdgeInsets.all(padding),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text('X'),
|
||||
);
|
||||
}
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.text('X'));
|
||||
await tester.pump(); // start animation
|
||||
await tester.pump(const Duration(milliseconds: 750));
|
||||
|
||||
final Finder materialFinder = find.descendant(
|
||||
of: find.byType(SnackBar),
|
||||
matching: find.byType(Material),
|
||||
);
|
||||
final Offset snackBarBottomLeft = tester.getBottomLeft(materialFinder);
|
||||
final Offset snackBarBottomRight = tester.getBottomRight(materialFinder);
|
||||
expect(snackBarBottomLeft.dx, padding);
|
||||
expect(snackBarBottomLeft.dy, 600 - padding); // Device height is 600.
|
||||
expect(snackBarBottomRight.dx, 800 - padding); // Device width is 800.
|
||||
});
|
||||
|
||||
testWidgets('Snackbar padding can be customized', (WidgetTester tester) async {
|
||||
const double padding = 20.0;
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Builder(
|
||||
builder: (BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
Scaffold.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('I am a snack bar.'),
|
||||
padding: EdgeInsets.all(padding),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text('X'),
|
||||
);
|
||||
}
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.text('X'));
|
||||
await tester.pump(); // start animation
|
||||
await tester.pump(const Duration(milliseconds: 750));
|
||||
|
||||
final Finder textFinder = find.text('I am a snack bar.');
|
||||
final Finder materialFinder = find.descendant(
|
||||
of: find.byType(SnackBar),
|
||||
matching: find.byType(Material),
|
||||
);
|
||||
final Offset textBottomLeft = tester.getBottomLeft(textFinder);
|
||||
final Offset textTopRight = tester.getTopRight(textFinder);
|
||||
final Offset snackBarBottomLeft = tester.getBottomLeft(materialFinder);
|
||||
final Offset snackBarTopRight = tester.getTopRight(materialFinder);
|
||||
expect(textBottomLeft.dx - snackBarBottomLeft.dx, padding);
|
||||
expect(snackBarTopRight.dx - textTopRight.dx, padding);
|
||||
// The text is given a vertical padding of 14 already.
|
||||
expect(snackBarBottomLeft.dy - textBottomLeft.dy, padding + 14);
|
||||
expect(textTopRight.dy - snackBarTopRight.dy, padding + 14);
|
||||
});
|
||||
|
||||
testWidgets('Snackbar width can be customized', (WidgetTester tester) async {
|
||||
const double width = 200.0;
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Builder(
|
||||
builder: (BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
Scaffold.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: const Text('I am a snack bar.'),
|
||||
width: width,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text('X'),
|
||||
);
|
||||
}
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.text('X'));
|
||||
await tester.pump(); // start animation
|
||||
await tester.pump(const Duration(milliseconds: 750));
|
||||
|
||||
final Finder materialFinder = find.descendant(
|
||||
of: find.byType(SnackBar),
|
||||
matching: find.byType(Material),
|
||||
);
|
||||
final Offset snackBarBottomLeft = tester.getBottomLeft(materialFinder);
|
||||
final Offset snackBarBottomRight = tester.getBottomRight(materialFinder);
|
||||
expect(snackBarBottomLeft.dx, (800 - width) / 2); // Device width is 800.
|
||||
expect(snackBarBottomRight.dx, (800 + width) / 2); // Device width is 800.
|
||||
});
|
||||
|
||||
testWidgets('Snackbar labels can be colored', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
|
||||
Reference in New Issue
Block a user