button_bar.dart 17.3 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'package:flutter/rendering.dart';
6
import 'package:flutter/widgets.dart';
7

8
import 'button_bar_theme.dart';
9
import 'button_theme.dart';
10 11
import 'dialog.dart';

12 13
/// An end-aligned row of buttons, laying out into a column if there is not
/// enough horizontal space.
14
///
15 16 17 18 19 20 21
/// 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.
///
22 23 24 25
/// 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
Dan Field's avatar
Dan Field committed
26
/// [ButtonBar.alignment] was set to [MainAxisAlignment.start], the buttons would
27 28
/// align to the horizontal start of the button bar.
///
29 30 31 32 33 34 35 36 37 38 39
/// 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].
40 41 42 43 44
///
/// Used by [Dialog] to arrange the actions at the bottom of the dialog.
///
/// See also:
///
45 46 47
///  * [TextButton], a simple flat button without a shadow.
///  * [ElevatedButton], a filled button whose material elevates when pressed.
///  * [OutlinedButton], a [TextButton] with a border outline.
48 49
///  * [Card], at the bottom of which it is common to place a [ButtonBar].
///  * [Dialog], which uses a [ButtonBar] for its actions.
50
///  * [ButtonBarTheme], which configures the [ButtonBar].
51 52 53
class ButtonBar extends StatelessWidget {
  /// Creates a button bar.
  ///
54 55
  /// Both [buttonMinWidth] and [buttonHeight] must be non-negative if they
  /// are not null.
56
  const ButtonBar({
57
    super.key,
58 59 60 61 62 63 64 65
    this.alignment,
    this.mainAxisSize,
    this.buttonTextTheme,
    this.buttonMinWidth,
    this.buttonHeight,
    this.buttonPadding,
    this.buttonAlignedDropdown,
    this.layoutBehavior,
66
    this.overflowDirection,
67
    this.overflowButtonSpacing,
68
    this.children = const <Widget>[],
69 70
  }) : assert(buttonMinWidth == null || buttonMinWidth >= 0.0),
       assert(buttonHeight == null || buttonHeight >= 0.0),
71
       assert(overflowButtonSpacing == null || overflowButtonSpacing >= 0.0);
72 73

  /// How the children should be placed along the horizontal axis.
74
  ///
75
  /// If null then it will use [ButtonBarThemeData.alignment]. If that is null,
76
  /// it will default to [MainAxisAlignment.end].
77
  final MainAxisAlignment? alignment;
78

79
  /// How much horizontal space is available. See [Row.mainAxisSize].
80
  ///
81
  /// If null then it will use the surrounding [ButtonBarThemeData.mainAxisSize].
82
  /// If that is null, it will default to [MainAxisSize.max].
83
  final MainAxisSize? mainAxisSize;
84

85 86
  /// Overrides the surrounding [ButtonBarThemeData.buttonTextTheme] to define a
  /// button's base colors, size, internal padding and shape.
87
  ///
88 89 90
  /// If null then it will use the surrounding
  /// [ButtonBarThemeData.buttonTextTheme]. If that is null, it will default to
  /// [ButtonTextTheme.primary].
91
  final ButtonTextTheme? buttonTextTheme;
92 93 94 95

  /// Overrides the surrounding [ButtonThemeData.minWidth] to define a button's
  /// minimum width.
  ///
96
  /// If null then it will use the surrounding [ButtonBarThemeData.buttonMinWidth].
97
  /// If that is null, it will default to 64.0 logical pixels.
98
  final double? buttonMinWidth;
99 100 101 102

  /// Overrides the surrounding [ButtonThemeData.height] to define a button's
  /// minimum height.
  ///
103
  /// If null then it will use the surrounding [ButtonBarThemeData.buttonHeight].
104
  /// If that is null, it will default to 36.0 logical pixels.
105
  final double? buttonHeight;
106 107 108 109

  /// Overrides the surrounding [ButtonThemeData.padding] to define the padding
  /// for a button's child (typically the button's label).
  ///
110
  /// If null then it will use the surrounding [ButtonBarThemeData.buttonPadding].
111 112
  /// If that is null, it will default to 8.0 logical pixels on the left
  /// and right.
113
  final EdgeInsetsGeometry? buttonPadding;
114 115 116 117

