427 lines
17 KiB
Dart
427 lines
17 KiB
Dart
// 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
|
|
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));
|
|
}
|
|
}
|
|
}
|