button_style.dart 20.5 KB
Newer Older
1 2 3 4 5 6 7 8 9 10
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:ui' show lerpDouble;

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

11
import 'ink_well.dart';
12 13 14 15 16 17
import 'material_state.dart';
import 'theme_data.dart';

/// The visual properties that most buttons have in common.
///
/// Buttons and their themes have a ButtonStyle property which defines the visual
18 19
/// properties whose default values are to be overridden. The default values are
/// defined by the individual button widgets and are typically based on overall
20 21 22 23 24 25 26 27 28 29 30
/// theme's [ThemeData.colorScheme] and [ThemeData.textTheme].
///
/// All of the ButtonStyle properties are null by default.
///
/// Many of the ButtonStyle properties are [MaterialStateProperty] objects which
/// resolve to different values depending on the button's state. For example
/// the [Color] properties are defined with `MaterialStateProperty<Color>` and
/// can resolve to different colors depending on if the button is pressed,
/// hovered, focused, disabled, etc.
///
/// These properties can override the default value for just one state or all of
31
/// them. For example to create a [ElevatedButton] whose background color is the
32 33 34 35
/// color scheme’s primary color with 50% opacity, but only when the button is
/// pressed, one could write:
///
/// ```dart
36
/// ElevatedButton(
37
///   style: ButtonStyle(
38
///     backgroundColor: MaterialStateProperty.resolveWith<Color?>(
39 40 41 42 43 44 45 46
///       (Set<MaterialState> states) {
///         if (states.contains(MaterialState.pressed))
///           return Theme.of(context).colorScheme.primary.withOpacity(0.5);
///         return null; // Use the component's default.
///       },
///     ),
///   ),
/// )
47
/// ```
48 49
///
/// In this case the background color for all other button states would fallback
50
/// to the ElevatedButton’s default values. To unconditionally set the button's
51 52 53
/// [backgroundColor] for all states one could write:
///
/// ```dart
54
/// ElevatedButton(
55 56 57 58
///   style: ButtonStyle(
///     backgroundColor: MaterialStateProperty.all<Color>(Colors.green),
///   ),
/// )
59
/// ```
60 61 62 63 64 65 66 67
///
/// Configuring a ButtonStyle directly makes it possible to very
/// precisely control the button’s visual attributes for all states.
/// This level of control is typically required when a custom
/// “branded” look and feel is desirable.  However, in many cases it’s
/// useful to make relatively sweeping changes based on a few initial
/// parameters with simple values. The button styleFrom() methods
/// enable such sweeping changes. See for example:
68
/// [TextButton.styleFrom], [ElevatedButton.styleFrom],
69 70 71 72 73 74 75 76 77 78 79
/// [OutlinedButton.styleFrom].
///
/// For example, to override the default text and icon colors for a
/// [TextButton], as well as its overlay color, with all of the
/// standard opacity adjustments for the pressed, focused, and
/// hovered states, one could write:
///
/// ```dart
/// TextButton(
///   style: TextButton.styleFrom(primary: Colors.green),
/// )
80
/// ```
81 82 83 84 85 86 87 88 89 90 91 92
///
/// To configure all of the application's text buttons in the same
/// way, specify the overall theme's `textButtonTheme`:
/// ```dart
/// MaterialApp(
///   theme: ThemeData(
///     textButtonTheme: TextButtonThemeData(
///       style: TextButton.styleFrom(primary: Colors.green),
///     ),
///   ),
///   home: MyAppHome(),
/// )
93
/// ```
94 95 96
/// See also:
///
///  * [TextButtonTheme], the theme for [TextButton]s.
97
///  * [ElevatedButtonTheme], the theme for [ElevatedButton]s.
98 99 100 101 102 103 104 105 106 107 108 109 110
///  * [OutlinedButtonTheme], the theme for [OutlinedButton]s.
@immutable
class ButtonStyle with Diagnosticable {
  /// Create a [ButtonStyle].
  const ButtonStyle({
    this.textStyle,
    this.backgroundColor,
    this.foregroundColor,
    this.overlayColor,
    this.shadowColor,
    this.elevation,
    this.padding,
    this.minimumSize,
111
    this.fixedSize,
112
    this.maximumSize,
113 114 115 116 117 118 119
    this.side,
    this.shape,
    this.mouseCursor,
    this.visualDensity,
    this.tapTargetSize,
    this.animationDuration,
    this.enableFeedback,
120
    this.alignment,
121
    this.splashFactory,
122 123 124 125 126
  });