  /// Overrides the surrounding [ButtonThemeData.alignedDropdown] to define whether
  /// a [DropdownButton] menu's width will match the button's width.
  ///
118
  /// If null then it will use the surrounding [ButtonBarThemeData.buttonAlignedDropdown].
119
  /// If that is null, it will default to false.
120
  final bool? buttonAlignedDropdown;
121 122 123 124 125 126

  /// Defines whether a [ButtonBar] should size itself with a minimum size
  /// constraint or with padding.
  ///
  /// Overrides the surrounding [ButtonThemeData.layoutBehavior].
  ///
127
  /// If null then it will use the surrounding [ButtonBarThemeData.layoutBehavior].
128
  /// If that is null, it will default [ButtonBarLayoutBehavior.padded].
129
  final ButtonBarLayoutBehavior? layoutBehavior;
130

131 132 133 134 135 136 137 138 139 140 141 142
  /// 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
143
  /// [ButtonBarThemeData.overflowDirection]. If that is null, it will
144
  /// default to [VerticalDirection.down].
145
  final VerticalDirection? overflowDirection;
146

147 148
  /// The spacing between buttons when the button bar overflows.
  ///
149 150 151 152 153 154 155 156
  /// 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.
  ///
  /// 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.
157 158 159
  ///
  /// If null then no spacing will be added in between buttons in
  /// an overflow state.
160
  final double? overflowButtonSpacing;
161

162 163
  /// The buttons to arrange horizontally.
  ///
164
  /// Typically [ElevatedButton] or [TextButton] widgets.
165 166 167 168
  final List<Widget> children;

  @override
  Widget build(BuildContext context) {
169 170 171 172 173 174 175 176 177 178 179 180
    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,
    );

181
    // We divide by 4.0 because we want half of the average of the left and right padding.
182
    final double paddingUnit = buttonTheme.padding.horizontal / 4.0;
183 184
    final Widget child = ButtonTheme.fromButtonThemeData(
      data: buttonTheme,
185
      child: _ButtonBarRow(
186 187
        mainAxisAlignment: alignment ?? barTheme.alignment ?? MainAxisAlignment.end,
        mainAxisSize: mainAxisSize ?? barTheme.mainAxisSize ?? MainAxisSize.max,
188
        overflowDirection: overflowDirection ?? barTheme.overflowDirection ?? VerticalDirection.down,
189
        overflowButtonSpacing: overflowButtonSpacing,
190 191 192 193 194 195 196
        children: children.map<Widget>((Widget child) {
          return Padding(
            padding: EdgeInsets.symmetric(horizontal: paddingUnit),
            child: child,
          );
        }).toList(),
      ),
197
    );
198 199 200 201 202
    switch (buttonTheme.layoutBehavior) {
      case ButtonBarLayoutBehavior.padded:
        return Padding(
          padding: EdgeInsets.symmetric(
            vertical: 2.0 * paddingUnit,
203
            horizontal: paddingUnit,
204 205 206 207 208 209 210 211 212 213 214
          ),
          child: child,
        );
      case ButtonBarLayoutBehavior.constrained:
        return Container(
          padding: EdgeInsets.symmetric(horizontal: paddingUnit),
          constraints: const BoxConstraints(minHeight: 52.0),
          alignment: Alignment.center,
          child: child,
        );
    }
215 216
  }
}
217 218 219 220 221

/// 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
222
/// maximum width constraints on the widget. If the button bar's width is
223 224 225 226 227 228 229
/// 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
230
/// [ButtonBar.alignment] was set to [MainAxisAlignment.start], the column of
231 232 233 234
/// 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.
235
  const _ButtonBarRow({
236 237 238
    required super.children,
    super.mainAxisSize,
    super.mainAxisAlignment,
239
    VerticalDirection overflowDirection = VerticalDirection.down,
240
    this.overflowButtonSpacing,
241
  }) : super(
242
    direction: Axis.horizontal,
243
    verticalDirection: overflowDirection,
244 245
  );

246
  final double? overflowButtonSpacing;
247

