elevated_button.dart 16.5 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14
// 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:math' as math;
import 'dart:ui' show lerpDouble;

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

import 'button_style.dart';
import 'button_style_button.dart';
import 'color_scheme.dart';
import 'constants.dart';
15
import 'elevated_button_theme.dart';
16 17
import 'ink_ripple.dart';
import 'ink_well.dart';
18 19 20 21
import 'material_state.dart';
import 'theme.dart';
import 'theme_data.dart';

22
/// A Material Design "elevated button".
23
///
24
/// Use elevated buttons to add dimension to otherwise mostly flat
25
/// layouts, e.g.  in long busy lists of content, or in wide
26
/// spaces. Avoid using elevated buttons on already-elevated content
27 28
/// such as dialogs or cards.
///
29
/// An elevated button is a label [child] displayed on a [Material]
30 31
/// widget whose [Material.elevation] increases when the button is
/// pressed. The label's [Text] and [Icon] widgets are displayed in
32
/// [style]'s [ButtonStyle.foregroundColor] and the button's filled
33 34
/// background is the [ButtonStyle.backgroundColor].
///
35 36
/// The elevated button's default style is defined by
/// [defaultStyleOf].  The style of this elevated button can be
37
/// overridden with its [style] parameter. The style of all elevated
38
/// buttons in a subtree can be overridden with the
39
/// [ElevatedButtonTheme], and the style of all of the elevated
40
/// buttons in an app can be overridden with the [Theme]'s
41
/// [ThemeData.elevatedButtonTheme] property.
42 43
///
/// The static [styleFrom] method is a convenient way to create a
44
/// elevated button [ButtonStyle] from simple values.
45 46 47 48
///
/// If [onPressed] and [onLongPress] callbacks are null, then the
/// button will be disabled.
///
49
/// {@tool dartpad}
50 51
/// This sample produces an enabled and a disabled ElevatedButton.
///
52
/// ** See code in examples/api/lib/material/elevated_button/elevated_button.0.dart **
53 54
/// {@end-tool}
///
55 56 57 58 59
/// See also:
///
///  * [TextButton], a simple flat button without a shadow.
///  * [OutlinedButton], a [TextButton] with a border outline.
///  * <https://material.io/design/components/buttons.html>
60 61
class ElevatedButton extends ButtonStyleButton {
  /// Create an ElevatedButton.
62 63
  ///
  /// The [autofocus] and [clipBehavior] arguments must not be null.
64
  const ElevatedButton({
65 66 67
    Key? key,
    required VoidCallback? onPressed,
    VoidCallback? onLongPress,
68 69
    ValueChanged<bool>? onHover,
    ValueChanged<bool>? onFocusChange,
70 71
    ButtonStyle? style,
    FocusNode? focusNode,
72 73
    bool autofocus = false,
    Clip clipBehavior = Clip.none,
74
    required Widget? child,
75 76 77 78
  }) : super(
    key: key,
    onPressed: onPressed,
    onLongPress: onLongPress,
79 80
    onHover: onHover,
    onFocusChange: onFocusChange,
81 82 83 84 85 86 87
    style: style,
    focusNode: focusNode,
    autofocus: autofocus,
    clipBehavior: clipBehavior,
    child: child,
  );

88
  /// Create an elevated button from a pair of widgets that serve as the button's
89 90 91 92 93 94
  /// [icon] and [label].
  ///
  /// The icon and label are arranged in a row and padded by 12 logical pixels
  /// at the start, and 16 at the end, with an 8 pixel gap in between.
  ///
  /// The [icon] and [label] arguments must not be null.
95
  factory ElevatedButton.icon({
96 97 98
    Key? key,
    required VoidCallback? onPressed,
    VoidCallback? onLongPress,
99 100
    ValueChanged<bool>? onHover,
    ValueChanged<bool>? onFocusChange,
101 102 103 104
    ButtonStyle? style,
    FocusNode? focusNode,
    bool? autofocus,
    Clip? clipBehavior,
105 106
    required Widget icon,
    required Widget label,
107
  }) = _ElevatedButtonWithIcon;
108

109
  /// A static convenience method that constructs an elevated button
110 111
  /// [ButtonStyle] given simple values.
  ///
Jan Mewes's avatar
Jan Mewes committed
112
  /// The [onPrimary], and [onSurface] colors are used to create a
113 114
  /// [MaterialStateProperty] [ButtonStyle.foregroundColor] value in the same
  /// way that [defaultStyleOf] uses the [ColorScheme] colors with the same
115
  /// names. Specify a value for [onPrimary] to specify the color of the
116 117 118 119
  /// button's text and icons as well as the overlay colors used to indicate the
  /// hover, focus, and pressed states. Use [primary] for the button's background
  /// fill color and [onSurface] to specify the button's disabled text, icon,
  /// and fill color.
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136
  ///
  /// The button's elevations are defined relative to the [elevation]
  /// parameter. The disabled elevation is the same as the parameter
  /// value, [elevation] + 2 is used when the button is hovered
  /// or focused, and elevation + 6 is used when the button is pressed.
  ///
  /// Similarly, the [enabledMouseCursor] and [disabledMouseCursor]
  /// parameters are used to construct [ButtonStyle].mouseCursor.
  ///
  /// All of the other parameters are either used directly or used to
  /// create a [MaterialStateProperty] with a single value for all
  /// states.
  ///
  /// All parameters default to null, by default this method returns
  /// a [ButtonStyle] that doesn't override anything.
  ///
  /// For example, to override the default text and icon colors for a
137
  /// [ElevatedButton], as well as its overlay color, with all of the
138 139 140 141
  /// standard opacity adjustments for the pressed, focused, and
  /// hovered states, one could write:
  ///
  /// ```dart
142
  /// ElevatedButton(
143
  ///   style: ElevatedButton.styleFrom(primary: Colors.green),
144 145 146
  /// )
  /// ```
  static ButtonStyle styleFrom({
147 148 149 150 151 152 153 154
    Color? primary,
    Color? onPrimary,
    Color? onSurface,
    Color? shadowColor,
    double? elevation,
    TextStyle? textStyle,
    EdgeInsetsGeometry? padding,
    Size? minimumSize,
155
    Size? fixedSize,
156
    Size? maximumSize,
157 158 159 160 161 162 163 164
    BorderSide? side,
    OutlinedBorder? shape,
    MouseCursor? enabledMouseCursor,
    MouseCursor? disabledMouseCursor,
    VisualDensity? visualDensity,
    MaterialTapTargetSize? tapTargetSize,
    Duration? animationDuration,
    bool? enableFeedback,
165
    AlignmentGeometry? alignment,
166
    InteractiveInkFeatureFactory? splashFactory,
167
  }) {
168
    final MaterialStateProperty<Color?>? backgroundColor = (onSurface == null && primary == null)
169
      ? null
170
      : _ElevatedButtonDefaultBackground(primary, onSurface);
171
    final MaterialStateProperty<Color?>? foregroundColor = (onSurface == null && onPrimary == null)
172
      ? null
173
      : _ElevatedButtonDefaultForeground(onPrimary, onSurface);
174
    final MaterialStateProperty<Color?>? overlayColor = (onPrimary == null)
175
      ? null
176
      : _ElevatedButtonDefaultOverlay(onPrimary);
177
    final MaterialStateProperty<double>? elevationValue = (elevation == null)
178
      ? null
179
      : _ElevatedButtonDefaultElevation(elevation);
180
    final MaterialStateProperty<MouseCursor?>? mouseCursor = (enabledMouseCursor == null && disabledMouseCursor == null)
181
      ? null
182
      : _ElevatedButtonDefaultMouseCursor(enabledMouseCursor, disabledMouseCursor);
183 184

    return ButtonStyle(
185
      textStyle: MaterialStateProperty.all<TextStyle?>(textStyle),
186 187 188 189 190 191 192
      backgroundColor: backgroundColor,
      foregroundColor: foregroundColor,
      overlayColor: overlayColor,
      shadowColor: ButtonStyleButton.allOrNull<Color>(shadowColor),
      elevation: elevationValue,
      padding: ButtonStyleButton.allOrNull<EdgeInsetsGeometry>(padding),
      minimumSize: ButtonStyleButton.allOrNull<Size>(minimumSize),
193
      fixedSize: ButtonStyleButton.allOrNull<Size>(fixedSize),
194
      maximumSize: ButtonStyleButton.allOrNull<Size>(maximumSize),
195 196 197 198 199 200 201
      side: ButtonStyleButton.allOrNull<BorderSide>(side),
      shape: ButtonStyleButton.allOrNull<OutlinedBorder>(shape),
      mouseCursor: mouseCursor,
      visualDensity: visualDensity,
      tapTargetSize: tapTargetSize,
      animationDuration: animationDuration,
      enableFeedback: enableFeedback,
202
      alignment: alignment,
203
      splashFactory: splashFactory,
204 205 206 207 208 209 210 211 212 213 214 215 216 217 218
    );
  }

