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

import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';

8 9 10 11 12
import 'color_scheme.dart';
import 'colors.dart';
import 'constants.dart';
import 'flat_button.dart';
import 'material_button.dart';
13
import 'material_state.dart';
14 15
import 'outline_button.dart';
import 'raised_button.dart';
16
import 'theme.dart';
17
import 'theme_data.dart' show MaterialTapTargetSize;
18 19 20 21 22 23 24

/// Used with [ButtonTheme] and [ButtonThemeData] to define a button's base
/// colors, and the defaults for the button's minimum size, internal padding,
/// and shape.
///
/// See also:
///
25 26
///  * [RaisedButton], [FlatButton], [OutlineButton], which are configured
///    based on the ambient [ButtonTheme].
27 28 29 30 31 32 33 34 35 36 37
enum ButtonTextTheme {
  /// Button text is black or white depending on [ThemeData.brightness].
  normal,

  /// Button text is [ThemeData.accentColor].
  accent,

  /// Button text is based on [ThemeData.primaryColor].
  primary,
}

38 39 40 41 42 43 44 45 46 47 48 49 50
/// Used with [ButtonTheme] and [ButtonThemeData] to define how the button bar
/// should size itself with either constraints or internal padding.
enum ButtonBarLayoutBehavior {
  /// Button bars will be constrained to a minimum height of 52.
  ///
  /// This setting is require to create button bars which conform to the
  /// material specification.
  constrained,

  /// Button bars will calculate their padding from the button theme padding.
  padded,
}

51 52
/// Used with [ButtonThemeData] to configure the color and geometry of buttons.
///
53 54 55 56 57
/// ### This class is obsolete.
///
/// Please use one or more of the new buttons and their themes
/// instead: [TextButton] and [TextButtonTheme], [ElevatedButton] and
/// [ElevatedButtonTheme], [OutlinedButton] and
58 59
/// [OutlinedButtonTheme]. The original classes have been deprecated,
/// please migrate code that uses them.  There's a detailed
60 61 62
/// migration guide for the new button and button theme classes in
/// [flutter.dev/go/material-button-migration-guide](https://flutter.dev/go/material-button-migration-guide).
///
63
/// A button theme can be specified as part of the overall Material theme
Josh Soref's avatar
Josh Soref committed
64
/// using [ThemeData.buttonTheme]. The Material theme's button theme data
65 66 67
/// can be overridden with [ButtonTheme].
///
/// The actual appearance of buttons depends on the button theme, the
68
/// button's enabled state, its elevation (if any), and the overall [Theme].
69 70 71
///
/// See also:
///
72 73
///  * [FlatButton] [RaisedButton], and [OutlineButton], which are styled
///    based on the ambient button theme.
74 75
///  * [RawMaterialButton], which can be used to configure a button that doesn't
///    depend on any inherited themes.
76
class ButtonTheme extends InheritedTheme {
77 78
  /// Creates a button theme.
  ///
79 80
  /// The [textTheme], [minWidth], [height], and [colorScheme] arguments
  /// must not be null.
81
  ButtonTheme({
82
    Key? key,
83
    ButtonTextTheme textTheme = ButtonTextTheme.normal,
84
    ButtonBarLayoutBehavior layoutBehavior = ButtonBarLayoutBehavior.padded,
85 86
    double minWidth = 88.0,
    double height = 36.0,
87 88
    EdgeInsetsGeometry? padding,
    ShapeBorder? shape,
89
    bool alignedDropdown = false,
90 91 92 93 94 95 96 97 98
    Color? buttonColor,
    Color? disabledColor,
    Color? focusColor,
    Color? hoverColor,
    Color? highlightColor,
    Color? splashColor,
    ColorScheme? colorScheme,
    MaterialTapTargetSize? materialTapTargetSize,
    required Widget child,
99 100 101
  }) : assert(textTheme != null),
       assert(minWidth != null && minWidth >= 0.0),
       assert(height != null && height >= 0.0),
102
       assert(alignedDropdown != null),
103
       assert(layoutBehavior != null),
104
       data = ButtonThemeData(
105 106 107 108 109
         textTheme: textTheme,
         minWidth: minWidth,
         height: height,
         padding: padding,
         shape: shape,
110 111
         alignedDropdown: alignedDropdown,
         layoutBehavior: layoutBehavior,
112 113
         buttonColor: buttonColor,
         disabledColor: disabledColor,
114 115
         focusColor: focusColor,
         hoverColor: hoverColor,
116 117 118 119
         highlightColor: highlightColor,
         splashColor: splashColor,
         colorScheme: colorScheme,
         materialTapTargetSize: materialTapTargetSize,
120 121 122
       ),
       super(key: key, child: child);

123 124 125 126
  /// Creates a button theme from [data].
  ///
  /// The [data] argument must not be null.
  const ButtonTheme.fromButtonThemeData({
127 128 129
    Key? key,
    required this.data,
    required Widget child,
130 131
  }) : assert(data != null),
       super(key: key, child: child);
132 133 134 135 136 137 138 139 140 141 142 143