  /// The style for a button's [Text] widget descendants.
  ///
  /// The color of the [textStyle] is typically not used directly, the
127
  /// [foregroundColor] is used instead.
128
  final MaterialStateProperty<TextStyle?>? textStyle;
129 130

  /// The button's background fill color.
131
  final MaterialStateProperty<Color?>? backgroundColor;
132 133 134 135 136 137 138

  /// The color for the button's [Text] and [Icon] widget descendants.
  ///
  /// This color is typically used instead of the color of the [textStyle]. All
  /// of the components that compute defaults from [ButtonStyle] values
  /// compute a default [foregroundColor] and use that instead of the
  /// [textStyle]'s color.
139
  final MaterialStateProperty<Color?>? foregroundColor;
140 141 142

  /// The highlight color that's typically used to indicate that
  /// the button is focused, hovered, or pressed.
143
  final MaterialStateProperty<Color?>? overlayColor;
144 145 146 147 148 149 150

  /// The shadow color of the button's [Material].
  ///
  /// The material's elevation shadow can be difficult to see for
  /// dark themes, so by default the button classes add a
  /// semi-transparent overlay to indicate elevation. See
  /// [ThemeData.applyElevationOverlayColor].
151
  final MaterialStateProperty<Color?>? shadowColor;
152 153

  /// The elevation of the button's [Material].
154
  final MaterialStateProperty<double?>? elevation;
155 156

  /// The padding between the button's boundary and its child.
157
  final MaterialStateProperty<EdgeInsetsGeometry?>? padding;
158 159 160 161 162

  /// The minimum size of the button itself.
  ///
  /// The size of the rectangle the button lies within may be larger
  /// per [tapTargetSize].
163 164
  ///
  /// This value must be less than or equal to [maximumSize].
165
  final MaterialStateProperty<Size?>? minimumSize;
166

167 168
  /// The button's size.
  ///
169 170 171
  /// This size is still constrained by the style's [minimumSize]
  /// and [maximumSize]. Fixed size dimensions whose value is
  /// [double.infinity] are ignored.
172 173 174 175 176 177
  ///
  /// To specify buttons with a fixed width and the default height use
  /// `fixedSize: Size.fromWidth(320)`. Similarly, to specify a fixed
  /// height and the default width use `fixedSize: Size.fromHeight(100)`.
  final MaterialStateProperty<Size?>? fixedSize;

178 179 180 181 182 183 184 185
  /// The maximum size of the button itself.
  ///
  /// A [Size.infinite] or null value for this property means that
  /// the button's maximum size is not constrained.
  ///
  /// This value must be greater than or equal to [minimumSize].
  final MaterialStateProperty<Size?>? maximumSize;

186 187 188 189
  /// The color and weight of the button's outline.
  ///
  /// This value is combined with [shape] to create a shape decorated
  /// with an outline.
190
  final MaterialStateProperty<BorderSide?>? side;
191 192 193 194 195

  /// The shape of the button's underlying [Material].
  ///
  /// This shape is combined with [side] to create a shape decorated
  /// with an outline.
196
  final MaterialStateProperty<OutlinedBorder?>? shape;
197 198 199

  /// The cursor for a mouse pointer when it enters or is hovering over
  /// this button's [InkWell].
200
  final MaterialStateProperty<MouseCursor?>? mouseCursor;
201 202 203 204 205 206 207 208 209

  /// Defines how compact the button's layout will be.
  ///
  /// {@macro flutter.material.themedata.visualDensity}
  ///
  /// See also:
  ///
  ///  * [ThemeData.visualDensity], which specifies the [visualDensity] for all widgets
  ///    within a [Theme].
210
  final VisualDensity? visualDensity;
211 212 213 214 215 216 217