  /// Defines the button's default appearance.
  ///
  /// The button [child]'s [Text] and [Icon] widgets are rendered with
  /// the [ButtonStyle]'s foreground color. The button's [InkWell] adds
  /// the style's overlay color when the button is focused, hovered
  /// or pressed. The button's background color becomes its [Material]
  /// color.
  ///
  /// All of the ButtonStyle's defaults appear below. In this list
  /// "Theme.foo" is shorthand for `Theme.of(context).foo`. Color
  /// scheme values like "onSurface(0.38)" are shorthand for
  /// `onSurface.withOpacity(0.38)`. [MaterialStateProperty] valued
nt4f04uNd's avatar
nt4f04uNd committed
219
  /// properties that are not followed by a sublist have the same
220 221 222 223 224 225 226 227
  /// value for all states, otherwise the values are as specified for
  /// each state, and "others" means all other states.
  ///
  /// The `textScaleFactor` is the value of
  /// `MediaQuery.of(context).textScaleFactor` and the names of the
  /// EdgeInsets constructors and `EdgeInsetsGeometry.lerp` have been
  /// abbreviated for readability.
  ///
228 229
  /// The color of the [ButtonStyle.textStyle] is not used, the
  /// [ButtonStyle.foregroundColor] color is used instead.
230 231 232 233 234 235 236 237 238 239 240
  ///
  /// * `textStyle` - Theme.textTheme.button
  /// * `backgroundColor`
  ///   * disabled - Theme.colorScheme.onSurface(0.12)
  ///   * others - Theme.colorScheme.primary
  /// * `foregroundColor`
  ///   * disabled - Theme.colorScheme.onSurface(0.38)
  ///   * others - Theme.colorScheme.onPrimary
  /// * `overlayColor`
  ///   * hovered - Theme.colorScheme.onPrimary(0.08)
  ///   * focused or pressed - Theme.colorScheme.onPrimary(0.24)
241
  /// * `shadowColor` - Theme.shadowColor
242 243
  /// * `elevation`
  ///   * disabled - 0
244 245 246
  ///   * default - 2
  ///   * hovered or focused - 4
  ///   * pressed - 8
247 248 249 250 251 252
  /// * `padding`
  ///   * textScaleFactor <= 1 - horizontal(16)
  ///   * `1 < textScaleFactor <= 2` - lerp(horizontal(16), horizontal(8))
  ///   * `2 < textScaleFactor <= 3` - lerp(horizontal(8), horizontal(4))
  ///   * `3 < textScaleFactor` - horizontal(4)
  /// * `minimumSize` - Size(64, 36)
253
  /// * `fixedSize` - null
254
  /// * `maximumSize` - Size.infinite
255
  /// * `side` - null
256 257
  /// * `shape` - RoundedRectangleBorder(borderRadius: BorderRadius.circular(4))
  /// * `mouseCursor`
258
  ///   * disabled - SystemMouseCursors.basic
259 260 261 262 263
  ///   * others - SystemMouseCursors.click
  /// * `visualDensity` - theme.visualDensity
  /// * `tapTargetSize` - theme.materialTapTargetSize
  /// * `animationDuration` - kThemeChangeDuration
  /// * `enableFeedback` - true
264
  /// * `alignment` - Alignment.center
265
  /// * `splashFactory` - InkRipple.splashFactory
266
  ///
267
  /// The default padding values for the [ElevatedButton.icon] factory are slightly different:
268 269 270 271 272 273
  ///
  /// * `padding`
  ///   * `textScaleFactor <= 1` - start(12) end(16)
  ///   * `1 < textScaleFactor <= 2` - lerp(start(12) end(16), horizontal(8))
  ///   * `2 < textScaleFactor <= 3` - lerp(horizontal(8), horizontal(4))
  ///   * `3 < textScaleFactor` - horizontal(4)
274 275 276 277 278
  ///
  /// The default value for `side`, which defines the appearance of the button's
  /// outline, is null. That means that the outline is defined by the button
  /// shape's [OutlinedBorder.side]. Typically the default value of an
  /// [OutlinedBorder]'s side is [BorderSide.none], so an outline is not drawn.
279 280
  @override
  ButtonStyle defaultStyleOf(BuildContext context) {
281
    final ThemeData theme = Theme.of(context);
282 283 284 285 286 287
    final ColorScheme colorScheme = theme.colorScheme;

    final EdgeInsetsGeometry scaledPadding = ButtonStyleButton.scaledPadding(
      const EdgeInsets.symmetric(horizontal: 16),
      const EdgeInsets.symmetric(horizontal: 8),
      const EdgeInsets.symmetric(horizontal: 4),
288
      MediaQuery.maybeOf(context)?.textScaleFactor ?? 1,
289 290 291 292 293 294
    );

    return styleFrom(
      primary: colorScheme.primary,
      onPrimary: colorScheme.onPrimary,
      onSurface: colorScheme.onSurface,
295
      shadowColor: theme.shadowColor,
296 297 298 299
      elevation: 2,
      textStyle: theme.textTheme.button,
      padding: scaledPadding,
      minimumSize: const Size(64, 36),
300
      maximumSize: Size.infinite,
301 302
      shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4))),
      enabledMouseCursor: SystemMouseCursors.click,
