button_bar.dart 18.2 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 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
/// ## Updating to [OverflowBar]
///
/// [ButtonBar] has been replace by a more efficient widget, [OverflowBar].
///
/// ```dart
/// // Before
/// ButtonBar(
///   alignment: MainAxisAlignment.spaceEvenly,
///   children: <Widget>[
///     TextButton( child: const Text('Button 1'), onPressed: () {}),
///     TextButton( child: const Text('Button 2'), onPressed: () {}),
///     TextButton( child: const Text('Button 3'), onPressed: () {}),
///   ],
/// );
/// ```
/// ```dart
/// // After
/// OverflowBar(
///   alignment: MainAxisAlignment.spaceEvenly,
///   children: <Widget>[
///     TextButton( child: const Text('Button 1'), onPressed: () {}),
///     TextButton( child: const Text('Button 2'), onPressed: () {}),
///     TextButton( child: const Text('Button 3'), onPressed: () {}),
///   ],
/// );
/// ```
///
/// See the [OverflowBar] documentation for more details.
///
/// ## Using [ButtonBar]
///
46 47 48 49 50 51 52
/// 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.
///
53 54 55 56
/// 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
57
/// [ButtonBar.alignment] was set to [MainAxisAlignment.start], the buttons would
58 59
/// align to the horizontal start of the button bar.
///
60 61 62 63 64 65 66 67 68 69 70
/// 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].
71 72 73 74 75
///
/// Used by [Dialog] to arrange the actions at the bottom of the dialog.
///
/// See also:
///
76 77 78
///  * [TextButton], a simple flat button without a shadow.
///  * [ElevatedButton], a filled button whose material elevates when pressed.
///  * [OutlinedButton], a [TextButton] with a border outline.
79 80
///  * [Card], at the bottom of which it is common to place a [ButtonBar].
///  * [Dialog], which uses a [ButtonBar] for its actions.
81
///  * [ButtonBarTheme], which configures the [ButtonBar].
82 83 84
class ButtonBar extends StatelessWidget {
  /// Creates a button bar.
  ///
85 86
  /// Both [buttonMinWidth] and [buttonHeight] must be non-negative if they
  /// are not null.
87
  const ButtonBar({
88
    super.key,
89 90 91 92 93 94 95 96
    this.alignment,
    this.mainAxisSize,
    this.buttonTextTheme,
    this.buttonMinWidth,
    this.buttonHeight,
    this.buttonPadding,
    this.buttonAlignedDropdown,
    this.layoutBehavior,
97
    this.overflowDirection,
98
    this.overflowButtonSpacing,
99
    this.children = const <Widget>[],
100 101
  }) : assert(buttonMinWidth == null || buttonMinWidth >= 0.0),
       assert(buttonHeight == null || buttonHeight >= 0.0),
102
       assert(overflowButtonSpacing == null || overflowButtonSpacing >= 0.0);
103 104

  /// How the children should be placed along the horizontal axis.
105
  ///
106
  /// If null then it will use [ButtonBarThemeData.alignment]. If that is null,
107
  /// it will default to [MainAxisAlignment.end].
108
  final MainAxisAlignment? alignment;
109

110
  /// How much horizontal space is available. See [Row.mainAxisSize].
111
  ///
112
  /// If null then it will use the surrounding [ButtonBarThemeData.mainAxisSize].
113
  /// If that is null, it will default to [MainAxisSize.max].
114
  final MainAxisSize? mainAxisSize;
115

116 117
  /// Overrides the surrounding [ButtonBarThemeData.buttonTextTheme] to define a
  /// button's base colors, size, internal padding and shape.
118
  ///
119 120 121
  /// If null then it will use the surrounding
  /// [ButtonBarThemeData.buttonTextTheme]. If that is null, it will default to
  /// [ButtonTextTheme.primary].
122
  final ButtonTextTheme? buttonTextTheme;
123 124 125 126