  /// Specifies the color and geometry of buttons.
  final ButtonThemeData data;

  /// The closest instance of this class that encloses the given context.
  ///
  /// Typical usage is as follows:
  ///
  /// ```dart
  /// ButtonThemeData theme = ButtonTheme.of(context);
  /// ```
  static ButtonThemeData of(BuildContext context) {
144 145
    final ButtonTheme? inheritedButtonTheme = context.dependOnInheritedWidgetOfExactType<ButtonTheme>();
    ButtonThemeData? buttonTheme = inheritedButtonTheme?.data;
146
    if (buttonTheme?.colorScheme == null) { // if buttonTheme or buttonTheme.colorScheme is null
147
      final ThemeData theme = Theme.of(context);
148
      buttonTheme ??= theme.buttonTheme;
149 150 151 152 153 154
      if (buttonTheme.colorScheme == null) {
        buttonTheme = buttonTheme.copyWith(
          colorScheme: theme.buttonTheme.colorScheme ?? theme.colorScheme,
        );
        assert(buttonTheme.colorScheme != null);
      }
155
    }
156
    return buttonTheme!;
157 158
  }

159 160
  @override
  Widget wrap(BuildContext context, Widget child) {
161
    return ButtonTheme.fromButtonThemeData(data: data, child: child);
162 163
  }

164
  @override
165
  bool updateShouldNotify(ButtonTheme oldWidget) => data != oldWidget.data;
166 167 168 169
}

/// Used with [ButtonTheme] to configure the color and geometry of buttons.
///
170 171 172 173 174 175 176 177 178 179 180 181
/// ### This class is obsolete.
///
/// Please use one or more of the new buttons and their themes instead:
///
///  * [TextButton], [TextButtonTheme], [TextButtonThemeData],
///  * [ElevatedButton], [ElevatedButtonTheme], [ElevatedButtonThemeData],
///  * [OutlinedButton], [OutlinedButtonTheme], [OutlinedButtonThemeData]
///
/// FlatButton, RaisedButton, and OutlineButton have been replaced by
/// TextButton, ElevatedButton, and OutlinedButton respectively.
/// ButtonTheme has been replaced by TextButtonTheme,
/// ElevatedButtonTheme, and OutlinedButtonTheme. The original classes
182
/// have been deprecated, please migrate code that uses them.
183 184 185 186
/// There's a detailed migration guide for the new button and button
/// theme classes in
/// [flutter.dev/go/material-button-migration-guide](https://flutter.dev/go/material-button-migration-guide).
///
187
/// A button theme can be specified as part of the overall Material theme
Josh Soref's avatar
Josh Soref committed
188
/// using [ThemeData.buttonTheme]. The Material theme's button theme data
189
/// can be overridden with [ButtonTheme].
190
@immutable
191
class ButtonThemeData with Diagnosticable {
192 193 194
  /// Create a button theme object that can be used with [ButtonTheme]
  /// or [ThemeData].
  ///
195
  /// The [textTheme], [minWidth], [height], [alignedDropdown], and
196 197 198 199 200 201
  /// [layoutBehavior] parameters must not be null. The [minWidth] and
  /// [height] parameters must greater than or equal to zero.
  ///
  /// The ButtonTheme's methods that have a [MaterialButton] parameter and
  /// have a name with a `get` prefix are used by [RaisedButton],
  /// [OutlineButton], and [FlatButton] to configure a [RawMaterialButton].
202
  const ButtonThemeData({
203 204 205
    this.textTheme = ButtonTextTheme.normal,
    this.minWidth = 88.0,
    this.height = 36.0,
206 207
    EdgeInsetsGeometry? padding,
    ShapeBorder? shape,
208
    this.layoutBehavior = ButtonBarLayoutBehavior.padded,
209
    this.alignedDropdown = false,
210 211 212 213 214 215
    Color? buttonColor,
    Color? disabledColor,
    Color? focusColor,
    Color? hoverColor,
    Color? highlightColor,
    Color? splashColor,
216
    this.colorScheme,
217
    MaterialTapTargetSize? materialTapTargetSize,
218 219 220
  }) : assert(textTheme != null),
       assert(minWidth != null && minWidth >= 0.0),
       assert(height != null && height >= 0.0),
221
       assert(alignedDropdown != null),
222
       assert(layoutBehavior != null),
223 224
       _buttonColor = buttonColor,
       _disabledColor = disabledColor,
225 226
       _focusColor = focusColor,
       _hoverColor = hoverColor,
227 228
       _highlightColor = highlightColor,
       _splashColor = splashColor,
229
       _padding = padding,
230 231
       _shape = shape,
       _materialTapTargetSize = materialTapTargetSize;
232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247

