diff --git a/packages/flutter/lib/src/material/button_bar.dart b/packages/flutter/lib/src/material/button_bar.dart index abe0b57cf7..ddcfbac9bd 100644 --- a/packages/flutter/lib/src/material/button_bar.dart +++ b/packages/flutter/lib/src/material/button_bar.dart @@ -65,9 +65,11 @@ class ButtonBar extends StatelessWidget { this.buttonAlignedDropdown, this.layoutBehavior, this.overflowDirection, + this.overflowButtonSpacing, this.children = const [], }) : assert(buttonMinWidth == null || buttonMinWidth >= 0.0), assert(buttonHeight == null || buttonHeight >= 0.0), + assert(overflowButtonSpacing == null || overflowButtonSpacing >= 0.0), super(key: key); /// How the children should be placed along the horizontal axis. @@ -143,6 +145,22 @@ class ButtonBar extends StatelessWidget { /// default to [VerticalDirection.down]. final VerticalDirection overflowDirection; + /// The spacing between buttons when the button bar overflows. + /// + /// If the [children] do not fit into a single row, they are + /// arranged into a column. This parameter provides additional + /// vertical space in between buttons when it does overflow. + /// + /// Note that the button spacing may appear to be more than + /// the value provided. This is because most buttons adhere to the + /// [MaterialTapTargetSize] of 48px. So, even though a button + /// might visually be 36px in height, it might still take up to + /// 48px vertically. + /// + /// If null then no spacing will be added in between buttons in + /// an overflow state. + final double overflowButtonSpacing; + /// The buttons to arrange horizontally. /// /// Typically [RaisedButton] or [FlatButton] widgets. @@ -176,6 +194,7 @@ class ButtonBar extends StatelessWidget { child: child, ); }).toList(), + overflowButtonSpacing: overflowButtonSpacing, ), ); switch (buttonTheme.layoutBehavior) { @@ -226,6 +245,7 @@ class _ButtonBarRow extends Flex { TextDirection textDirection, VerticalDirection overflowDirection = VerticalDirection.down, TextBaseline textBaseline, + this.overflowButtonSpacing, }) : super( children: children, direction: direction, @@ -237,6 +257,8 @@ class _ButtonBarRow extends Flex { textBaseline: textBaseline, ); + final double overflowButtonSpacing; + @override _RenderButtonBarRow createRenderObject(BuildContext context) { return _RenderButtonBarRow( @@ -247,6 +269,7 @@ class _ButtonBarRow extends Flex { textDirection: getEffectiveTextDirection(context), verticalDirection: verticalDirection, textBaseline: textBaseline, + overflowButtonSpacing: overflowButtonSpacing, ); } @@ -259,7 +282,8 @@ class _ButtonBarRow extends Flex { ..crossAxisAlignment = crossAxisAlignment ..textDirection = getEffectiveTextDirection(context) ..verticalDirection = verticalDirection - ..textBaseline = textBaseline; + ..textBaseline = textBaseline + ..overflowButtonSpacing = overflowButtonSpacing; } } @@ -289,7 +313,9 @@ class _RenderButtonBarRow extends RenderFlex { @required TextDirection textDirection, VerticalDirection verticalDirection = VerticalDirection.down, TextBaseline textBaseline, + this.overflowButtonSpacing, }) : assert(textDirection != null), + assert(overflowButtonSpacing == null || overflowButtonSpacing >= 0), super( children: children, direction: direction, @@ -302,6 +328,7 @@ class _RenderButtonBarRow extends RenderFlex { ); bool _hasCheckedLayoutWidth = false; + double overflowButtonSpacing; @override BoxConstraints get constraints { @@ -391,6 +418,9 @@ class _RenderButtonBarRow extends RenderFlex { child = childParentData.previousSibling; break; } + + if (overflowButtonSpacing != null && child != null) + currentHeight += overflowButtonSpacing; } size = constraints.constrain(Size(constraints.maxWidth, currentHeight)); } diff --git a/packages/flutter/lib/src/material/dialog.dart b/packages/flutter/lib/src/material/dialog.dart index 211bf88f11..5ffa1b6770 100644 --- a/packages/flutter/lib/src/material/dialog.dart +++ b/packages/flutter/lib/src/material/dialog.dart @@ -20,6 +20,7 @@ import 'ink_well.dart'; import 'material.dart'; import 'material_localizations.dart'; import 'theme.dart'; +import 'theme_data.dart'; // Examples can assume: // enum Department { treasury, state } @@ -223,6 +224,7 @@ class AlertDialog extends StatelessWidget { this.actions, this.actionsPadding = EdgeInsets.zero, this.actionsOverflowDirection, + this.actionsOverflowButtonSpacing, this.buttonPadding, this.backgroundColor, this.elevation, @@ -342,6 +344,22 @@ class AlertDialog extends StatelessWidget { /// * [ButtonBar], which [actions] configures to lay itself out. final VerticalDirection actionsOverflowDirection; + /// The spacing between [actions] when the button bar overflows. + /// + /// If the widgets in [actions] do not fit into a single row, they are + /// arranged into a column. This parameter provides additional + /// vertical space in between buttons when it does overflow. + /// + /// Note that the button spacing may appear to be more than + /// the value provided. This is because most buttons adhere to the + /// [MaterialTapTargetSize] of 48px. So, even though a button + /// might visually be 36px in height, it might still take up to + /// 48px vertically. + /// + /// If null then no spacing will be added in between buttons in + /// an overflow state. + final double actionsOverflowButtonSpacing; + /// The padding that surrounds each button in [actions]. /// /// This is different from [actionsPadding], which defines the padding @@ -445,6 +463,7 @@ class AlertDialog extends StatelessWidget { child: ButtonBar( buttonPadding: buttonPadding, overflowDirection: actionsOverflowDirection, + overflowButtonSpacing: actionsOverflowButtonSpacing, children: actions, ), ); diff --git a/packages/flutter/test/material/button_bar_test.dart b/packages/flutter/test/material/button_bar_test.dart index 30f70c2ce0..014bb9509a 100644 --- a/packages/flutter/test/material/button_bar_test.dart +++ b/packages/flutter/test/material/button_bar_test.dart @@ -570,7 +570,60 @@ void main() { final Rect containerOneRect = tester.getRect(find.byKey(keyOne)); final Rect containerTwoRect = tester.getRect(find.byKey(keyTwo)); // Second [Container] should appear above first container. - expect(containerTwoRect.bottom, containerOneRect.top); + expect(containerTwoRect.bottom, lessThanOrEqualTo(containerOneRect.top)); + }, + ); + + testWidgets( + 'ButtonBar has no spacing by default when overflowing', + (WidgetTester tester) async { + final Key keyOne = UniqueKey(); + final Key keyTwo = UniqueKey(); + await tester.pumpWidget( + MaterialApp( + home: ButtonBar( + alignment: MainAxisAlignment.center, + // Set padding to zero to align buttons with edge of button bar. + buttonPadding: EdgeInsets.zero, + children: [ + Container(key: keyOne, height: 50.0, width: 500.0), + Container(key: keyTwo, height: 50.0, width: 500.0), + ], + ), + ), + ); + + final Rect containerOneRect = tester.getRect(find.byKey(keyOne)); + final Rect containerTwoRect = tester.getRect(find.byKey(keyTwo)); + expect(containerOneRect.bottom, containerTwoRect.top); + }, + ); + + testWidgets( + "ButtonBar's children respects overflowButtonSpacing when overflowing", + (WidgetTester tester) async { + final Key keyOne = UniqueKey(); + final Key keyTwo = UniqueKey(); + await tester.pumpWidget( + MaterialApp( + home: ButtonBar( + alignment: MainAxisAlignment.center, + // Set padding to zero to align buttons with edge of button bar. + buttonPadding: EdgeInsets.zero, + // Set the overflow button spacing to ensure add some space between + // buttons in an overflow case. + overflowButtonSpacing: 10.0, + children: [ + Container(key: keyOne, height: 50.0, width: 500.0), + Container(key: keyTwo, height: 50.0, width: 500.0), + ], + ), + ), + ); + + final Rect containerOneRect = tester.getRect(find.byKey(keyOne)); + final Rect containerTwoRect = tester.getRect(find.byKey(keyTwo)); + expect(containerOneRect.bottom, containerTwoRect.top - 10.0); }, ); }); diff --git a/packages/flutter/test/material/dialog_test.dart b/packages/flutter/test/material/dialog_test.dart index a6eb5d1c18..061b6cc4e6 100644 --- a/packages/flutter/test/material/dialog_test.dart +++ b/packages/flutter/test/material/dialog_test.dart @@ -557,7 +557,7 @@ void main() { ); // right }); - testWidgets('Dialogs can set the vertical direction of actions', (WidgetTester tester) async { + testWidgets('Dialogs can set the vertical direction of overflowing actions', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(); final GlobalKey key2 = GlobalKey(); @@ -589,7 +589,74 @@ void main() { final Rect buttonOneRect = tester.getRect(find.byKey(key1)); final Rect buttonTwoRect = tester.getRect(find.byKey(key2)); // Second [RaisedButton] should appear above the first. - expect(buttonTwoRect.bottom, buttonOneRect.top); + expect(buttonTwoRect.bottom, lessThanOrEqualTo(buttonOneRect.top)); + }); + + testWidgets('Dialogs have no spacing by default for overflowing actions', (WidgetTester tester) async { + final GlobalKey key1 = GlobalKey(); + final GlobalKey key2 = GlobalKey(); + + final AlertDialog dialog = AlertDialog( + title: const Text('title'), + content: const Text('content'), + actions: [ + RaisedButton( + key: key1, + onPressed: () {}, + child: const Text('Looooooooooooooong button 1'), + ), + RaisedButton( + key: key2, + onPressed: () {}, + child: const Text('Looooooooooooooong button 2'), + ), + ], + ); + + await tester.pumpWidget( + _buildAppWithDialog(dialog), + ); + + await tester.tap(find.text('X')); + await tester.pumpAndSettle(); + + final Rect buttonOneRect = tester.getRect(find.byKey(key1)); + final Rect buttonTwoRect = tester.getRect(find.byKey(key2)); + expect(buttonOneRect.bottom, buttonTwoRect.top); + }); + + testWidgets('Dialogs can set the button spacing of overflowing actions', (WidgetTester tester) async { + final GlobalKey key1 = GlobalKey(); + final GlobalKey key2 = GlobalKey(); + + final AlertDialog dialog = AlertDialog( + title: const Text('title'), + content: const Text('content'), + actions: [ + RaisedButton( + key: key1, + onPressed: () {}, + child: const Text('Looooooooooooooong button 1'), + ), + RaisedButton( + key: key2, + onPressed: () {}, + child: const Text('Looooooooooooooong button 2'), + ), + ], + actionsOverflowButtonSpacing: 10.0, + ); + + await tester.pumpWidget( + _buildAppWithDialog(dialog), + ); + + await tester.tap(find.text('X')); + await tester.pumpAndSettle(); + + final Rect buttonOneRect = tester.getRect(find.byKey(key1)); + final Rect buttonTwoRect = tester.getRect(find.byKey(key2)); + expect(buttonOneRect.bottom, buttonTwoRect.top - 10.0); }); testWidgets('Dialogs removes MediaQuery padding and view insets', (WidgetTester tester) async {