248 249 250 251 252 253 254
  @override
  _RenderButtonBarRow createRenderObject(BuildContext context) {
    return _RenderButtonBarRow(
      direction: direction,
      mainAxisAlignment: mainAxisAlignment,
      mainAxisSize: mainAxisSize,
      crossAxisAlignment: crossAxisAlignment,
255
      textDirection: getEffectiveTextDirection(context)!,
256 257
      verticalDirection: verticalDirection,
      textBaseline: textBaseline,
258
      overflowButtonSpacing: overflowButtonSpacing,
259 260 261 262 263 264 265 266 267 268 269 270
    );
  }

  @override
  void updateRenderObject(BuildContext context, covariant _RenderButtonBarRow renderObject) {
    renderObject
      ..direction = direction
      ..mainAxisAlignment = mainAxisAlignment
      ..mainAxisSize = mainAxisSize
      ..crossAxisAlignment = crossAxisAlignment
      ..textDirection = getEffectiveTextDirection(context)
      ..verticalDirection = verticalDirection
271 272
      ..textBaseline = textBaseline
      ..overflowButtonSpacing = overflowButtonSpacing;
273 274 275 276 277 278 279
  }
}

/// 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
280
/// maximum width constraints on the widget. If the button bar's width is
281 282 283 284 285 286 287
/// 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
288
/// [ButtonBar.alignment] was set to [MainAxisAlignment.start], the buttons would
289 290 291 292 293
/// 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({
294 295 296 297 298 299 300
    super.direction,
    super.mainAxisSize,
    super.mainAxisAlignment,
    super.crossAxisAlignment,
    required TextDirection super.textDirection,
    super.verticalDirection,
    super.textBaseline,
301
    this.overflowButtonSpacing,
302
  }) : assert(overflowButtonSpacing == null || overflowButtonSpacing >= 0);
303 304

  bool _hasCheckedLayoutWidth = false;
305
  double? overflowButtonSpacing;
306 307 308

  @override
  BoxConstraints get constraints {
309
    if (_hasCheckedLayoutWidth) {
310
      return super.constraints;
311
    }
312
    return super.constraints.copyWith(maxWidth: double.infinity);
313 314
  }

315 316 317 318 319 320 321 322 323 324 325 326 327
  @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);
328
      if (overflowButtonSpacing != null && child != null) {
329
        currentHeight += overflowButtonSpacing!;
330
      }
331 332 333 334
    }
    return constraints.constrain(Size(constraints.maxWidth, currentHeight));
  }

335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354
  @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);
355
      RenderBox? child;
356
      double currentHeight = 0.0;
357 358 359 360 361 362
      switch (verticalDirection) {
        case VerticalDirection.down:
          child = firstChild;
        case VerticalDirection.up:
          child = lastChild;
      }
363 364

      while (child != null) {
365
        final FlexParentData childParentData = child.parentData! as FlexParentData;
366 367 368 369 370 371

        // 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
372 373 374
        // alignment for a row. For [MainAxisAlignment.spaceAround],
        // [MainAxisAlignment.spaceBetween] and [MainAxisAlignment.spaceEvenly]
        // cases, use [MainAxisAlignment.start].
375
        switch (textDirection!) {
376 377 378 379 380 381 382
          case TextDirection.ltr:
            switch (mainAxisAlignment) {
              case MainAxisAlignment.center:
                final double midpoint = (constraints.maxWidth - child.size.width) / 2.0;
                childParentData.offset = Offset(midpoint, currentHeight);
              case MainAxisAlignment.end:
                childParentData.offset = Offset(constraints.maxWidth - child.size.width, currentHeight);
383 384 385 386
              case MainAxisAlignment.spaceAround:
              case MainAxisAlignment.spaceBetween:
              case MainAxisAlignment.spaceEvenly:
              case MainAxisAlignment.start:
387 388 389 390 391 392 393 394 395
                childParentData.offset = Offset(0, currentHeight);
            }
          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);
              case MainAxisAlignment.end:
                childParentData.offset = Offset(0, currentHeight);
396 397 398 399
              case MainAxisAlignment.spaceAround:
              case MainAxisAlignment.spaceBetween:
              case MainAxisAlignment.spaceEvenly:
              case MainAxisAlignment.start:
400 401 402 403
                childParentData.offset = Offset(constraints.maxWidth - child.size.width, currentHeight);
            }
        }
        currentHeight += child.size.height;
404 405 406 407 408 409
        switch (verticalDirection) {
          case VerticalDirection.down:
            child = childParentData.nextSibling;
          case VerticalDirection.up:
            child = childParentData.previousSibling;
        }
410

411
        if (overflowButtonSpacing != null && child != null) {
412
          currentHeight += overflowButtonSpacing!;
413
        }
414 415 416 417 418
      }
      size = constraints.constrain(Size(constraints.maxWidth, currentHeight));
    }
  }
}