  /// Configures the minimum size of the area within which the button may be pressed.
  ///
  /// If the [tapTargetSize] is larger than [minimumSize], the button will include
  /// a transparent margin that responds to taps.
  ///
  /// Always defaults to [ThemeData.materialTapTargetSize].
218
  final MaterialTapTargetSize? tapTargetSize;
219 220 221 222

  /// Defines the duration of animated changes for [shape] and [elevation].
  ///
  /// Typically the component default value is [kThemeChangeDuration].
223
  final Duration? animationDuration;
224 225 226 227 228 229 230 231 232 233 234

  /// Whether detected gestures should provide acoustic and/or haptic feedback.
  ///
  /// For example, on Android a tap will produce a clicking sound and a
  /// long-press will produce a short vibration, when feedback is enabled.
  ///
  /// Typically the component default value is true.
  ///
  /// See also:
  ///
  ///  * [Feedback] for providing platform-specific feedback to certain actions.
235
  final bool? enableFeedback;
236

237 238 239 240 241 242 243 244 245 246
  /// The alignment of the button's child.
  ///
  /// Typically buttons are sized to be just big enough to contain the child and its
  /// padding. If the button's size is constrained to a fixed size, for example by
  /// enclosing it with a [SizedBox], this property defines how the child is aligned
  /// within the available space.
  ///
  /// Always defaults to [Alignment.center].
  final AlignmentGeometry? alignment;

247 248 249 250 251 252 253 254 255 256 257 258 259 260 261
  /// Creates the [InkWell] splash factory, which defines the appearance of
  /// "ink" splashes that occur in response to taps.
  ///
  /// Use [NoSplash.splashFactory] to defeat ink splash rendering. For example:
  /// ```dart
  /// ElevatedButton(
  ///   style: ElevatedButton.styleFrom(
  ///     splashFactory: NoSplash.splashFactory,
  ///   ),
  ///   onPressed: () { },
  ///   child: Text('No Splash'),
  /// )
  /// ```
  final InteractiveInkFeatureFactory? splashFactory;

262 263 264
  /// Returns a copy of this ButtonStyle with the given fields replaced with
  /// the new values.
  ButtonStyle copyWith({
265 266 267 268 269 270 271 272
    MaterialStateProperty<TextStyle?>? textStyle,
    MaterialStateProperty<Color?>? backgroundColor,
    MaterialStateProperty<Color?>? foregroundColor,
    MaterialStateProperty<Color?>? overlayColor,
    MaterialStateProperty<Color?>? shadowColor,
    MaterialStateProperty<double?>? elevation,
    MaterialStateProperty<EdgeInsetsGeometry?>? padding,
    MaterialStateProperty<Size?>? minimumSize,
273
    MaterialStateProperty<Size?>? fixedSize,
274
    MaterialStateProperty<Size?>? maximumSize,
275 276 277
    MaterialStateProperty<BorderSide?>? side,
    MaterialStateProperty<OutlinedBorder?>? shape,
    MaterialStateProperty<MouseCursor?>? mouseCursor,
278 279 280 281
    VisualDensity? visualDensity,
    MaterialTapTargetSize? tapTargetSize,
    Duration? animationDuration,
    bool? enableFeedback,
282
    AlignmentGeometry? alignment,
283
    InteractiveInkFeatureFactory? splashFactory,
284 285 286 287 288 289 290 291 292 293
  }) {
    return ButtonStyle(
      textStyle: textStyle ?? this.textStyle,
      backgroundColor: backgroundColor ?? this.backgroundColor,
      foregroundColor: foregroundColor ?? this.foregroundColor,
      overlayColor: overlayColor ?? this.overlayColor,
      shadowColor: shadowColor ?? this.shadowColor,
      elevation: elevation ?? this.elevation,
      padding: padding ?? this.padding,
      minimumSize: minimumSize ?? this.minimumSize,
294
      fixedSize: fixedSize ?? this.fixedSize,
295
      maximumSize: maximumSize ?? this.maximumSize,
296 297 298 299 300 301 302
      side: side ?? this.side,
      shape: shape ?? this.shape,
      mouseCursor: mouseCursor ?? this.mouseCursor,
      visualDensity: visualDensity ?? this.visualDensity,
      tapTargetSize: tapTargetSize ?? this.tapTargetSize,
      animationDuration: animationDuration ?? this.animationDuration,
      enableFeedback: enableFeedback ?? this.enableFeedback,
303
      alignment: alignment ?? this.alignment,
304
      splashFactory: splashFactory ?? this.splashFactory,
305 306 307 308 309 310 311 312
    );
  }

