diff --git a/packages/flutter/lib/src/material/scaffold.dart b/packages/flutter/lib/src/material/scaffold.dart index e7aae7db858..07e69bba701 100644 --- a/packages/flutter/lib/src/material/scaffold.dart +++ b/packages/flutter/lib/src/material/scaffold.dart @@ -2832,7 +2832,7 @@ class ScaffoldState extends State with TickerProviderStateMixin, Resto ?? themeData.snackBarTheme.behavior ?? SnackBarBehavior.fixed; isSnackBarFloating = snackBarBehavior == SnackBarBehavior.floating; - snackBarWidth = _messengerSnackBar?._widget.width; + snackBarWidth = _messengerSnackBar?._widget.width ?? themeData.snackBarTheme.width; _addIfNonNull( children, diff --git a/packages/flutter/lib/src/material/snack_bar.dart b/packages/flutter/lib/src/material/snack_bar.dart index d4a23825c0f..e56fc853275 100644 --- a/packages/flutter/lib/src/material/snack_bar.dart +++ b/packages/flutter/lib/src/material/snack_bar.dart @@ -293,8 +293,9 @@ class SnackBar extends StatefulWidget { /// 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. + /// If this property is null, then [SnackBarThemeData.width] of + /// [ThemeData.snackBarTheme] is used. If that is null, the snack bar will + /// take up the full device width less the margin. final double? width; /// The shape of the snack bar's [Material]. @@ -470,6 +471,7 @@ class _SnackBarState extends State { final TextStyle? contentTextStyle = snackBarTheme.contentTextStyle ?? ThemeData(brightness: brightness).textTheme.titleMedium; final SnackBarBehavior snackBarBehavior = widget.behavior ?? snackBarTheme.behavior ?? SnackBarBehavior.fixed; + final double? width = widget.width ?? snackBarTheme.width; assert((){ // Whether the behavior is set through the constructor or the theme, // assert that our other properties are configured properly. @@ -485,7 +487,7 @@ class _SnackBarState extends State { } } assert(widget.margin == null, message('Margin')); - assert(widget.width == null, message('Width')); + assert(width == null, message('Width')); } return true; }()); @@ -567,10 +569,10 @@ class _SnackBarState extends State { const double topMargin = 5.0; const double bottomMargin = 10.0; // If width is provided, do not include horizontal margins. - if (widget.width != null) { + if (width != null) { snackBar = Container( margin: const EdgeInsets.only(top: topMargin, bottom: bottomMargin), - width: widget.width, + width: width, child: snackBar, ); } else { diff --git a/packages/flutter/lib/src/material/snack_bar_theme.dart b/packages/flutter/lib/src/material/snack_bar_theme.dart index 33ab1faa57d..4e463788e00 100644 --- a/packages/flutter/lib/src/material/snack_bar_theme.dart +++ b/packages/flutter/lib/src/material/snack_bar_theme.dart @@ -60,8 +60,12 @@ class SnackBarThemeData with Diagnosticable { this.elevation, this.shape, this.behavior, - }) : assert(elevation == null || elevation >= 0.0); - + this.width, + }) : assert(elevation == null || elevation >= 0.0), + assert( + width == null || + (width != null && identical(behavior, SnackBarBehavior.floating)), + 'Width can only be set if behaviour is SnackBarBehavior.floating'); /// Default value for [SnackBar.backgroundColor]. /// /// If null, [SnackBar] defaults to dark grey: `Color(0xFF323232)`. @@ -104,6 +108,13 @@ class SnackBarThemeData with Diagnosticable { /// If null, [SnackBar] will default to [SnackBarBehavior.fixed]. final SnackBarBehavior? behavior; + /// Default value for [SnackBar.width]. + /// + /// If this property is null, then the snack bar will take up the full device + /// width less the margin. This value is only used when [behavior] is + /// [SnackBarBehavior.floating]. + final double? width; + /// Creates a copy of this object with the given fields replaced with the /// new values. SnackBarThemeData copyWith({ @@ -114,6 +125,7 @@ class SnackBarThemeData with Diagnosticable { double? elevation, ShapeBorder? shape, SnackBarBehavior? behavior, + double? width, }) { return SnackBarThemeData( backgroundColor: backgroundColor ?? this.backgroundColor, @@ -123,6 +135,7 @@ class SnackBarThemeData with Diagnosticable { elevation: elevation ?? this.elevation, shape: shape ?? this.shape, behavior: behavior ?? this.behavior, + width: width ?? this.width, ); } @@ -141,19 +154,21 @@ class SnackBarThemeData with Diagnosticable { elevation: lerpDouble(a?.elevation, b?.elevation, t), shape: ShapeBorder.lerp(a?.shape, b?.shape, t), behavior: t < 0.5 ? a?.behavior : b?.behavior, + width: lerpDouble(a?.width, b?.width, t), ); } @override int get hashCode => Object.hash( - backgroundColor, - actionTextColor, - disabledActionTextColor, - contentTextStyle, - elevation, - shape, - behavior, - ); + backgroundColor, + actionTextColor, + disabledActionTextColor, + contentTextStyle, + elevation, + shape, + behavior, + width, + ); @override bool operator ==(Object other) { @@ -170,7 +185,8 @@ class SnackBarThemeData with Diagnosticable { && other.contentTextStyle == contentTextStyle && other.elevation == elevation && other.shape == shape - && other.behavior == behavior; + && other.behavior == behavior + && other.width == width; } @override @@ -183,5 +199,6 @@ class SnackBarThemeData with Diagnosticable { properties.add(DoubleProperty('elevation', elevation, defaultValue: null)); properties.add(DiagnosticsProperty('shape', shape, defaultValue: null)); properties.add(DiagnosticsProperty('behavior', behavior, defaultValue: null)); + properties.add(DoubleProperty('width', width, defaultValue: null)); } } diff --git a/packages/flutter/test/material/snack_bar_test.dart b/packages/flutter/test/material/snack_bar_test.dart index 46e9db182e2..53a820a5fab 100644 --- a/packages/flutter/test/material/snack_bar_test.dart +++ b/packages/flutter/test/material/snack_bar_test.dart @@ -724,6 +724,93 @@ void main() { expect(snackBarBottomRight.dx, (800 + width) / 2); // Device width is 800. }); + testWidgets('Snackbar width can be customized from ThemeData', + (WidgetTester tester) async { + const double width = 200.0; + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + snackBarTheme: const SnackBarThemeData( + width: width, behavior: SnackBarBehavior.floating), + ), + home: Scaffold( + body: Builder( + builder: (BuildContext context) { + return GestureDetector( + onTap: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Feeling snackish'), + ), + ); + }, + 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 width customization takes preference of widget over theme', + (WidgetTester tester) async { + const double themeWidth = 200.0; + const double widgetWidth = 400.0; + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + snackBarTheme: const SnackBarThemeData( + width: themeWidth, behavior: SnackBarBehavior.floating), + ), + home: Scaffold( + body: Builder( + builder: (BuildContext context) { + return GestureDetector( + onTap: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Feeling super snackish'), + width: widgetWidth, + ), + ); + }, + 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 - widgetWidth) / 2); // Device width is 800. + expect(snackBarBottomRight.dx, (800 + widgetWidth) / 2); // Device width is 800. + }); + testWidgets('Snackbar labels can be colored', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( diff --git a/packages/flutter/test/material/snack_bar_theme_test.dart b/packages/flutter/test/material/snack_bar_theme_test.dart index 240e27b76e2..7162b5df3b5 100644 --- a/packages/flutter/test/material/snack_bar_theme_test.dart +++ b/packages/flutter/test/material/snack_bar_theme_test.dart @@ -21,9 +21,22 @@ void main() { expect(snackBarTheme.elevation, null); expect(snackBarTheme.shape, null); expect(snackBarTheme.behavior, null); + expect(snackBarTheme.width, null); }); - testWidgets('Default SnackBarThemeData debugFillProperties', (WidgetTester tester) async { + test( + 'SnackBarTheme throws assertion if width is provided with fixed behaviour', + () { + expect( + () => SnackBarThemeData( + behavior: SnackBarBehavior.fixed, + width: 300.0, + ), + throwsAssertionError); + }); + + testWidgets('Default SnackBarThemeData debugFillProperties', + (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const SnackBarThemeData().debugFillProperties(builder); @@ -45,6 +58,7 @@ void main() { elevation: 2.0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(2.0))), behavior: SnackBarBehavior.floating, + width: 400.0, ).debugFillProperties(builder); final List description = builder.properties @@ -60,6 +74,7 @@ void main() { 'elevation: 2.0', 'shape: RoundedRectangleBorder(BorderSide(width: 0.0, style: none), BorderRadius.circular(2.0))', 'behavior: SnackBarBehavior.floating', + 'width: 400.0', ]); }); @@ -145,6 +160,7 @@ void main() { const ShapeBorder shape = RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(9.0)), ); + const double snackBarWidth = 400.0; await tester.pumpWidget(MaterialApp( theme: ThemeData(snackBarTheme: _snackBarTheme()), @@ -155,6 +171,8 @@ void main() { onTap: () { ScaffoldMessenger.of(context).showSnackBar(SnackBar( backgroundColor: backgroundColor, + behavior: SnackBarBehavior.floating, + width: snackBarWidth, elevation: elevation, shape: shape, content: const Text('I am a snack bar.'), @@ -177,13 +195,20 @@ void main() { await tester.pump(); // start animation await tester.pump(const Duration(milliseconds: 750)); + final Finder materialFinder = _getSnackBarMaterialFinder(tester); final Material material = _getSnackBarMaterial(tester); - final RenderParagraph button = _getSnackBarActionTextRenderObject(tester, action); + final RenderParagraph button = + _getSnackBarActionTextRenderObject(tester, action); expect(material.color, backgroundColor); expect(material.elevation, elevation); expect(material.shape, shape); expect(button.text.style!.color, textColor); + // Assert width. + final Offset snackBarBottomLeft = tester.getBottomLeft(materialFinder.first); + final Offset snackBarBottomRight = tester.getBottomRight(materialFinder.first); + expect(snackBarBottomLeft.dx, (800 - snackBarWidth) / 2); // Device width is 800. + expect(snackBarBottomRight.dx, (800 + snackBarWidth) / 2); // Device width is 800. }); testWidgets('SnackBar theme behavior is correct for floating', (WidgetTester tester) async { @@ -376,10 +401,15 @@ SnackBarThemeData _snackBarTheme() { Material _getSnackBarMaterial(WidgetTester tester) { return tester.widget( - find.descendant( - of: find.byType(SnackBar), - matching: find.byType(Material), - ).first, + _getSnackBarMaterialFinder(tester).first, + ); +} + +Finder _getSnackBarMaterialFinder(WidgetTester tester) { + return find.descendant( + of: find.byType(SnackBar), + matching: find.byType(Material), + ); }