  /// The minimum width for buttons.
  ///
  /// The actual horizontal space allocated for a button's child is
  /// at least this value less the theme's horizontal [padding].
  ///
  /// Defaults to 88.0 logical pixels.
  final double minWidth;

  /// The minimum height for buttons.
  ///
  /// Defaults to 36.0 logical pixels.
  final double height;

  /// Defines a button's base colors, and the defaults for the button's minimum
  /// size, internal padding, and shape.
248 249 250
  ///
  /// Despite the name, this property is not a [TextTheme], its value is not a
  /// collection of [TextStyle]s.
251 252
  final ButtonTextTheme textTheme;

253 254
  /// Defines whether a [ButtonBar] should size itself with a minimum size
  /// constraint or with padding.
255 256 257 258
  ///
  /// Defaults to [ButtonBarLayoutBehavior.padded].
  final ButtonBarLayoutBehavior layoutBehavior;

259 260
  /// Simply a convenience that returns [minWidth] and [height] as a
  /// [BoxConstraints] object:
261
  ///
262
  /// ```dart
263
  /// return BoxConstraints(
264
  ///   minWidth: minWidth,
265
  ///   minHeight: height,
266 267 268
  /// );
  /// ```
  BoxConstraints get constraints {
269
    return BoxConstraints(
270 271 272 273 274 275 276 277 278
      minWidth: minWidth,
      minHeight: height,
    );
  }

  /// Padding for a button's child (typically the button's label).
  ///
  /// Defaults to 24.0 on the left and right if [textTheme] is
  /// [ButtonTextTheme.primary], 16.0 on the left and right otherwise.
279 280 281 282 283
  ///
  /// See also:
  ///
  ///  * [getPadding], which is used by [RaisedButton], [OutlineButton]
  ///    and [FlatButton].
284 285
  EdgeInsetsGeometry get padding {
    if (_padding != null)
286
      return _padding!;
287
    switch (textTheme) {
288 289 290 291
      case ButtonTextTheme.normal:
      case ButtonTextTheme.accent:
        return const EdgeInsets.symmetric(horizontal: 16.0);
      case ButtonTextTheme.primary:
292
        return const EdgeInsets.symmetric(horizontal: 24.0);
293 294
    }
  }
295
  final EdgeInsetsGeometry? _padding;
296 297 298 299 300 301 302 303 304 305

  /// The shape of a button's material.
  ///
  /// The button's highlight and splash are clipped to this shape. If the
  /// button has an elevation, then its drop shadow is defined by this
  /// shape as well.
  ///
  /// Defaults to a rounded rectangle with circular corner radii of 4.0 if
  /// [textTheme] is [ButtonTextTheme.primary], a rounded rectangle with
  /// circular corner radii of 2.0 otherwise.
306 307 308 309 310
  ///
  /// See also:
  ///
  ///  * [getShape], which is used by [RaisedButton], [OutlineButton]
  ///    and [FlatButton].
311 312
  ShapeBorder get shape {
    if (_shape != null)
313
      return _shape!;
314 315 316 317
    switch (textTheme) {
      case ButtonTextTheme.normal:
      case ButtonTextTheme.accent:
        return const RoundedRectangleBorder(
318
          borderRadius: BorderRadius.all(Radius.circular(2.0)),
319 320 321
        );
      case ButtonTextTheme.primary:
        return const RoundedRectangleBorder(
322
          borderRadius: BorderRadius.all(Radius.circular(4.0)),
323 324 325
        );
    }
  }
326
  final ShapeBorder? _shape;
327

328 329 330 331 332 333 334 335 336 337 338
  /// If true, then a [DropdownButton] menu's width will match the button's
  /// width.
  ///
  /// If false (the default), then the dropdown's menu will be wider than
  /// its button. In either case the dropdown button will line up the leading
  /// edge of the menu's value with the leading edge of the values
  /// displayed by the menu items.
  ///
  /// This property only affects [DropdownButton] and its menu.
  final bool alignedDropdown;

339 340 341 342
  /// The background fill color for [RaisedButton]s.
  ///
  /// This property is null by default.
  ///
343 344
  /// If the button is in the focused, hovering, or highlighted state, then the
  /// [focusColor], [hoverColor], or [highlightColor] will take precedence over
345
  /// the [buttonColor].
346
  ///
347 348 349 350
  /// See also:
  ///
  ///  * [getFillColor], which is used by [RaisedButton] to compute its
  ///    background fill color.
351
  final Color? _buttonColor;
352 353 354 355 356 357 358 359 360