  /// Returns a copy of this ButtonStyle where the non-null fields in [style]
  /// have replaced the corresponding null fields in this ButtonStyle.
  ///
  /// In other words, [style] is used to fill in unspecified (null) fields
  /// this ButtonStyle.
313
  ButtonStyle merge(ButtonStyle? style) {
314 315 316 317 318 319 320 321 322 323 324
    if (style == null)
      return this;
    return copyWith(
      textStyle: textStyle ?? style.textStyle,
      backgroundColor: backgroundColor ?? style.backgroundColor,
      foregroundColor: foregroundColor ?? style.foregroundColor,
      overlayColor: overlayColor ?? style.overlayColor,
      shadowColor: shadowColor ?? style.shadowColor,
      elevation: elevation ?? style.elevation,
      padding: padding ?? style.padding,
      minimumSize: minimumSize ?? style.minimumSize,
325
      fixedSize: fixedSize ?? style.fixedSize,
326
      maximumSize: maximumSize ?? style.maximumSize,
327 328 329 330 331 332 333
      side: side ?? style.side,
      shape: shape ?? style.shape,
      mouseCursor: mouseCursor ?? style.mouseCursor,
      visualDensity: visualDensity ?? style.visualDensity,
      tapTargetSize: tapTargetSize ?? style.tapTargetSize,
      animationDuration: animationDuration ?? style.animationDuration,
      enableFeedback: enableFeedback ?? style.enableFeedback,
334
      alignment: alignment ?? style.alignment,
335
      splashFactory: splashFactory ?? style.splashFactory,
336 337 338 339 340 341 342 343 344 345 346 347 348 349
    );
  }

  @override
  int get hashCode {
    return hashValues(
      textStyle,
      backgroundColor,
      foregroundColor,
      overlayColor,
      shadowColor,
      elevation,
      padding,
      minimumSize,
350
      fixedSize,
351
      maximumSize,
352 353 354 355 356 357 358
      side,
      shape,
      mouseCursor,
      visualDensity,
      tapTargetSize,
      animationDuration,
      enableFeedback,
359
      alignment,
360
      splashFactory,
361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378
    );
  }