303
      disabledMouseCursor: SystemMouseCursors.basic,
304 305 306 307
      visualDensity: theme.visualDensity,
      tapTargetSize: theme.materialTapTargetSize,
      animationDuration: kThemeChangeDuration,
      enableFeedback: true,
308
      alignment: Alignment.center,
309
      splashFactory: InkRipple.splashFactory,
310 311 312
    );
  }

313 314
  /// Returns the [ElevatedButtonThemeData.style] of the closest
  /// [ElevatedButtonTheme] ancestor.
315
  @override
316 317
  ButtonStyle? themeStyleOf(BuildContext context) {
    return ElevatedButtonTheme.of(context).style;
318 319 320 321
  }
}

@immutable
322
class _ElevatedButtonDefaultBackground extends MaterialStateProperty<Color?> with Diagnosticable {
323
  _ElevatedButtonDefaultBackground(this.primary, this.onSurface);
324

325 326
  final Color? primary;
  final Color? onSurface;
327 328

  @override
329
  Color? resolve(Set<MaterialState> states) {
330 331 332 333 334 335 336
    if (states.contains(MaterialState.disabled))
      return onSurface?.withOpacity(0.12);
    return primary;
  }
}

@immutable
337
class _ElevatedButtonDefaultForeground extends MaterialStateProperty<Color?> with Diagnosticable {
338
  _ElevatedButtonDefaultForeground(this.onPrimary, this.onSurface);
339

340 341
  final Color? onPrimary;
  final Color? onSurface;
342 343