  /// The background fill color for disabled [RaisedButton]s.
  ///
  /// This property is null by default.
  ///
  /// See also:
  ///
  ///  * [getDisabledFillColor], which is used by [RaisedButton] to compute its
  ///    background fill color.
361
  final Color? _disabledColor;
362

363 364 365 366 367 368 369 370 371 372 373
  /// The fill color of the button when it has the input focus.
  ///
  /// This property is null by default.
  ///
  /// If the button is in the hovering or highlighted state, then the [hoverColor]
  /// or [highlightColor] will take precedence over the [focusColor].
  ///
  /// See also:
  ///
  ///  * [getFocusColor], which is used by [RaisedButton], [OutlineButton]
  ///    and [FlatButton].
374
  final Color? _focusColor;
375 376 377 378 379 380 381 382 383 384 385 386

  /// The fill color of the button when a pointer is hovering over it.
  ///
  /// This property is null by default.
  ///
  /// If the button is in the highlighted state, then the [highlightColor] will
  /// take precedence over the [hoverColor].
  ///
  /// See also:
  ///
  ///  * [getHoverColor], which is used by [RaisedButton], [OutlineButton]
  ///    and [FlatButton].
387
  final Color? _hoverColor;
388

389 390 391 392 393 394 395 396
  /// The color of the overlay that appears when a button is pressed.
  ///
  /// This property is null by default.
  ///
  /// See also:
  ///
  ///  * [getHighlightColor], which is used by [RaisedButton], [OutlineButton]
  ///    and [FlatButton].
397
  final Color? _highlightColor;
398 399 400 401 402 403 404 405 406

  /// The color of the ink "splash" overlay that appears when a button is tapped.
  ///
  /// This property is null by default.
  ///
  /// See also:
  ///
  ///  * [getSplashColor], which is used by [RaisedButton], [OutlineButton]
  ///    and [FlatButton].
407
  final Color? _splashColor;
408 409 410 411 412 413 414 415 416 417 418

  /// A set of thirteen colors that can be used to derive the button theme's
  /// colors.
  ///
  /// This property was added much later than the theme's set of highly
  /// specific colors, like [ThemeData.buttonColor], [ThemeData.highlightColor],
  /// [ThemeData.splashColor] etc.
  ///
  /// The colors for new button classes can be defined exclusively in terms
  /// of [colorScheme]. When it's possible, the existing buttons will
  /// (continue to) gradually migrate to it.
419
  final ColorScheme? colorScheme;
420 421 422 423 424 425 426 427 428

  // The minimum size of a button's tap target.
  //
  // This property is null by default.
  //
  // See also:
  //
  //  * [getMaterialTargetTapSize], which is used by [RaisedButton],
  //    [OutlineButton] and [FlatButton].
429
  final MaterialTapTargetSize? _materialTapTargetSize;
430 431 432 433 434 435

  /// The [button]'s overall brightness.
  ///
  /// Returns the button's [MaterialButton.colorBrightness] if it is non-null,
  /// otherwise the color scheme's [ColorScheme.brightness] is returned.
  Brightness getBrightness(MaterialButton button) {
436
    return button.colorBrightness ?? colorScheme!.brightness;
437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452
  }

  /// Defines the [button]'s base colors, and the defaults for the button's
  /// minimum size, internal padding, and shape.
  ///
  /// Despite the name, this property is not the [TextTheme] whose
  /// [TextTheme.button] is used as the button text's [TextStyle].
  ButtonTextTheme getTextTheme(MaterialButton button) {
    return button.textTheme ?? textTheme;
  }

  /// The foreground color of the [button]'s text and icon when
  /// [MaterialButton.onPressed] is null (when MaterialButton.enabled is false).
  ///
  /// Returns the button's [MaterialButton.disabledColor] if it is non-null.
  /// Otherwise the color scheme's [ColorScheme.onSurface] color is returned
453
  /// with its opacity set to 0.38.
454
  ///
455 456
  /// If [MaterialButton.textColor] is a [MaterialStateProperty<Color>], it will be
  /// used as the `disabledTextColor`. It will be resolved in the [MaterialState.disabled] state.
457
  Color getDisabledTextColor(MaterialButton button) {
458
    if (button.textColor is MaterialStateProperty<Color?>)
459
      return button.textColor!;
460
    if (button.disabledTextColor != null)
461 462
      return button.disabledTextColor!;
    return colorScheme!.onSurface.withOpacity(0.38);
463 464 465
  }