  @override
  bool operator ==(Object other) {
    if (identical(this, other))
      return true;
    if (other.runtimeType != runtimeType)
      return false;
    return other is ButtonStyle
        && other.textStyle == textStyle
        && other.backgroundColor == backgroundColor
        && other.foregroundColor == foregroundColor
        && other.overlayColor == overlayColor
        && other.shadowColor == shadowColor
        && other.elevation == elevation
        && other.padding == padding
        && other.minimumSize == minimumSize
379
        && other.fixedSize == fixedSize
380
        && other.maximumSize == maximumSize
381 382 383 384 385 386
        && other.side == side
        && other.shape == shape
        && other.mouseCursor == mouseCursor
        && other.visualDensity == visualDensity
        && other.tapTargetSize == tapTargetSize
        && other.animationDuration == animationDuration
387
        && other.enableFeedback == enableFeedback
388 389
        && other.alignment == alignment
        && other.splashFactory == splashFactory;
390 391 392 393 394
  }

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
395 396 397 398 399 400 401 402
    properties.add(DiagnosticsProperty<MaterialStateProperty<TextStyle?>>('textStyle', textStyle, defaultValue: null));
    properties.add(DiagnosticsProperty<MaterialStateProperty<Color?>>('backgroundColor', backgroundColor, defaultValue: null));
    properties.add(DiagnosticsProperty<MaterialStateProperty<Color?>>('foregroundColor', foregroundColor, defaultValue: null));
    properties.add(DiagnosticsProperty<MaterialStateProperty<Color?>>('overlayColor', overlayColor, defaultValue: null));
    properties.add(DiagnosticsProperty<MaterialStateProperty<Color?>>('shadowColor', shadowColor, defaultValue: null));
    properties.add(DiagnosticsProperty<MaterialStateProperty<double?>>('elevation', elevation, defaultValue: null));
    properties.add(DiagnosticsProperty<MaterialStateProperty<EdgeInsetsGeometry?>>('padding', padding, defaultValue: null));
    properties.add(DiagnosticsProperty<MaterialStateProperty<Size?>>('minimumSize', minimumSize, defaultValue: null));
403
    properties.add(DiagnosticsProperty<MaterialStateProperty<Size?>>('fixedSize', fixedSize, defaultValue: null));
404
    properties.add(DiagnosticsProperty<MaterialStateProperty<Size?>>('maximumSize', maximumSize, defaultValue: null));
405 406 407
    properties.add(DiagnosticsProperty<MaterialStateProperty<BorderSide?>>('side', side, defaultValue: null));
    properties.add(DiagnosticsProperty<MaterialStateProperty<OutlinedBorder?>>('shape', shape, defaultValue: null));
    properties.add(DiagnosticsProperty<MaterialStateProperty<MouseCursor?>>('mouseCursor', mouseCursor, defaultValue: null));
408 409 410 411
    properties.add(DiagnosticsProperty<VisualDensity>('visualDensity', visualDensity, defaultValue: null));
    properties.add(EnumProperty<MaterialTapTargetSize>('tapTargetSize', tapTargetSize, defaultValue: null));
    properties.add(DiagnosticsProperty<Duration>('animationDuration', animationDuration, defaultValue: null));
    properties.add(DiagnosticsProperty<bool>('enableFeedback', enableFeedback, defaultValue: null));
412
    properties.add(DiagnosticsProperty<AlignmentGeometry>('alignment', alignment, defaultValue: null));
413 414 415
  }

  /// Linearly interpolate between two [ButtonStyle]s.
416
  static ButtonStyle? lerp(ButtonStyle? a, ButtonStyle? b, double t) {
417 418 419 420
    assert (t != null);
    if (a == null && b == null)
      return null;
    return ButtonStyle(
421 422 423 424 425 426 427 428
      textStyle: _lerpProperties<TextStyle?>(a?.textStyle, b?.textStyle, t, TextStyle.lerp),
      backgroundColor: _lerpProperties<Color?>(a?.backgroundColor, b?.backgroundColor, t, Color.lerp),
      foregroundColor:  _lerpProperties<Color?>(a?.foregroundColor, b?.foregroundColor, t, Color.lerp),
      overlayColor: _lerpProperties<Color?>(a?.overlayColor, b?.overlayColor, t, Color.lerp),
      shadowColor: _lerpProperties<Color?>(a?.shadowColor, b?.shadowColor, t, Color.lerp),
      elevation: _lerpProperties<double?>(a?.elevation, b?.elevation, t, lerpDouble),
      padding:  _lerpProperties<EdgeInsetsGeometry?>(a?.padding, b?.padding, t, EdgeInsetsGeometry.lerp),
      minimumSize: _lerpProperties<Size?>(a?.minimumSize, b?.minimumSize, t, Size.lerp),
429
      fixedSize: _lerpProperties<Size?>(a?.fixedSize, b?.fixedSize, t, Size.lerp),
430
      maximumSize: _lerpProperties<Size?>(a?.maximumSize, b?.maximumSize, t, Size.lerp),
431 432
      side: _lerpSides(a?.side, b?.side, t),
      shape: _lerpShapes(a?.shape, b?.shape, t),
433 434 435 436 437
      mouseCursor: t < 0.5 ? a?.mouseCursor : b?.mouseCursor,
      visualDensity: t < 0.5 ? a?.visualDensity : b?.visualDensity,
      tapTargetSize: t < 0.5 ? a?.tapTargetSize : b?.tapTargetSize,
      animationDuration: t < 0.5 ? a?.animationDuration : b?.animationDuration,
      enableFeedback: t < 0.5 ? a?.enableFeedback : b?.enableFeedback,
438
      alignment: AlignmentGeometry.lerp(a?.alignment, b?.alignment, t),
439
      splashFactory: t < 0.5 ? a?.splashFactory : b?.splashFactory,
440 441 442
    );
  }