  @override
344
  Color? resolve(Set<MaterialState> states) {
345 346 347 348 349 350 351
    if (states.contains(MaterialState.disabled))
      return onSurface?.withOpacity(0.38);
    return onPrimary;
  }
}

@immutable
352
class _ElevatedButtonDefaultOverlay extends MaterialStateProperty<Color?> with Diagnosticable {
353
  _ElevatedButtonDefaultOverlay(this.onPrimary);
354 355 356 357

  final Color onPrimary;

  @override
358
  Color? resolve(Set<MaterialState> states) {
359
    if (states.contains(MaterialState.hovered))
360
      return onPrimary.withOpacity(0.08);
361
    if (states.contains(MaterialState.focused) || states.contains(MaterialState.pressed))
362
      return onPrimary.withOpacity(0.24);
363 364 365 366 367
    return null;
  }
}

@immutable
368 369
class _ElevatedButtonDefaultElevation extends MaterialStateProperty<double> with Diagnosticable {
  _ElevatedButtonDefaultElevation(this.elevation);
370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387

  final double elevation;

  @override
  double resolve(Set<MaterialState> states) {
    if (states.contains(MaterialState.disabled))
      return 0;
    if (states.contains(MaterialState.hovered))
      return elevation + 2;
    if (states.contains(MaterialState.focused))
      return elevation + 2;
    if (states.contains(MaterialState.pressed))
      return elevation + 6;
    return elevation;
  }
}