  /// The [button]'s background color when [MaterialButton.onPressed] is null
466
  /// (when [MaterialButton.enabled] is false).
467 468 469
  ///
  /// Returns the button's [MaterialButton.disabledColor] if it is non-null.
  ///
470
  /// Otherwise the value of the `disabledColor` constructor parameter
471 472 473
  /// is returned, if it is non-null.
  ///
  /// Otherwise the color scheme's [ColorScheme.onSurface] color is returned
474
  /// with its opacity set to 0.38.
475 476
  Color getDisabledFillColor(MaterialButton button) {
    if (button.disabledColor != null)
477
      return button.disabledColor!;
478
    if (_disabledColor != null)
479 480
      return _disabledColor!;
    return colorScheme!.onSurface.withOpacity(0.38);
481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507
  }

  /// The button's background fill color or null for buttons that don't have
  /// a background color.
  ///
  /// Returns [MaterialButton.color] if it is non-null and the button
  /// is enabled.
  ///
  /// Otherwise, returns [MaterialButton.disabledColor] if it is non-null and
  /// the button is disabled.
  ///
  /// Otherwise, if button is a [FlatButton] or an [OutlineButton] then null is
  /// returned.
  ///
  /// Otherwise, if button is a [RaisedButton], returns the `buttonColor`
  /// constructor parameter if it was non-null and the button is enabled.
  ///
  /// Otherwise the fill color depends on the value of [getTextTheme].
  ///
  ///  * [ButtonTextTheme.normal] or [ButtonTextTheme.accent], the
  ///    color scheme's [ColorScheme.primary] color if the [button] is enabled
  ///    the value of [getDisabledFillColor] otherwise.
  ///  * [ButtonTextTheme.primary], if the [button] is enabled then the value
  ///    of the `buttonColor` constructor parameter if it is non-null,
  ///    otherwise the color scheme's ColorScheme.primary color. If the button
  ///    is not enabled then the colorScheme's [ColorScheme.onSurface] color
  ///    with opacity 0.12.
508 509
  Color? getFillColor(MaterialButton button) {
    final Color? fillColor = button.enabled ? button.color : button.disabledColor;
510 511 512
    if (fillColor != null)
      return fillColor;

513
    if (button is FlatButton || button is OutlineButton || button.runtimeType == MaterialButton)
514 515 516 517 518 519 520 521
      return null;

    if (button.enabled && button is RaisedButton && _buttonColor != null)
      return _buttonColor;

    switch (getTextTheme(button)) {
      case ButtonTextTheme.normal:
      case ButtonTextTheme.accent:
522
        return button.enabled ? colorScheme!.primary : getDisabledFillColor(button);
523 524
      case ButtonTextTheme.primary:
        return button.enabled
525 526
          ? _buttonColor ?? colorScheme!.primary
          : colorScheme!.onSurface.withOpacity(0.12);
527 528 529 530 531 532 533
    }
  }

  /// The foreground color of the [button]'s text and icon.
  ///
  /// If [button] is not [MaterialButton.enabled], the value of
  /// [getDisabledTextColor] is returned. If the button is enabled and
534 535
  /// [MaterialButton.textColor] is non-null, then [MaterialButton.textColor]
  /// is returned.
536 537 538 539
  ///
  /// Otherwise the text color depends on the value of [getTextTheme]
  /// and [getBrightness].
  ///
540 541 542
  ///  * [ButtonTextTheme.normal]: [Colors.white] is used if [getBrightness]
  ///    resolves to [Brightness.dark]. [Colors.black87] is used if
  ///    [getBrightness] resolves to [Brightness.light].
543
  ///  * [ButtonTextTheme.accent]: [ColorScheme.secondary] of [colorScheme].
544
  ///  * [ButtonTextTheme.primary]: If [getFillColor] is dark then [Colors.white],
545
  ///    otherwise if [button] is a [FlatButton] or an [OutlineButton] then
546
  ///    [ColorScheme.primary] of [colorScheme], otherwise [Colors.black].
547 548 549 550 551
  Color getTextColor(MaterialButton button) {
    if (!button.enabled)
      return getDisabledTextColor(button);

    if (button.textColor != null)
552
      return button.textColor!;
553 554 555 556 557 558

    switch (getTextTheme(button)) {
      case ButtonTextTheme.normal:
        return getBrightness(button) == Brightness.dark ? Colors.white : Colors.black87;

      case ButtonTextTheme.accent:
559
        return colorScheme!.secondary;
560

561
      case ButtonTextTheme.primary:
562
        final Color? fillColor = getFillColor(button);
563 564 565 566 567 568
        final bool fillIsDark = fillColor != null
          ? ThemeData.estimateBrightnessForColor(fillColor) == Brightness.dark
          : getBrightness(button) == Brightness.dark;
        if (fillIsDark)
          return Colors.white;
        if (button is FlatButton || button is OutlineButton)
569
          return colorScheme!.primary;
570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588
        return Colors.black;
    }
  }

