button_bar.dart 18.5 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
    Key? 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
       super(key: key);
73 74

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

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

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

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

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

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

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

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

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

148 149 150 151 152 153 154 155 156 157 158 159 160 161
  /// 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.
162
  final double? overflowButtonSpacing;
163

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

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

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

/// 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
224
/// maximum width constraints on the widget. If the button bar's width is
225 226 227 228 229 230 231
/// 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
232
/// [ButtonBar.alignment] was set to [MainAxisAlignment.start], the column of
233 234 235 236 237
/// 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({
238
    required List<Widget> children,
239 240 241 242
    Axis direction = Axis.horizontal,
    MainAxisSize mainAxisSize = MainAxisSize.max,
    MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,
    CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center,
243
    TextDirection? textDirection,
244
    VerticalDirection overflowDirection = VerticalDirection.down,
245
    TextBaseline? textBaseline,
246
    this.overflowButtonSpacing,
247 248 249 250 251 252 253
  }) : super(
    children: children,
    direction: direction,
    mainAxisSize: mainAxisSize,
    mainAxisAlignment: mainAxisAlignment,
    crossAxisAlignment: crossAxisAlignment,
    textDirection: textDirection,
254
    verticalDirection: overflowDirection,
255 256 257
    textBaseline: textBaseline,
  );

258
  final double? overflowButtonSpacing;
259

260 261 262 263 264 265 266
  @override
  _RenderButtonBarRow createRenderObject(BuildContext context) {
    return _RenderButtonBarRow(
      direction: direction,
      mainAxisAlignment: mainAxisAlignment,
      mainAxisSize: mainAxisSize,
      crossAxisAlignment: crossAxisAlignment,
267
      textDirection: getEffectiveTextDirection(context)!,
268 269
      verticalDirection: verticalDirection,
      textBaseline: textBaseline,
270
      overflowButtonSpacing: overflowButtonSpacing,
271 272 273 274 275 276 277 278 279 280 281 282
    );
  }

  @override
  void updateRenderObject(BuildContext context, covariant _RenderButtonBarRow renderObject) {
    renderObject
      ..direction = direction
      ..mainAxisAlignment = mainAxisAlignment
      ..mainAxisSize = mainAxisSize
      ..crossAxisAlignment = crossAxisAlignment
      ..textDirection = getEffectiveTextDirection(context)
      ..verticalDirection = verticalDirection
283 284
      ..textBaseline = textBaseline
      ..overflowButtonSpacing = overflowButtonSpacing;
285 286 287 288 289 290 291
  }
}

/// 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
292
/// maximum width constraints on the widget. If the button bar's width is
293 294 295 296 297 298 299
/// 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
300
/// [ButtonBar.alignment] was set to [MainAxisAlignment.start], the buttons would
301 302 303 304 305
/// 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({
306
    List<RenderBox>? children,
307 308 309 310
    Axis direction = Axis.horizontal,
    MainAxisSize mainAxisSize = MainAxisSize.max,
    MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,
    CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center,
311
    required TextDirection textDirection,
312
    VerticalDirection verticalDirection = VerticalDirection.down,
313
    TextBaseline? textBaseline,
314
    this.overflowButtonSpacing,
315
  }) : assert(textDirection != null),
316
       assert(overflowButtonSpacing == null || overflowButtonSpacing >= 0),
317 318 319 320 321 322 323 324 325 326 327 328
       super(
         children: children,
         direction: direction,
         mainAxisSize: mainAxisSize,
         mainAxisAlignment: mainAxisAlignment,
         crossAxisAlignment: crossAxisAlignment,
         textDirection: textDirection,
         verticalDirection: verticalDirection,
         textBaseline: textBaseline,
       );

  bool _hasCheckedLayoutWidth = false;
329
  double? overflowButtonSpacing;
330 331 332 333 334

  @override
  BoxConstraints get constraints {
    if (_hasCheckedLayoutWidth)
      return super.constraints;
335
    return super.constraints.copyWith(maxWidth: double.infinity);
336 337
  }

338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356
  @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));
  }

357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376
  @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);
377
      RenderBox? child;
378
      double currentHeight = 0.0;
379 380 381 382 383 384 385 386
      switch (verticalDirection) {
        case VerticalDirection.down:
          child = firstChild;
          break;
        case VerticalDirection.up:
          child = lastChild;
          break;
      }
387 388

      while (child != null) {
389
        final FlexParentData childParentData = child.parentData! as FlexParentData;
390 391 392 393 394 395

        // 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
396 397 398
        // alignment for a row. For [MainAxisAlignment.spaceAround],
        // [MainAxisAlignment.spaceBetween] and [MainAxisAlignment.spaceEvenly]
        // cases, use [MainAxisAlignment.start].
399
        switch (textDirection!) {
400 401 402 403 404 405 406 407 408
          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;
409 410 411 412
              case MainAxisAlignment.spaceAround:
              case MainAxisAlignment.spaceBetween:
              case MainAxisAlignment.spaceEvenly:
              case MainAxisAlignment.start:
413 414 415 416 417 418 419 420 421 422 423 424 425
                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;
426 427 428 429
              case MainAxisAlignment.spaceAround:
              case MainAxisAlignment.spaceBetween:
              case MainAxisAlignment.spaceEvenly:
              case MainAxisAlignment.start:
430 431 432 433 434 435
                childParentData.offset = Offset(constraints.maxWidth - child.size.width, currentHeight);
                break;
            }
            break;
        }
        currentHeight += child.size.height;
436 437 438 439 440 441 442 443
        switch (verticalDirection) {
          case VerticalDirection.down:
            child = childParentData.nextSibling;
            break;
          case VerticalDirection.up:
            child = childParentData.previousSibling;
            break;
        }
444 445

        if (overflowButtonSpacing != null && child != null)
446
          currentHeight += overflowButtonSpacing!;
447 448 449 450 451
      }
      size = constraints.constrain(Size(constraints.maxWidth, currentHeight));
    }
  }
}