Commit 01742673 authored by Shi-Hao Hong's avatar Shi-Hao Hong Committed by Flutter GitHub Bot

ButtonBar.verticalDirection and AlertDialog.actionsVerticalDirection (#48728)

parent ad9476dc
...@@ -64,6 +64,7 @@ class ButtonBar extends StatelessWidget { ...@@ -64,6 +64,7 @@ class ButtonBar extends StatelessWidget {
this.buttonPadding, this.buttonPadding,
this.buttonAlignedDropdown, this.buttonAlignedDropdown,
this.layoutBehavior, this.layoutBehavior,
this.overflowDirection,
this.children = const <Widget>[], this.children = const <Widget>[],
}) : assert(buttonMinWidth == null || buttonMinWidth >= 0.0), }) : assert(buttonMinWidth == null || buttonMinWidth >= 0.0),
assert(buttonHeight == null || buttonHeight >= 0.0), assert(buttonHeight == null || buttonHeight >= 0.0),
...@@ -126,6 +127,22 @@ class ButtonBar extends StatelessWidget { ...@@ -126,6 +127,22 @@ class ButtonBar extends StatelessWidget {
/// If that is null, it will default [ButtonBarLayoutBehavior.padded]. /// If that is null, it will default [ButtonBarLayoutBehavior.padded].
final ButtonBarLayoutBehavior layoutBehavior; final ButtonBarLayoutBehavior layoutBehavior;
/// Defines the vertical direction of a [ButtonBar]'s children if it
/// overflows.
///
/// If [children] do not fit into a single row, then they
/// are arranged in a column. The first action is at the top of the
/// column if this property is set to [VerticalDirection.down], since it
/// "starts" at the top and "ends" at the bottom. On the other hand,
/// the first action will be at the bottom of the column if this
/// property is set to [VerticalDirection.up], since it "starts" at the
/// bottom and "ends" at the top.
///
/// If null then it will use the surrounding
/// [ButtonBarTheme.overflowDirection]. If that is null, it will
/// default to [VerticalDirection.down].
final VerticalDirection overflowDirection;
/// The buttons to arrange horizontally. /// The buttons to arrange horizontally.
/// ///
/// Typically [RaisedButton] or [FlatButton] widgets. /// Typically [RaisedButton] or [FlatButton] widgets.
...@@ -152,6 +169,7 @@ class ButtonBar extends StatelessWidget { ...@@ -152,6 +169,7 @@ class ButtonBar extends StatelessWidget {
child: _ButtonBarRow( child: _ButtonBarRow(
mainAxisAlignment: alignment ?? barTheme.alignment ?? MainAxisAlignment.end, mainAxisAlignment: alignment ?? barTheme.alignment ?? MainAxisAlignment.end,
mainAxisSize: mainAxisSize ?? barTheme.mainAxisSize ?? MainAxisSize.max, mainAxisSize: mainAxisSize ?? barTheme.mainAxisSize ?? MainAxisSize.max,
overflowDirection: overflowDirection ?? barTheme.overflowDirection ?? VerticalDirection.down,
children: children.map<Widget>((Widget child) { children: children.map<Widget>((Widget child) {
return Padding( return Padding(
padding: EdgeInsets.symmetric(horizontal: paddingUnit), padding: EdgeInsets.symmetric(horizontal: paddingUnit),
...@@ -206,7 +224,7 @@ class _ButtonBarRow extends Flex { ...@@ -206,7 +224,7 @@ class _ButtonBarRow extends Flex {
MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start, MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,
CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center, CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center,
TextDirection textDirection, TextDirection textDirection,
VerticalDirection verticalDirection = VerticalDirection.down, VerticalDirection overflowDirection = VerticalDirection.down,
TextBaseline textBaseline, TextBaseline textBaseline,
}) : super( }) : super(
children: children, children: children,
...@@ -215,7 +233,7 @@ class _ButtonBarRow extends Flex { ...@@ -215,7 +233,7 @@ class _ButtonBarRow extends Flex {
mainAxisAlignment: mainAxisAlignment, mainAxisAlignment: mainAxisAlignment,
crossAxisAlignment: crossAxisAlignment, crossAxisAlignment: crossAxisAlignment,
textDirection: textDirection, textDirection: textDirection,
verticalDirection: verticalDirection, verticalDirection: overflowDirection,
textBaseline: textBaseline, textBaseline: textBaseline,
); );
...@@ -312,8 +330,16 @@ class _RenderButtonBarRow extends RenderFlex { ...@@ -312,8 +330,16 @@ class _RenderButtonBarRow extends RenderFlex {
super.performLayout(); super.performLayout();
} else { } else {
final BoxConstraints childConstraints = constraints.copyWith(minWidth: 0.0); final BoxConstraints childConstraints = constraints.copyWith(minWidth: 0.0);
RenderBox child = firstChild; RenderBox child;
double currentHeight = 0.0; double currentHeight = 0.0;
switch (verticalDirection) {
case VerticalDirection.down:
child = firstChild;
break;
case VerticalDirection.up:
child = lastChild;
break;
}
while (child != null) { while (child != null) {
final FlexParentData childParentData = child.parentData as FlexParentData; final FlexParentData childParentData = child.parentData as FlexParentData;
...@@ -357,7 +383,14 @@ class _RenderButtonBarRow extends RenderFlex { ...@@ -357,7 +383,14 @@ class _RenderButtonBarRow extends RenderFlex {
break; break;
} }
currentHeight += child.size.height; currentHeight += child.size.height;
child = childParentData.nextSibling; switch (verticalDirection) {
case VerticalDirection.down:
child = childParentData.nextSibling;
break;
case VerticalDirection.up:
child = childParentData.previousSibling;
break;
}
} }
size = constraints.constrain(Size(constraints.maxWidth, currentHeight)); size = constraints.constrain(Size(constraints.maxWidth, currentHeight));
} }
......
...@@ -38,6 +38,7 @@ class ButtonBarThemeData extends Diagnosticable { ...@@ -38,6 +38,7 @@ class ButtonBarThemeData extends Diagnosticable {
this.buttonPadding, this.buttonPadding,
this.buttonAlignedDropdown, this.buttonAlignedDropdown,
this.layoutBehavior, this.layoutBehavior,
this.overflowDirection,
}) : assert(buttonMinWidth == null || buttonMinWidth >= 0.0), }) : assert(buttonMinWidth == null || buttonMinWidth >= 0.0),
assert(buttonHeight == null || buttonHeight >= 0.0); assert(buttonHeight == null || buttonHeight >= 0.0);
...@@ -97,6 +98,18 @@ class ButtonBarThemeData extends Diagnosticable { ...@@ -97,6 +98,18 @@ class ButtonBarThemeData extends Diagnosticable {
/// constraint or with padding. /// constraint or with padding.
final ButtonBarLayoutBehavior layoutBehavior; final ButtonBarLayoutBehavior layoutBehavior;
/// Defines the vertical direction of a [ButtonBar]'s children if it
/// overflows.
///
/// If the [ButtonBar]'s children do not fit into a single row, then they
/// are arranged in a column. The first action is at the top of the
/// column if this property is set to [VerticalDirection.down], since it
/// "starts" at the top and "ends" at the bottom. On the other hand,
/// the first action will be at the bottom of the column if this
/// property is set to [VerticalDirection.up], since it "starts" at the
/// bottom and "ends" at the top.
final VerticalDirection overflowDirection;
/// Creates a copy of this object but with the given fields replaced with the /// Creates a copy of this object but with the given fields replaced with the
/// new values. /// new values.
ButtonBarThemeData copyWith({ ButtonBarThemeData copyWith({
...@@ -108,6 +121,7 @@ class ButtonBarThemeData extends Diagnosticable { ...@@ -108,6 +121,7 @@ class ButtonBarThemeData extends Diagnosticable {
EdgeInsetsGeometry buttonPadding, EdgeInsetsGeometry buttonPadding,
bool buttonAlignedDropdown, bool buttonAlignedDropdown,
ButtonBarLayoutBehavior layoutBehavior, ButtonBarLayoutBehavior layoutBehavior,
VerticalDirection overflowDirection,
}) { }) {
return ButtonBarThemeData( return ButtonBarThemeData(
alignment: alignment ?? this.alignment, alignment: alignment ?? this.alignment,
...@@ -118,6 +132,7 @@ class ButtonBarThemeData extends Diagnosticable { ...@@ -118,6 +132,7 @@ class ButtonBarThemeData extends Diagnosticable {
buttonPadding: buttonPadding ?? this.buttonPadding, buttonPadding: buttonPadding ?? this.buttonPadding,
buttonAlignedDropdown: buttonAlignedDropdown ?? this.buttonAlignedDropdown, buttonAlignedDropdown: buttonAlignedDropdown ?? this.buttonAlignedDropdown,
layoutBehavior: layoutBehavior ?? this.layoutBehavior, layoutBehavior: layoutBehavior ?? this.layoutBehavior,
overflowDirection: overflowDirection ?? this.overflowDirection,
); );
} }
...@@ -139,6 +154,7 @@ class ButtonBarThemeData extends Diagnosticable { ...@@ -139,6 +154,7 @@ class ButtonBarThemeData extends Diagnosticable {
buttonPadding: EdgeInsetsGeometry.lerp(a?.buttonPadding, b?.buttonPadding, t), buttonPadding: EdgeInsetsGeometry.lerp(a?.buttonPadding, b?.buttonPadding, t),
buttonAlignedDropdown: t < 0.5 ? a.buttonAlignedDropdown : b.buttonAlignedDropdown, buttonAlignedDropdown: t < 0.5 ? a.buttonAlignedDropdown : b.buttonAlignedDropdown,
layoutBehavior: t < 0.5 ? a.layoutBehavior : b.layoutBehavior, layoutBehavior: t < 0.5 ? a.layoutBehavior : b.layoutBehavior,
overflowDirection: t < 0.5 ? a.overflowDirection : b.overflowDirection,
); );
} }
...@@ -153,6 +169,7 @@ class ButtonBarThemeData extends Diagnosticable { ...@@ -153,6 +169,7 @@ class ButtonBarThemeData extends Diagnosticable {
buttonPadding, buttonPadding,
buttonAlignedDropdown, buttonAlignedDropdown,
layoutBehavior, layoutBehavior,
overflowDirection,
); );
} }
...@@ -170,7 +187,8 @@ class ButtonBarThemeData extends Diagnosticable { ...@@ -170,7 +187,8 @@ class ButtonBarThemeData extends Diagnosticable {
&& other.buttonHeight == buttonHeight && other.buttonHeight == buttonHeight
&& other.buttonPadding == buttonPadding && other.buttonPadding == buttonPadding
&& other.buttonAlignedDropdown == buttonAlignedDropdown && other.buttonAlignedDropdown == buttonAlignedDropdown
&& other.layoutBehavior == layoutBehavior; && other.layoutBehavior == layoutBehavior
&& other.overflowDirection == overflowDirection;
} }
@override @override
...@@ -188,6 +206,7 @@ class ButtonBarThemeData extends Diagnosticable { ...@@ -188,6 +206,7 @@ class ButtonBarThemeData extends Diagnosticable {
ifTrue: 'dropdown width matches button', ifTrue: 'dropdown width matches button',
defaultValue: null)); defaultValue: null));
properties.add(DiagnosticsProperty<ButtonBarLayoutBehavior>('layoutBehavior', layoutBehavior, defaultValue: null)); properties.add(DiagnosticsProperty<ButtonBarLayoutBehavior>('layoutBehavior', layoutBehavior, defaultValue: null));
properties.add(DiagnosticsProperty<VerticalDirection>('overflowDirection', overflowDirection, defaultValue: null));
} }
} }
......
...@@ -222,6 +222,7 @@ class AlertDialog extends StatelessWidget { ...@@ -222,6 +222,7 @@ class AlertDialog extends StatelessWidget {
this.contentTextStyle, this.contentTextStyle,
this.actions, this.actions,
this.actionsPadding = EdgeInsets.zero, this.actionsPadding = EdgeInsets.zero,
this.actionsOverflowDirection,
this.buttonPadding, this.buttonPadding,
this.backgroundColor, this.backgroundColor,
this.elevation, this.elevation,
...@@ -315,8 +316,32 @@ class AlertDialog extends StatelessWidget { ...@@ -315,8 +316,32 @@ class AlertDialog extends StatelessWidget {
/// ) /// )
/// ``` /// ```
/// {@end-tool} /// {@end-tool}
///
/// See also:
///
/// * [ButtonBar], which [actions] configures to lay itself out.
final EdgeInsetsGeometry actionsPadding; final EdgeInsetsGeometry actionsPadding;
/// The vertical direction of [actions] if the children overflow
/// horizontally.
///
/// If the dialog's [actions] do not fit into a single row, then they
/// are arranged in a column. The first action is at the top of the
/// column if this property is set to [VerticalDirection.down], since it
/// "starts" at the top and "ends" at the bottom. On the other hand,
/// the first action will be at the bottom of the column if this
/// property is set to [VerticalDirection.up], since it "starts" at the
/// bottom and "ends" at the top.
///
/// If null then it will use the surrounding
/// [ButtonBarTheme.overflowDirection]. If that is null, it will
/// default to [VerticalDirection.down].
///
/// See also:
///
/// * [ButtonBar], which [actions] configures to lay itself out.
final VerticalDirection actionsOverflowDirection;
/// The padding that surrounds each button in [actions]. /// The padding that surrounds each button in [actions].
/// ///
/// This is different from [actionsPadding], which defines the padding /// This is different from [actionsPadding], which defines the padding
...@@ -325,6 +350,10 @@ class AlertDialog extends StatelessWidget { ...@@ -325,6 +350,10 @@ class AlertDialog extends StatelessWidget {
/// If this property is null, then it will use the surrounding /// If this property is null, then it will use the surrounding
/// [ButtonBarTheme.buttonPadding]. If that is null, it will default to /// [ButtonBarTheme.buttonPadding]. If that is null, it will default to
/// 8.0 logical pixels on the left and right. /// 8.0 logical pixels on the left and right.
///
/// See also:
///
/// * [ButtonBar], which [actions] configures to lay itself out.
final EdgeInsetsGeometry buttonPadding; final EdgeInsetsGeometry buttonPadding;
/// {@macro flutter.material.dialog.backgroundColor} /// {@macro flutter.material.dialog.backgroundColor}
...@@ -415,6 +444,7 @@ class AlertDialog extends StatelessWidget { ...@@ -415,6 +444,7 @@ class AlertDialog extends StatelessWidget {
padding: actionsPadding, padding: actionsPadding,
child: ButtonBar( child: ButtonBar(
buttonPadding: buttonPadding, buttonPadding: buttonPadding,
overflowDirection: actionsOverflowDirection,
children: actions, children: actions,
), ),
); );
......
...@@ -544,5 +544,34 @@ void main() { ...@@ -544,5 +544,34 @@ void main() {
expect(containerOneRect.left, buttonBarRect.left); expect(containerOneRect.left, buttonBarRect.left);
}, },
); );
testWidgets(
"ButtonBar's children respects verticalDirection 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 vertical direction to start from the bottom and lay
// out upwards.
overflowDirection: VerticalDirection.up,
children: <Widget>[
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));
// Second [Container] should appear above first container.
expect(containerTwoRect.bottom, containerOneRect.top);
},
);
}); });
} }
...@@ -18,6 +18,7 @@ void main() { ...@@ -18,6 +18,7 @@ void main() {
expect(buttonBarTheme.buttonPadding, null); expect(buttonBarTheme.buttonPadding, null);
expect(buttonBarTheme.buttonAlignedDropdown, null); expect(buttonBarTheme.buttonAlignedDropdown, null);
expect(buttonBarTheme.layoutBehavior, null); expect(buttonBarTheme.layoutBehavior, null);
expect(buttonBarTheme.overflowDirection, null);
}); });
test('ThemeData uses default ButtonBarThemeData', () { test('ThemeData uses default ButtonBarThemeData', () {
...@@ -39,6 +40,7 @@ void main() { ...@@ -39,6 +40,7 @@ void main() {
buttonPadding: EdgeInsets.symmetric(vertical: 5.0), buttonPadding: EdgeInsets.symmetric(vertical: 5.0),
buttonAlignedDropdown: false, buttonAlignedDropdown: false,
layoutBehavior: ButtonBarLayoutBehavior.padded, layoutBehavior: ButtonBarLayoutBehavior.padded,
overflowDirection: VerticalDirection.down,
); );
const ButtonBarThemeData barThemeAccent = ButtonBarThemeData( const ButtonBarThemeData barThemeAccent = ButtonBarThemeData(
alignment: MainAxisAlignment.center, alignment: MainAxisAlignment.center,
...@@ -49,6 +51,7 @@ void main() { ...@@ -49,6 +51,7 @@ void main() {
buttonPadding: EdgeInsets.symmetric(horizontal: 10.0), buttonPadding: EdgeInsets.symmetric(horizontal: 10.0),
buttonAlignedDropdown: true, buttonAlignedDropdown: true,
layoutBehavior: ButtonBarLayoutBehavior.constrained, layoutBehavior: ButtonBarLayoutBehavior.constrained,
overflowDirection: VerticalDirection.up,
); );
final ButtonBarThemeData lerp = ButtonBarThemeData.lerp(barThemePrimary, barThemeAccent, 0.5); final ButtonBarThemeData lerp = ButtonBarThemeData.lerp(barThemePrimary, barThemeAccent, 0.5);
...@@ -60,6 +63,7 @@ void main() { ...@@ -60,6 +63,7 @@ void main() {
expect(lerp.buttonPadding, equals(const EdgeInsets.fromLTRB(5.0, 2.5, 5.0, 2.5))); expect(lerp.buttonPadding, equals(const EdgeInsets.fromLTRB(5.0, 2.5, 5.0, 2.5)));
expect(lerp.buttonAlignedDropdown, isTrue); expect(lerp.buttonAlignedDropdown, isTrue);
expect(lerp.layoutBehavior, equals(ButtonBarLayoutBehavior.constrained)); expect(lerp.layoutBehavior, equals(ButtonBarLayoutBehavior.constrained));
expect(lerp.overflowDirection, equals(VerticalDirection.up));
}); });
testWidgets('Default ButtonBarThemeData debugFillProperties', (WidgetTester tester) async { testWidgets('Default ButtonBarThemeData debugFillProperties', (WidgetTester tester) async {
...@@ -85,6 +89,7 @@ void main() { ...@@ -85,6 +89,7 @@ void main() {
buttonPadding: EdgeInsets.symmetric(horizontal: 7.3), buttonPadding: EdgeInsets.symmetric(horizontal: 7.3),
buttonAlignedDropdown: true, buttonAlignedDropdown: true,
layoutBehavior: ButtonBarLayoutBehavior.constrained, layoutBehavior: ButtonBarLayoutBehavior.constrained,
overflowDirection: VerticalDirection.up,
).debugFillProperties(builder); ).debugFillProperties(builder);
final List<String> description = builder.properties final List<String> description = builder.properties
...@@ -101,6 +106,7 @@ void main() { ...@@ -101,6 +106,7 @@ void main() {
'padding: EdgeInsets(7.3, 0.0, 7.3, 0.0)', 'padding: EdgeInsets(7.3, 0.0, 7.3, 0.0)',
'dropdown width matches button', 'dropdown width matches button',
'layoutBehavior: ButtonBarLayoutBehavior.constrained', 'layoutBehavior: ButtonBarLayoutBehavior.constrained',
'overflowDirection: VerticalDirection.up',
]); ]);
}); });
......
...@@ -531,6 +531,41 @@ void main() { ...@@ -531,6 +531,41 @@ void main() {
); // right ); // right
}); });
testWidgets('Dialogs can set the vertical direction of 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: <Widget>[
RaisedButton(
key: key1,
onPressed: () {},
child: const Text('Looooooooooooooong button 1'),
),
RaisedButton(
key: key2,
onPressed: () {},
child: const Text('Looooooooooooooong button 2'),
),
],
actionsOverflowDirection: VerticalDirection.up,
);
await tester.pumpWidget(
_appWithAlertDialog(tester, 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));
// Second [RaisedButton] should appear above the first.
expect(buttonTwoRect.bottom, buttonOneRect.top);
});
testWidgets('Dialogs removes MediaQuery padding and view insets', (WidgetTester tester) async { testWidgets('Dialogs removes MediaQuery padding and view insets', (WidgetTester tester) async {
BuildContext outerContext; BuildContext outerContext;
BuildContext routeContext; BuildContext routeContext;
......
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