  /// The color of the ink "splash" overlay that appears when the (enabled)
  /// [button] is tapped.
  ///
  /// Returns the button's [MaterialButton.splashColor] if it is non-null.
  ///
  /// Otherwise, returns the value of the `splashColor` constructor parameter
  /// it is non-null and [button] is a [RaisedButton] or an [OutlineButton].
  ///
  /// Otherwise, returns the value of the `splashColor` constructor parameter
  /// if it is non-null and [button] is a [FlatButton] and
  /// [getTextTheme] is not [ButtonTextTheme.primary]
  ///
  /// Otherwise, returns [getTextColor] with an opacity of 0.12.
  Color getSplashColor(MaterialButton button) {
    if (button.splashColor != null)
589
      return button.splashColor!;
590 591

    if (_splashColor != null && (button is RaisedButton || button is OutlineButton))
592
      return _splashColor!;
593 594 595 596 597

    if (_splashColor != null && button is FlatButton) {
      switch (getTextTheme(button)) {
        case ButtonTextTheme.normal:
        case ButtonTextTheme.accent:
598
          return _splashColor!;
599 600 601 602 603 604 605 606
        case ButtonTextTheme.primary:
          break;
      }
    }

    return getTextColor(button).withOpacity(0.12);
  }

607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632
  /// The fill color of the button when it has input focus.
  ///
  /// Returns the button's [MaterialButton.focusColor] if it is non-null.
  /// Otherwise the focus color depends on [getTextTheme]:
  ///
  ///  * [ButtonTextTheme.normal], [ButtonTextTheme.accent]: returns the
  ///    value of the `focusColor` constructor parameter if it is non-null,
  ///    otherwise the value of [getTextColor] with opacity 0.12.
  ///  * [ButtonTextTheme.primary], returns [Colors.transparent].
  Color getFocusColor(MaterialButton button) {
    return button.focusColor ?? _focusColor ?? getTextColor(button).withOpacity(0.12);
  }

  /// The fill color of the button when it has input focus.
  ///
  /// Returns the button's [MaterialButton.focusColor] if it is non-null.
  /// Otherwise the focus color depends on [getTextTheme]:
  ///
  ///  * [ButtonTextTheme.normal], [ButtonTextTheme.accent],
  ///    [ButtonTextTheme.primary]: returns the value of the `focusColor`
  ///    constructor parameter if it is non-null, otherwise the value of
  ///    [getTextColor] with opacity 0.04.
  Color getHoverColor(MaterialButton button) {
    return button.hoverColor ?? _hoverColor ?? getTextColor(button).withOpacity(0.04);
  }

633 634 635 636 637 638 639 640 641 642 643
  /// The color of the overlay that appears when the [button] is pressed.
  ///
  /// Returns the button's [MaterialButton.highlightColor] if it is non-null.
  /// Otherwise the highlight color depends on [getTextTheme]:
  ///
  ///  * [ButtonTextTheme.normal], [ButtonTextTheme.accent]: returns the
  ///    value of the `highlightColor` constructor parameter if it is non-null,
  ///    otherwise the value of [getTextColor] with opacity 0.16.
  ///  * [ButtonTextTheme.primary], returns [Colors.transparent].
  Color getHighlightColor(MaterialButton button) {
    if (button.highlightColor != null)
644
      return button.highlightColor!;
645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661

    switch (getTextTheme(button)) {
      case ButtonTextTheme.normal:
      case ButtonTextTheme.accent:
        return _highlightColor ?? getTextColor(button).withOpacity(0.16);
      case ButtonTextTheme.primary:
        return Colors.transparent;
    }
  }

  /// The [button]'s elevation when it is enabled and has not been pressed.
  ///
  /// Returns the button's [MaterialButton.elevation] if it is non-null.
  ///
  /// If button is a [FlatButton] then elevation is 0.0, otherwise it is 2.0.
  double getElevation(MaterialButton button) {
    if (button.elevation != null)
662
      return button.elevation!;
663 664 665 666 667
    if (button is FlatButton)
      return 0.0;
    return 2.0;
  }

668 669 670 671 672 673 674 675
  /// The [button]'s elevation when it is enabled and has focus.
  ///
  /// Returns the button's [MaterialButton.focusElevation] if it is non-null.
  ///
  /// If button is a [FlatButton] or an [OutlineButton] then the focus
  /// elevation is 0.0, otherwise the highlight elevation is 4.0.
  double getFocusElevation(MaterialButton button) {
    if (button.focusElevation != null)
676
      return button.focusElevation!;
677 678 679 680 681 682 683 684 685 686 687 688 689 690 691
    if (button is FlatButton)
      return 0.0;
    if (button is OutlineButton)
      return 0.0;
    return 4.0;
  }

