blob: aab00384302ef2ea2ad036f6629876970b710526 [file] [log] [blame]
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/widgets.dart';
import 'package:flutter/rendering.dart';
import 'button_bar_theme.dart';
import 'button_theme.dart';
import 'dialog.dart';
/// An end-aligned row of buttons, laying out into a column if there is not
/// enough horizontal space.
///
/// Places the buttons horizontally according to the [buttonPadding]. The
/// children are laid out in a [Row] with [MainAxisAlignment.end]. When the
/// [Directionality] is [TextDirection.ltr], the button bar's children are
/// right justified and the last child becomes the rightmost child. When the
/// [Directionality] [TextDirection.rtl] the children are left justified and
/// the last child becomes the leftmost child.
///
/// If the button bar's width exceeds the maximum width constraint on the
/// widget, it aligns its buttons in a column. The key difference here
/// is that the [MainAxisAlignment] will then be treated as a
/// cross-axis/horizontal alignment. For example, if the buttons overflow and
/// [ButtonBar.alignment] was set to [MainAxisAlignment.start], the buttons would
/// align to the horizontal start of the button bar.
///
/// The [ButtonBar] can be configured with a [ButtonBarTheme]. For any null
/// property on the ButtonBar, the surrounding ButtonBarTheme's property
/// will be used instead. If the ButtonBarTheme's property is null
/// as well, the property will default to a value described in the field
/// documentation below.
///
/// The [children] are wrapped in a [ButtonTheme] that is a copy of the
/// surrounding ButtonTheme with the button properties overridden by the
/// properties of the ButtonBar as described above. These properties include
/// [buttonTextTheme], [buttonMinWidth], [buttonHeight], [buttonPadding],
/// and [buttonAlignedDropdown].
///
/// Used by [Dialog] to arrange the actions at the bottom of the dialog.
///
/// See also:
///
/// * [TextButton], a simple flat button without a shadow.
/// * [ElevatedButton], a filled button whose material elevates when pressed.
/// * [OutlinedButton], a [TextButton] with a border outline.
/// * [Card], at the bottom of which it is common to place a [ButtonBar].
/// * [Dialog], which uses a [ButtonBar] for its actions.
/// * [ButtonBarTheme], which configures the [ButtonBar].
class ButtonBar extends StatelessWidget {
/// Creates a button bar.
///
/// Both [buttonMinWidth] and [buttonHeight] must be non-negative if they
/// are not null.
const ButtonBar({
Key? key,
this.alignment,
this.mainAxisSize,
this.buttonTextTheme,
this.buttonMinWidth,
this.buttonHeight,
this.buttonPadding,
this.buttonAlignedDropdown,
this.layoutBehavior,
this.overflowDirection,
this.overflowButtonSpacing,
this.children = const <Widget>[],
}) : 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.
///
/// If null then it will use [ButtonBarThemeData.alignment]. If that is null,
/// it will default to [MainAxisAlignment.end].
final MainAxisAlignment? alignment;
/// How much horizontal space is available. See [Row.mainAxisSize].
///
/// If null then it will use the surrounding [ButtonBarThemeData.mainAxisSize].
/// If that is null, it will default to [MainAxisSize.max].
final MainAxisSize? mainAxisSize;
/// Overrides the surrounding [ButtonBarThemeData.buttonTextTheme] to define a
/// button's base colors, size, internal padding and shape.
///
/// If null then it will use the surrounding
/// [ButtonBarThemeData.buttonTextTheme]. If that is null, it will default to
/// [ButtonTextTheme.primary].
final ButtonTextTheme? buttonTextTheme;
/// Overrides the surrounding [ButtonThemeData.minWidth] to define a button's
/// minimum width.
///
/// If null then it will use the surrounding [ButtonBarThemeData.buttonMinWidth].
/// If that is null, it will default to 64.0 logical pixels.
final double? buttonMinWidth;
/// Overrides the surrounding [ButtonThemeData.height] to define a button's
/// minimum height.
///
/// If null then it will use the surrounding [ButtonBarThemeData.buttonHeight].
/// If that is null, it will default to 36.0 logical pixels.
final double? buttonHeight;
/// Overrides the surrounding [ButtonThemeData.padding] to define the padding
/// for a button's child (typically the button's label).
///
/// If null then it will use the surrounding [ButtonBarThemeData.buttonPadding].
/// If that is null, it will default to 8.0 logical pixels on the left
/// and right.
final EdgeInsetsGeometry? buttonPadding;
/// Overrides the surrounding [ButtonThemeData.alignedDropdown] to define whether
/// a [DropdownButton] menu's width will match the button's width.
///
/// If null then it will use the surrounding [ButtonBarThemeData.buttonAlignedDropdown].
/// If that is null, it will default to false.
final bool? buttonAlignedDropdown;
/// Defines whether a [ButtonBar] should size itself with a minimum size
/// constraint or with padding.
///
/// Overrides the surrounding [ButtonThemeData.layoutBehavior].
///
/// If null then it will use the surrounding [ButtonBarThemeData.layoutBehavior].
/// If that is null, it will default [ButtonBarLayoutBehavior.padded].
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
/// [ButtonBarThemeData.overflowDirection]. If that is null, it will
/// 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 [ElevatedButton] or [TextButton] widgets.
final List<Widget> children;
@override
Widget build(BuildContext context) {
final ButtonThemeData parentButtonTheme = ButtonTheme.of(context);
final ButtonBarThemeData barTheme = ButtonBarTheme.of(context);
final ButtonThemeData buttonTheme = parentButtonTheme.copyWith(
textTheme: buttonTextTheme ?? barTheme.buttonTextTheme ?? ButtonTextTheme.primary,
minWidth: buttonMinWidth ?? barTheme.buttonMinWidth ?? 64.0,
height: buttonHeight ?? barTheme.buttonHeight ?? 36.0,
padding: buttonPadding ?? barTheme.buttonPadding ?? const EdgeInsets.symmetric(horizontal: 8.0),
alignedDropdown: buttonAlignedDropdown ?? barTheme.buttonAlignedDropdown ?? false,
layoutBehavior: layoutBehavior ?? barTheme.layoutBehavior ?? ButtonBarLayoutBehavior.padded,
);
// We divide by 4.0 because we want half of the average of the left and right padding.
final double paddingUnit = buttonTheme.padding.horizontal / 4.0;
final Widget child = ButtonTheme.fromButtonThemeData(
data: buttonTheme,
child: _ButtonBarRow(
mainAxisAlignment: alignment ?? barTheme.alignment ?? MainAxisAlignment.end,
mainAxisSize: mainAxisSize ?? barTheme.mainAxisSize ?? MainAxisSize.max,
overflowDirection: overflowDirection ?? barTheme.overflowDirection ?? VerticalDirection.down,
children: children.map<Widget>((Widget child) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: paddingUnit),
child: child,
);
}).toList(),
overflowButtonSpacing: overflowButtonSpacing,
),
);
switch (buttonTheme.layoutBehavior) {
case ButtonBarLayoutBehavior.padded:
return Padding(
padding: EdgeInsets.symmetric(
vertical: 2.0 * paddingUnit,
horizontal: paddingUnit,
),
child: child,
);
case ButtonBarLayoutBehavior.constrained:
return Container(
padding: EdgeInsets.symmetric(horizontal: paddingUnit),
constraints: const BoxConstraints(minHeight: 52.0),
alignment: Alignment.center,
child: child,
);
}
}
}
/// Attempts to display buttons in a row, but displays them in a column if
/// there is not enough horizontal space.
///
/// It first attempts to lay out its buttons as though there were no
/// maximum width constraints on the widget. If the button bar's width is
/// less than the maximum width constraints of the widget, it then lays
/// out the widget as though it were placed in a [Row].
///
/// However, if the button bar's width exceeds the maximum width constraint on
/// the widget, it then aligns its buttons in a column. The key difference here
/// is that the [MainAxisAlignment] will then be treated as a
/// cross-axis/horizontal alignment. For example, if the buttons overflow and
/// [ButtonBar.alignment] was set to [MainAxisAlignment.start], the column of
/// buttons would align to the horizontal start of the button bar.
class _ButtonBarRow extends Flex {
/// Creates a button bar that attempts to display in a row, but displays in
/// a column if there is insufficient horizontal space.
_ButtonBarRow({
required List<Widget> children,
Axis direction = Axis.horizontal,
MainAxisSize mainAxisSize = MainAxisSize.max,
MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,
CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center,
TextDirection? textDirection,
VerticalDirection overflowDirection = VerticalDirection.down,
TextBaseline? textBaseline,
this.overflowButtonSpacing,
}) : super(
children: children,
direction: direction,
mainAxisSize: mainAxisSize,
mainAxisAlignment: mainAxisAlignment,
crossAxisAlignment: crossAxisAlignment,
textDirection: textDirection,
verticalDirection: overflowDirection,
textBaseline: textBaseline,
);
final double? overflowButtonSpacing;
@override
_RenderButtonBarRow createRenderObject(BuildContext context) {
return _RenderButtonBarRow(
direction: direction,
mainAxisAlignment: mainAxisAlignment,
mainAxisSize: mainAxisSize,
crossAxisAlignment: crossAxisAlignment,
textDirection: getEffectiveTextDirection(context)!,
verticalDirection: verticalDirection,
textBaseline: textBaseline,
overflowButtonSpacing: overflowButtonSpacing,
);
}
@override
void updateRenderObject(BuildContext context, covariant _RenderButtonBarRow renderObject) {
renderObject
..direction = direction
..mainAxisAlignment = mainAxisAlignment
..mainAxisSize = mainAxisSize
..crossAxisAlignment = crossAxisAlignment
..textDirection = getEffectiveTextDirection(context)
..verticalDirection = verticalDirection
..textBaseline = textBaseline
..overflowButtonSpacing = overflowButtonSpacing;
}
}
/// Attempts to display buttons in a row, but displays them in a column if
/// there is not enough horizontal space.
///
/// It first attempts to lay out its buttons as though there were no
/// maximum width constraints on the widget. If the button bar's width is
/// less than the maximum width constraints of the widget, it then lays
/// out the widget as though it were placed in a [Row].
///
/// However, if the button bar's width exceeds the maximum width constraint on
/// the widget, it then aligns its buttons in a column. The key difference here
/// is that the [MainAxisAlignment] will then be treated as a
/// cross-axis/horizontal alignment. For example, if the buttons overflow and
/// [ButtonBar.alignment] was set to [MainAxisAlignment.start], the buttons would
/// align to the horizontal start of the button bar.
class _RenderButtonBarRow extends RenderFlex {
/// Creates a button bar that attempts to display in a row, but displays in
/// a column if there is insufficient horizontal space.
_RenderButtonBarRow({
List<RenderBox>? children,
Axis direction = Axis.horizontal,
MainAxisSize mainAxisSize = MainAxisSize.max,
MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,
CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center,
required TextDirection textDirection,
VerticalDirection verticalDirection = VerticalDirection.down,
TextBaseline? textBaseline,
this.overflowButtonSpacing,
}) : assert(textDirection != null),
assert(overflowButtonSpacing == null || overflowButtonSpacing >= 0),
super(
children: children,
direction: direction,
mainAxisSize: mainAxisSize,
mainAxisAlignment: mainAxisAlignment,
crossAxisAlignment: crossAxisAlignment,
textDirection: textDirection,
verticalDirection: verticalDirection,
textBaseline: textBaseline,
);
bool _hasCheckedLayoutWidth = false;
double? overflowButtonSpacing;
@override
BoxConstraints get constraints {
if (_hasCheckedLayoutWidth)
return super.constraints;
return super.constraints.copyWith(maxWidth: double.infinity);
}
@override
Size computeDryLayout(BoxConstraints constraints) {
final Size size = super.computeDryLayout(constraints.copyWith(maxWidth: double.infinity));
if (size.width <= constraints.maxWidth) {
return super.computeDryLayout(constraints);
}
double currentHeight = 0.0;
RenderBox? child = firstChild;
while (child != null) {
final BoxConstraints childConstraints = constraints.copyWith(minWidth: 0.0);
final Size childSize = child.getDryLayout(childConstraints);
currentHeight += childSize.height;
child = childAfter(child);
if (overflowButtonSpacing != null && child != null)
currentHeight += overflowButtonSpacing!;
}
return constraints.constrain(Size(constraints.maxWidth, currentHeight));
}
@override
void performLayout() {
// Set check layout width to false in reload or update cases.
_hasCheckedLayoutWidth = false;
// Perform layout to ensure that button bar knows how wide it would
// ideally want to be.
super.performLayout();
_hasCheckedLayoutWidth = true;
// If the button bar is constrained by width and it overflows, set the
// buttons to align vertically. Otherwise, lay out the button bar
// horizontally.
if (size.width <= constraints.maxWidth) {
// A second performLayout is required to ensure that the original maximum
// width constraints are used. The original perform layout call assumes
// a maximum width constraint of infinity.
super.performLayout();
} else {
final BoxConstraints childConstraints = constraints.copyWith(minWidth: 0.0);
RenderBox? child;
double currentHeight = 0.0;
switch (verticalDirection) {
case VerticalDirection.down:
child = firstChild;
break;
case VerticalDirection.up:
child = lastChild;
break;
}
while (child != null) {
final FlexParentData childParentData = child.parentData! as FlexParentData;
// Lay out the child with the button bar's original constraints, but
// with minimum width set to zero.
child.layout(childConstraints, parentUsesSize: true);
// Set the cross axis alignment for the column to match the main axis
// alignment for a row. For [MainAxisAlignment.spaceAround],
// [MainAxisAlignment.spaceBetween] and [MainAxisAlignment.spaceEvenly]
// cases, use [MainAxisAlignment.start].
switch (textDirection!) {
case TextDirection.ltr:
switch (mainAxisAlignment) {
case MainAxisAlignment.center:
final double midpoint = (constraints.maxWidth - child.size.width) / 2.0;
childParentData.offset = Offset(midpoint, currentHeight);
break;
case MainAxisAlignment.end:
childParentData.offset = Offset(constraints.maxWidth - child.size.width, currentHeight);
break;
default:
childParentData.offset = Offset(0, currentHeight);
break;
}
break;
case TextDirection.rtl:
switch (mainAxisAlignment) {
case MainAxisAlignment.center:
final double midpoint = constraints.maxWidth / 2.0 - child.size.width / 2.0;
childParentData.offset = Offset(midpoint, currentHeight);
break;
case MainAxisAlignment.end:
childParentData.offset = Offset(0, currentHeight);
break;
default:
childParentData.offset = Offset(constraints.maxWidth - child.size.width, currentHeight);
break;
}
break;
}
currentHeight += child.size.height;
switch (verticalDirection) {
case VerticalDirection.down:
child = childParentData.nextSibling;
break;
case VerticalDirection.up:
child = childParentData.previousSibling;
break;
}
if (overflowButtonSpacing != null && child != null)
currentHeight += overflowButtonSpacing!;
}
size = constraints.constrain(Size(constraints.maxWidth, currentHeight));
}
}
}