@immutable
388
class _ElevatedButtonDefaultMouseCursor extends MaterialStateProperty<MouseCursor?> with Diagnosticable {
389
  _ElevatedButtonDefaultMouseCursor(this.enabledCursor, this.disabledCursor);
390

391 392
  final MouseCursor? enabledCursor;
  final MouseCursor? disabledCursor;
393 394

  @override
395
  MouseCursor? resolve(Set<MaterialState> states) {
396 397 398 399 400 401
    if (states.contains(MaterialState.disabled))
      return disabledCursor;
    return enabledCursor;
  }
}

402 403
class _ElevatedButtonWithIcon extends ElevatedButton {
  _ElevatedButtonWithIcon({
404 405 406
    Key? key,
    required VoidCallback? onPressed,
    VoidCallback? onLongPress,
407 408
    ValueChanged<bool>? onHover,
    ValueChanged<bool>? onFocusChange,
409 410 411 412 413 414
    ButtonStyle? style,
    FocusNode? focusNode,
    bool? autofocus,
    Clip? clipBehavior,
    required Widget icon,
    required Widget label,
415 416 417 418 419 420
  }) : assert(icon != null),
       assert(label != null),
       super(
         key: key,
         onPressed: onPressed,
         onLongPress: onLongPress,
421 422
         onHover: onHover,
         onFocusChange: onFocusChange,
423 424 425 426 427 428 429 430 431 432 433 434 435
         style: style,
         focusNode: focusNode,
         autofocus: autofocus ?? false,
         clipBehavior: clipBehavior ?? Clip.none,
         child: _ElevatedButtonWithIconChild(icon: icon, label: label),
      );

  @override
  ButtonStyle defaultStyleOf(BuildContext context) {
    final EdgeInsetsGeometry scaledPadding = ButtonStyleButton.scaledPadding(
      const EdgeInsetsDirectional.fromSTEB(12, 0, 16, 0),
      const EdgeInsets.symmetric(horizontal: 8),
      const EdgeInsetsDirectional.fromSTEB(8, 0, 4, 0),
436
      MediaQuery.maybeOf(context)?.textScaleFactor ?? 1,
437 438
    );
    return super.defaultStyleOf(context).copyWith(
439
      padding: MaterialStateProperty.all<EdgeInsetsGeometry>(scaledPadding),
440 441 442 443 444
    );
  }
}

class _ElevatedButtonWithIconChild extends StatelessWidget {
445
  const _ElevatedButtonWithIconChild({ Key? key, required this.label, required this.icon }) : super(key: key);
446 447 448 449 450 451

  final Widget label;
  final Widget icon;

  @override
  Widget build(BuildContext context) {
452
    final double scale = MediaQuery.maybeOf(context)?.textScaleFactor ?? 1;
453
    final double gap = scale <= 1 ? 8 : lerpDouble(8, 4, math.min(scale - 1, 1))!;
454 455
    return Row(
      mainAxisSize: MainAxisSize.min,
456
      children: <Widget>[icon, SizedBox(width: gap), Flexible(child: label)],
457 458 459
    );
  }
}