  /// The [button]'s elevation when it is enabled and has focus.
  ///
  /// Returns the button's [MaterialButton.hoverElevation] if it is non-null.
  ///
  /// If button is a [FlatButton] or an [OutlineButton] then the hover
  /// elevation is 0.0, otherwise the highlight elevation is 4.0.
  double getHoverElevation(MaterialButton button) {
    if (button.hoverElevation != null)
692
      return button.hoverElevation!;
693 694 695 696 697 698 699
    if (button is FlatButton)
      return 0.0;
    if (button is OutlineButton)
      return 0.0;
    return 4.0;
  }

700 701 702 703
  /// The [button]'s elevation when it is enabled and has been pressed.
  ///
  /// Returns the button's [MaterialButton.highlightElevation] if it is non-null.
  ///
704
  /// If button is a [FlatButton] or an [OutlineButton] then the highlight
705
  /// elevation is 0.0, otherwise the highlight elevation is 8.0.
706 707
  double getHighlightElevation(MaterialButton button) {
    if (button.highlightElevation != null)
708
      return button.highlightElevation!;
709 710 711
    if (button is FlatButton)
      return 0.0;
    if (button is OutlineButton)
712
      return 0.0;
713 714 715 716 717 718 719 720
    return 8.0;
  }

  /// The [button]'s elevation when [MaterialButton.onPressed] is null (when
  /// MaterialButton.enabled is false).
  ///
  /// Returns the button's [MaterialButton.elevation] if it is non-null.
  ///
721
  /// Otherwise the disabled elevation is 0.0.
722 723
  double getDisabledElevation(MaterialButton button) {
    if (button.disabledElevation != null)
724
      return button.disabledElevation!;
725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742
    return 0.0;
  }

  /// Padding for the [button]'s child (typically the button's label).
  ///
  /// Returns the button's [MaterialButton.padding] if it is non-null.
  ///
  /// If this is a button constructed with [RaisedButton.icon] or
  /// [FlatButton.icon] or [OutlineButton.icon] then the padding is:
  /// `EdgeInsetsDirectional.only(start: 12.0, end: 16.0)`.
  ///
  /// Otherwise, returns [padding] if it is non-null.
  ///
  /// Otherwise, returns horizontal padding of 24.0 on the left and right if
  /// [getTextTheme] is [ButtonTextTheme.primary], 16.0 on the left and right
  /// otherwise.
  EdgeInsetsGeometry getPadding(MaterialButton button) {
    if (button.padding != null)
743
      return button.padding!;
744 745 746 747 748

    if (button is MaterialButtonWithIconMixin)
      return const EdgeInsetsDirectional.only(start: 12.0, end: 16.0);

    if (_padding != null)
749
      return _padding!;
750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784

    switch (getTextTheme(button)) {
      case ButtonTextTheme.normal:
      case ButtonTextTheme.accent:
        return const EdgeInsets.symmetric(horizontal: 16.0);
      case ButtonTextTheme.primary:
        return const EdgeInsets.symmetric(horizontal: 24.0);
    }
  }

  /// The shape of the [button]'s [Material].
  ///
  /// Returns the button's [MaterialButton.shape] if it is non-null, otherwise
  /// [shape] is returned.
  ShapeBorder getShape(MaterialButton button) {
    return button.shape ?? shape;
  }

  /// The duration of the [button]'s highlight animation.
  ///
  /// Returns the button's [MaterialButton.animationDuration] it if is non-null,
  /// otherwise 200ms.
  Duration getAnimationDuration(MaterialButton button) {
    return button.animationDuration ?? kThemeChangeDuration;
  }

  /// The [BoxConstraints] that the define the [button]'s size.
  ///
  /// By default this method just returns [constraints]. Subclasses
  /// could override this method to return a value that was,
  /// for example, based on the button's type.
  BoxConstraints getConstraints(MaterialButton button) => constraints;