  /// Overrides the surrounding [ButtonThemeData.minWidth] to define a button's
  /// minimum width.
  ///
127
  /// If null then it will use the surrounding [ButtonBarThemeData.buttonMinWidth].
128
  /// If that is null, it will default to 64.0 logical pixels.
129
  final double? buttonMinWidth;
130 131 132 133

  /// Overrides the surrounding [ButtonThemeData.height] to define a button's
  /// minimum height.
  ///
134
  /// If null then it will use the surrounding [ButtonBarThemeData.buttonHeight].
135
  /// If that is null, it will default to 36.0 logical pixels.
136
  final double? buttonHeight;
137 138 139 140

  /// Overrides the surrounding [ButtonThemeData.padding] to define the padding
  /// for a button's child (typically the button's label).
  ///
141
  /// If null then it will use the surrounding [ButtonBarThemeData.buttonPadding].
142 143
  /// If that is null, it will default to 8.0 logical pixels on the left
  /// and right.
144
  final EdgeInsetsGeometry? buttonPadding;
145 146 147 148

  /// Overrides the surrounding [ButtonThemeData.alignedDropdown] to define whether
  /// a [DropdownButton] menu's width will match the button's width.
  ///
149
  /// If null then it will use the surrounding [ButtonBarThemeData.buttonAlignedDropdown].
150
  /// If that is null, it will default to false.
151
  final bool? buttonAlignedDropdown;
152 153 154 155 156 157

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

162 163 164 165 166 167 168 169 170 171 172 173
  /// 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
174
  /// [ButtonBarThemeData.overflowDirection]. If that is null, it will
175
  /// default to [VerticalDirection.down].
176
  final VerticalDirection? overflowDirection;
177

178 179
  /// The spacing between buttons when the button bar overflows.
  ///
180 181 182 183 184 185 186 187
  /// 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.
188 189 190
  ///
  /// If null then no spacing will be added in between buttons in
  /// an overflow state.
191
  final double? overflowButtonSpacing;
192

193 194
  /// The buttons to arrange horizontally.
  ///
195
  /// Typically [ElevatedButton] or [TextButton] widgets.
196 197 198 199
  final List<Widget> children;

  @override
  Widget build(BuildContext context) {
200 201 202 203 204 205 206 207 208 209 210 211
    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,
    );

212
    // We divide by 4.0 because we want half of the average of the left and right padding.
213
    final double paddingUnit = buttonTheme.padding.horizontal / 4.0;
214 215
    final Widget child = ButtonTheme.fromButtonThemeData(
      data: buttonTheme,
216
      child: _ButtonBarRow(
217 218
        mainAxisAlignment: alignment ?? barTheme.alignment ?? MainAxisAlignment.end,
        mainAxisSize: mainAxisSize ?? barTheme.mainAxisSize ?? MainAxisSize.max,
219
        overflowDirection: overflowDirection ?? barTheme.overflowDirection ?? VerticalDirection.down,
220
        overflowButtonSpacing: overflowButtonSpacing,
221 222 223 224 225 226 227
        children: children.map<Widget>((Widget child) {
          return Padding(
            padding: EdgeInsets.symmetric(horizontal: paddingUnit),
            child: child,
          );
        }).toList(),
      ),
228
    );
229 230 231 232 233
    switch (buttonTheme.layoutBehavior) {
      case ButtonBarLayoutBehavior.padded:
        return Padding(
          padding: EdgeInsets.symmetric(
            vertical: 2.0 * paddingUnit,
234
            horizontal: paddingUnit,
235 236 237 238 239 240 241 242 243 244 245
          ),
          child: child,
        );
      case ButtonBarLayoutBehavior.constrained:
        return Container(
          padding: EdgeInsets.symmetric(horizontal: paddingUnit),
          constraints: const BoxConstraints(minHeight: 52.0),
          alignment: Alignment.center,
          child: child,
        );
    }
246 247
  }
}
248 249 250 251 252