443
  static MaterialStateProperty<T?>? _lerpProperties<T>(MaterialStateProperty<T>? a, MaterialStateProperty<T>? b, double t, T? Function(T?, T?, double) lerpFunction ) {
444 445 446 447 448 449 450
    // Avoid creating a _LerpProperties object for a common case.
    if (a == null && b == null)
      return null;
    return _LerpProperties<T>(a, b, t, lerpFunction);
  }

  // Special case because BorderSide.lerp() doesn't support null arguments
451
  static MaterialStateProperty<BorderSide?>? _lerpSides(MaterialStateProperty<BorderSide?>? a, MaterialStateProperty<BorderSide?>? b, double t) {
452 453 454 455 456 457
    if (a == null && b == null)
      return null;
    return _LerpSides(a, b, t);
  }

  // TODO(hansmuller): OutlinedBorder needs a lerp method - https://github.com/flutter/flutter/issues/60555.
458
  static MaterialStateProperty<OutlinedBorder?>? _lerpShapes(MaterialStateProperty<OutlinedBorder?>? a, MaterialStateProperty<OutlinedBorder?>? b, double t) {
459 460 461 462 463 464
    if (a == null && b == null)
      return null;
    return _LerpShapes(a, b, t);
  }
}

465
class _LerpProperties<T> implements MaterialStateProperty<T?> {
466 467
  const _LerpProperties(this.a, this.b, this.t, this.lerpFunction);

468 469
  final MaterialStateProperty<T>? a;
  final MaterialStateProperty<T>? b;
470
  final double t;
471
  final T? Function(T?, T?, double) lerpFunction;
472 473

  @override
474 475 476
  T? resolve(Set<MaterialState> states) {
    final T? resolvedA = a?.resolve(states);
    final T? resolvedB = b?.resolve(states);
477 478 479 480
    return lerpFunction(resolvedA, resolvedB, t);
  }
}

481
class _LerpSides implements MaterialStateProperty<BorderSide?> {
482 483
  const _LerpSides(this.a, this.b, this.t);

484 485
  final MaterialStateProperty<BorderSide?>? a;
  final MaterialStateProperty<BorderSide?>? b;
486 487 488
  final double t;

  @override
489 490 491
  BorderSide? resolve(Set<MaterialState> states) {
    final BorderSide? resolvedA = a?.resolve(states);
    final BorderSide? resolvedB = b?.resolve(states);
492 493 494
    if (resolvedA == null && resolvedB == null)
      return null;
    if (resolvedA == null)
495
      return BorderSide.lerp(BorderSide(width: 0, color: resolvedB!.color.withAlpha(0)), resolvedB, t);
496
    if (resolvedB == null)
497
      return BorderSide.lerp(resolvedA, BorderSide(width: 0, color: resolvedA.color.withAlpha(0)), t);
498 499 500 501
    return BorderSide.lerp(resolvedA, resolvedB, t);
  }
}

502
class _LerpShapes implements MaterialStateProperty<OutlinedBorder?> {
503 504
  const _LerpShapes(this.a, this.b, this.t);

505 506
  final MaterialStateProperty<OutlinedBorder?>? a;
  final MaterialStateProperty<OutlinedBorder?>? b;
507 508 509
  final double t;

  @override
510 511 512 513
  OutlinedBorder? resolve(Set<MaterialState> states) {
    final OutlinedBorder? resolvedA = a?.resolve(states);
    final OutlinedBorder? resolvedB = b?.resolve(states);
    return ShapeBorder.lerp(resolvedA, resolvedB, t) as OutlinedBorder?;
514 515
  }
}