  /// The minimum size of the [button]'s tap target.
  ///
785
  /// Returns the button's [MaterialButton.materialTapTargetSize] if it is non-null.
786
  ///
787
  /// Otherwise the value of the `materialTapTargetSize` constructor
788 789 790 791 792 793 794
  /// parameter is returned if that's non-null.
  ///
  /// Otherwise [MaterialTapTargetSize.padded] is returned.
  MaterialTapTargetSize getMaterialTapTargetSize(MaterialButton button) {
    return button.materialTapTargetSize ?? _materialTapTargetSize ?? MaterialTapTargetSize.padded;
  }

795 796 797
  /// Creates a copy of this button theme data object with the matching fields
  /// replaced with the non-null parameter values.
  ButtonThemeData copyWith({
798 799 800 801 802 803 804 805 806 807 808 809 810 811 812
    ButtonTextTheme? textTheme,
    ButtonBarLayoutBehavior? layoutBehavior,
    double? minWidth,
    double? height,
    EdgeInsetsGeometry? padding,
    ShapeBorder? shape,
    bool? alignedDropdown,
    Color? buttonColor,
    Color? disabledColor,
    Color? focusColor,
    Color? hoverColor,
    Color? highlightColor,
    Color? splashColor,
    ColorScheme? colorScheme,
    MaterialTapTargetSize? materialTapTargetSize,
813
  }) {
814
    return ButtonThemeData(
815
      textTheme: textTheme ?? this.textTheme,
816
      layoutBehavior: layoutBehavior ?? this.layoutBehavior,
817 818 819 820 821
      minWidth: minWidth ?? this.minWidth,
      height: height ?? this.height,
      padding: padding ?? this.padding,
      shape: shape ?? this.shape,
      alignedDropdown: alignedDropdown ?? this.alignedDropdown,
822 823
      buttonColor: buttonColor ?? _buttonColor,
      disabledColor: disabledColor ?? _disabledColor,
824 825
      focusColor: focusColor ?? _focusColor,
      hoverColor: hoverColor ?? _hoverColor,
826 827 828 829
      highlightColor: highlightColor ?? _highlightColor,
      splashColor: splashColor ?? _splashColor,
      colorScheme: colorScheme ?? this.colorScheme,
      materialTapTargetSize: materialTapTargetSize ?? _materialTapTargetSize,
830 831 832
    );
  }

833
  @override
834
  bool operator ==(Object other) {
835 836
    if (other.runtimeType != runtimeType)
      return false;
837 838 839 840 841 842 843 844 845 846 847 848 849 850 851
    return other is ButtonThemeData
        && other.textTheme == textTheme
        && other.minWidth == minWidth
        && other.height == height
        && other.padding == padding
        && other.shape == shape
        && other.alignedDropdown == alignedDropdown
        && other._buttonColor == _buttonColor
        && other._disabledColor == _disabledColor
        && other._focusColor == _focusColor
        && other._hoverColor == _hoverColor
        && other._highlightColor == _highlightColor
        && other._splashColor == _splashColor
        && other.colorScheme == colorScheme
        && other._materialTapTargetSize == _materialTapTargetSize;
852 853 854 855 856 857 858 859 860 861
  }

  @override
  int get hashCode {
    return hashValues(
      textTheme,
      minWidth,
      height,
      padding,
      shape,
862
      alignedDropdown,
863 864
      _buttonColor,
      _disabledColor,
865 866
      _focusColor,
      _hoverColor,
867 868 869 870
      _highlightColor,
      _splashColor,
      colorScheme,
      _materialTapTargetSize,
871 872
    );
  }
873 874

  @override
875 876
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
877
    const ButtonThemeData defaultTheme = ButtonThemeData();
878 879 880 881 882 883
    properties.add(EnumProperty<ButtonTextTheme>('textTheme', textTheme, defaultValue: defaultTheme.textTheme));
    properties.add(DoubleProperty('minWidth', minWidth, defaultValue: defaultTheme.minWidth));
    properties.add(DoubleProperty('height', height, defaultValue: defaultTheme.height));
    properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding, defaultValue: defaultTheme.padding));
    properties.add(DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: defaultTheme.shape));
    properties.add(FlagProperty('alignedDropdown',
884 885 886 887
      value: alignedDropdown,
      defaultValue: defaultTheme.alignedDropdown,
      ifTrue: 'dropdown width matches button',
    ));
888 889 890 891 892 893
    properties.add(ColorProperty('buttonColor', _buttonColor, defaultValue: null));
    properties.add(ColorProperty('disabledColor', _disabledColor, defaultValue: null));
    properties.add(ColorProperty('focusColor', _focusColor, defaultValue: null));
    properties.add(ColorProperty('hoverColor', _hoverColor, defaultValue: null));
    properties.add(ColorProperty('highlightColor', _highlightColor, defaultValue: null));
    properties.add(ColorProperty('splashColor', _splashColor, defaultValue: null));
894 895
    properties.add(DiagnosticsProperty<ColorScheme>('colorScheme', colorScheme, defaultValue: defaultTheme.colorScheme));
    properties.add(DiagnosticsProperty<MaterialTapTargetSize>('materialTapTargetSize', _materialTapTargetSize, defaultValue: null));
896
  }
897
}