Unverified Commit b79efb8d authored by rami-a's avatar rami-a Committed by GitHub

[Material] Allow for customizing Snack bar margin, padding, and width (#61180)

parent cda6c27f
......@@ -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) {
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: const EdgeInsets.fromLTRB(15.0, 5.0, 15.0, 10.0),
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(
......
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