/// 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
253
/// maximum width constraints on the widget. If the button bar's width is
254 255 256 257 258 259 260
/// 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
261
/// [ButtonBar.alignment] was set to [MainAxisAlignment.start], the column of
262 263 264 265
/// 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.
266
  const _ButtonBarRow({
267 268 269
    required super.children,
    super.mainAxisSize,
    super.mainAxisAlignment,
270
    VerticalDirection overflowDirection = VerticalDirection.down,
271
    this.overflowButtonSpacing,
272
  }) : super(
273
    direction: Axis.horizontal,
274
    verticalDirection: overflowDirection,
275 276
  );

277
  final double? overflowButtonSpacing;
278

279 280 281 282 283 284 285
  @override
  _RenderButtonBarRow createRenderObject(BuildContext context) {
    return _RenderButtonBarRow(
      direction: direction,
      mainAxisAlignment: mainAxisAlignment,
      mainAxisSize: mainAxisSize,
      crossAxisAlignment: crossAxisAlignment,
286
      textDirection: getEffectiveTextDirection(context)!,
287 288
      verticalDirection: verticalDirection,
      textBaseline: textBaseline,
289
      overflowButtonSpacing: overflowButtonSpacing,
290 291 292 293 294 295 296 297 298 299 300 301
    );
  }

  @override
  void updateRenderObject(BuildContext context, covariant _RenderButtonBarRow renderObject) {
    renderObject
      ..direction = direction
      ..mainAxisAlignment = mainAxisAlignment
      ..mainAxisSize = mainAxisSize
      ..crossAxisAlignment = crossAxisAlignment
      ..textDirection = getEffectiveTextDirection(context)
      ..verticalDirection = verticalDirection
302 303
      ..textBaseline = textBaseline
      ..overflowButtonSpacing = overflowButtonSpacing;
304 305 306 307 308 309 310
  }
}

/// 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
311
/// maximum width constraints on the widget. If the button bar's width is
312 313 314 315 316 317 318
/// 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
319
/// [ButtonBar.alignment] was set to [MainAxisAlignment.start], the buttons would
320 321 322 323 324
/// 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({
325 326 327 328 329 330 331
    super.direction,
    super.mainAxisSize,
    super.mainAxisAlignment,
    super.crossAxisAlignment,
    required TextDirection super.textDirection,
    super.verticalDirection,
    super.textBaseline,
332
    this.overflowButtonSpacing,
333
  }) : assert(overflowButtonSpacing == null || overflowButtonSpacing >= 0);
334 335

  bool _hasCheckedLayoutWidth = false;
336
  double? overflowButtonSpacing;
337 338 339

  @override
  BoxConstraints get constraints {
340
    if (_hasCheckedLayoutWidth) {
341
      return super.constraints;
342
    }
343
    return super.constraints.copyWith(maxWidth: double.infinity);
344 345
  }

346 347 348 349 350 351 352 353 354 355 356 357 358
  @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);
359
      if (overflowButtonSpacing != null && child != null) {
360
        currentHeight += overflowButtonSpacing!;
361
      }
362 363 364 365
    }
    return constraints.constrain(Size(constraints.maxWidth, currentHeight));
  }

366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385
  @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);
386
      RenderBox? child;
387
      double currentHeight = 0.0;
388 389 390 391 392 393
      switch (verticalDirection) {
        case VerticalDirection.down:
          child = firstChild;
        case VerticalDirection.up:
          child = lastChild;
      }
394 395

      while (child != null) {
396
        final FlexParentData childParentData = child.parentData! as FlexParentData;
397 398 399 400 401 402

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

442
        if (overflowButtonSpacing != null && child != null) {
443
          currentHeight += overflowButtonSpacing!;
444
        }
445 446 447 448 449
      }
      size = constraints.constrain(Size(constraints.maxWidth, currentHeight));
    }
  }
}