material_button.dart 16.9 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 6
import 'dart:ui';

7 8 9 10 11 12 13 14 15 16 17 18 19 20
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';

import 'button.dart';
import 'button_theme.dart';
import 'constants.dart';
import 'ink_well.dart';
import 'material.dart';
import 'theme.dart';
import 'theme_data.dart';

/// A utility class for building Material buttons that depend on the
/// ambient [ButtonTheme] and [Theme].
///
21 22
/// This class is planned to be deprecated in a future release.
/// Please use one or more of these buttons and associated themes instead:
23
///
24 25 26
///  * [TextButton], [TextButtonTheme], [TextButtonThemeData],
///  * [ElevatedButton], [ElevatedButtonTheme], [ElevatedButtonThemeData],
///  * [OutlinedButton], [OutlinedButtonTheme], [OutlinedButtonThemeData]
27
///
28 29
/// The button's size will expand to fit the child widget, if necessary.
///
30 31
/// MaterialButtons whose [onPressed] and [onLongPress] callbacks are null will be disabled. To have
/// an enabled button, make sure to pass a non-null value for [onPressed] or [onLongPress].
32 33 34 35 36 37 38 39 40 41 42
///
/// To create a button directly, without inheriting theme defaults, use
/// [RawMaterialButton].
///
/// If you want an ink-splash effect for taps, but don't want to use a button,
/// consider using [InkWell] directly.
///
/// See also:
///
///  * [IconButton], to create buttons that contain icons rather than text.
class MaterialButton extends StatelessWidget {
43
  /// Creates a Material Design button.
44
  ///
45 46
  /// To create a custom Material button consider using [TextButton],
  /// [ElevatedButton], or [OutlinedButton].
47 48 49 50 51
  ///
  /// The [autofocus] and [clipBehavior] arguments must not be null.
  /// Additionally,  [elevation], [hoverElevation], [focusElevation],
  /// [highlightElevation], and [disabledElevation] must be non-negative, if
  /// specified.
52
  const MaterialButton({
53
    super.key,
54
    required this.onPressed,
55
    this.onLongPress,
56
    this.onHighlightChanged,
57
    this.mouseCursor,
58 59 60 61 62
    this.textTheme,
    this.textColor,
    this.disabledTextColor,
    this.color,
    this.disabledColor,
63 64
    this.focusColor,
    this.hoverColor,
65 66 67 68
    this.highlightColor,
    this.splashColor,
    this.colorBrightness,
    this.elevation,
69 70
    this.focusElevation,
    this.hoverElevation,
71 72 73
    this.highlightElevation,
    this.disabledElevation,
    this.padding,
74
    this.visualDensity,
75 76
    this.shape,
    this.clipBehavior = Clip.none,
77
    this.focusNode,
78
    this.autofocus = false,
79 80 81 82
    this.materialTapTargetSize,
    this.animationDuration,
    this.minWidth,
    this.height,
83
    this.enableFeedback = true,
84
    this.child,
85 86
  }) : assert(clipBehavior != null),
       assert(autofocus != null),
87 88 89 90
       assert(elevation == null || elevation >= 0.0),
       assert(focusElevation == null || focusElevation >= 0.0),
       assert(hoverElevation == null || hoverElevation >= 0.0),
       assert(highlightElevation == null || highlightElevation >= 0.0),
91
       assert(disabledElevation == null || disabledElevation >= 0.0);
92 93 94

  /// The callback that is called when the button is tapped or otherwise activated.
  ///
95 96 97 98 99
  /// If this callback and [onLongPress] are null, then the button will be disabled.
  ///
  /// See also:
  ///
  ///  * [enabled], which is true if the button is enabled.
100
  final VoidCallback? onPressed;
101

102 103 104 105 106 107 108
  /// The callback that is called when the button is long-pressed.
  ///
  /// If this callback and [onPressed] are null, then the button will be disabled.
  ///
  /// See also:
  ///
  ///  * [enabled], which is true if the button is enabled.
109
  final VoidCallback? onLongPress;
110

111 112
  /// Called by the underlying [InkWell] widget's [InkWell.onHighlightChanged]
  /// callback.
113 114 115 116
  ///
  /// If [onPressed] changes from null to non-null while a gesture is ongoing,
  /// this can fire during the build phase (in which case calling
  /// [State.setState] is not allowed).
117
  final ValueChanged<bool>? onHighlightChanged;
118

119
  /// {@macro flutter.material.RawMaterialButton.mouseCursor}
120 121
  ///
  /// If this property is null, [MaterialStateMouseCursor.clickable] will be used.
122
  final MouseCursor? mouseCursor;
123

124 125 126 127
  /// Defines the button's base colors, and the defaults for the button's minimum
  /// size, internal padding, and shape.
  ///
  /// Defaults to `ButtonTheme.of(context).textTheme`.
128
  final ButtonTextTheme? textTheme;
129 130 131

  /// The color to use for this button's text.
  ///
132
  /// The button's [Material.textStyle] will be the current theme's button text
133
  /// style, [TextTheme.labelLarge] of [ThemeData.textTheme], configured with this
134
  /// color.
135 136 137 138
  ///
  /// The default text color depends on the button theme's text theme,
  /// [ButtonThemeData.textTheme].
  ///
139 140
  /// If [textColor] is a [MaterialStateProperty<Color>], [disabledTextColor]
  /// will be ignored.
141
  ///
142
  /// See also:
143 144 145
  ///
  ///  * [disabledTextColor], the text color to use when the button has been
  ///    disabled.
146
  final Color? textColor;
147 148 149

  /// The color to use for this button's text when the button is disabled.
  ///
150
  /// The button's [Material.textStyle] will be the current theme's button text
151
  /// style, [TextTheme.labelLarge] of [ThemeData.textTheme], configured with this
152
  /// color.
153 154 155 156
  ///
  /// The default value is the theme's disabled color,
  /// [ThemeData.disabledColor].
  ///
157 158
  /// If [textColor] is a [MaterialStateProperty<Color>], [disabledTextColor]
  /// will be ignored.
159
  ///
160
  /// See also:
161
  ///
162
  ///  * [textColor] - The color to use for this button's text when the button is [enabled].
163
  final Color? disabledTextColor;
164 165 166 167 168

  /// The button's fill color, displayed by its [Material], while it
  /// is in its default (unpressed, [enabled]) state.
  ///
  /// See also:
169 170
  ///
  ///  * [disabledColor] - the fill color of the button when the button is disabled.
171
  final Color? color;
172 173 174 175 176 177 178

  /// The fill color of the button when the button is disabled.
  ///
  /// The default value of this color is the theme's disabled color,
  /// [ThemeData.disabledColor].
  ///
  /// See also:
179 180
  ///
  ///  * [color] - the fill color of the button when the button is [enabled].
181
  final Color? disabledColor;
182 183 184 185 186 187 188 189 190 191 192 193

  /// The splash color of the button's [InkWell].
  ///
  /// The ink splash indicates that the button has been touched. It
  /// appears on top of the button's child and spreads in an expanding
  /// circle beginning where the touch occurred.
  ///
  /// The default splash color is the current theme's splash color,
  /// [ThemeData.splashColor].
  ///
  /// The appearance of the splash can be configured with the theme's splash
  /// factory, [ThemeData.splashFactory].
194
  final Color? splashColor;
195

196 197 198 199
  /// The fill color of the button's [Material] when it has the input focus.
  ///
  /// The button changed focus color when the button has the input focus. It
  /// appears behind the button's child.
200
  final Color? focusColor;
201 202 203 204 205 206

  /// The fill color of the button's [Material] when a pointer is hovering over
  /// it.
  ///
  /// The button changes fill color when a pointer is hovering over the button.
  /// It appears behind the button's child.
207
  final Color? hoverColor;
208

209 210 211 212 213 214 215 216 217
  /// The highlight color of the button's [InkWell].
  ///
  /// The highlight indicates that the button is actively being pressed. It
  /// appears on top of the button's child and quickly spreads to fill
  /// the button, and then fades out.
  ///
  /// If [textTheme] is [ButtonTextTheme.primary], the default highlight color is
  /// transparent (in other words the highlight doesn't appear). Otherwise it's
  /// the current theme's highlight color, [ThemeData.highlightColor].
218
  final Color? highlightColor;
219

220 221 222
  /// The z-coordinate at which to place this button relative to its parent.
  ///
  /// This controls the size of the shadow below the raised button.
223
  ///
224 225
  /// Defaults to 2, the appropriate elevation for raised buttons. The value
  /// is always non-negative.
226 227 228
  ///
  /// See also:
  ///
229
  ///  * [TextButton], a button with no elevation or fill color.
230 231 232
  ///  * [focusElevation], the elevation when the button is focused.
  ///  * [hoverElevation], the elevation when a pointer is hovering over the
  ///    button.
233 234
  ///  * [disabledElevation], the elevation when the button is disabled.
  ///  * [highlightElevation], the elevation when the button is pressed.
235
  final double? elevation;
236

237 238 239 240 241 242 243 244 245 246 247
  /// The elevation for the button's [Material] when the button
  /// is [enabled] and a pointer is hovering over it.
  ///
  /// Defaults to 4.0. The value is always non-negative.
  ///
  /// See also:
  ///
  ///  * [elevation], the default elevation.
  ///  * [focusElevation], the elevation when the button is focused.
  ///  * [disabledElevation], the elevation when the button is disabled.
  ///  * [highlightElevation], the elevation when the button is pressed.
248
  final double? hoverElevation;
249 250 251 252 253 254 255 256 257 258 259 260 261

  /// The elevation for the button's [Material] when the button
  /// is [enabled] and has the input focus.
  ///
  /// Defaults to 4.0. The value is always non-negative.
  ///
  /// See also:
  ///
  ///  * [elevation], the default elevation.
  ///  * [hoverElevation], the elevation when a pointer is hovering over the
  ///    button.
  ///  * [disabledElevation], the elevation when the button is disabled.
  ///  * [highlightElevation], the elevation when the button is pressed.
262
  final double? focusElevation;
263

264 265
  /// The elevation for the button's [Material] relative to its parent when the
  /// button is [enabled] and pressed.
266 267 268 269 270
  ///
  /// This controls the size of the shadow below the button. When a tap
  /// down gesture occurs within the button, its [InkWell] displays a
  /// [highlightColor] "highlight".
  ///
271
  /// Defaults to 8.0. The value is always non-negative.
272 273 274 275
  ///
  /// See also:
  ///
  ///  * [elevation], the default elevation.
276 277 278
  ///  * [focusElevation], the elevation when the button is focused.
  ///  * [hoverElevation], the elevation when a pointer is hovering over the
  ///    button.
279
  ///  * [disabledElevation], the elevation when the button is disabled.
280
  final double? highlightElevation;
281

282 283
  /// The elevation for the button's [Material] relative to its parent when the
  /// button is not [enabled].
284
  ///
285
  /// Defaults to 0.0. The value is always non-negative.
286 287 288 289 290
  ///
  /// See also:
  ///
  ///  * [elevation], the default elevation.
  ///  * [highlightElevation], the elevation when the button is pressed.
291
  final double? disabledElevation;
292 293 294

  /// The theme brightness to use for this button.
  ///
295 296 297 298 299 300 301
  /// Defaults to the theme's brightness in [ThemeData.brightness]. Setting
  /// this value determines the button text's colors based on
  /// [ButtonThemeData.getTextColor].
  ///
  /// See also:
  ///
  ///  * [ButtonTextTheme], uses [Brightness] to determine text color.
302
  final Brightness? colorBrightness;
303 304 305 306

  /// The button's label.
  ///
  /// Often a [Text] widget in all caps.
307
  final Widget? child;
308 309 310 311

  /// Whether the button is enabled or disabled.
  ///
  /// Buttons are disabled by default. To enable a button, set its [onPressed]
312 313
  /// or [onLongPress] properties to a non-null value.
  bool get enabled => onPressed != null || onLongPress != null;
314 315 316 317 318

  /// The internal padding for the button's [child].
  ///
  /// Defaults to the value from the current [ButtonTheme],
  /// [ButtonThemeData.padding].
319
  final EdgeInsetsGeometry? padding;
320

321 322 323 324 325 326
  /// Defines how compact the button's layout will be.
  ///
  /// {@macro flutter.material.themedata.visualDensity}
  ///
  /// See also:
  ///
327 328
  ///  * [ThemeData.visualDensity], which specifies the [visualDensity] for all
  ///    widgets within a [Theme].
329
  final VisualDensity? visualDensity;
330

331 332 333 334 335
  /// The shape of the 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.
336 337 338
  ///
  /// Defaults to the value from the current [ButtonTheme],
  /// [ButtonThemeData.shape].
339
  final ShapeBorder? shape;
340

341
  /// {@macro flutter.material.Material.clipBehavior}
342 343
  ///
  /// Defaults to [Clip.none], and must not be null.
344 345
  final Clip clipBehavior;

346
  /// {@macro flutter.widgets.Focus.focusNode}
347
  final FocusNode? focusNode;
348

349 350 351
  /// {@macro flutter.widgets.Focus.autofocus}
  final bool autofocus;

352 353 354
  /// Defines the duration of animated changes for [shape] and [elevation].
  ///
  /// The default value is [kThemeChangeDuration].
355
  final Duration? animationDuration;
356 357 358 359 360 361 362

  /// Configures the minimum size of the tap target.
  ///
  /// Defaults to [ThemeData.materialTapTargetSize].
  ///
  /// See also:
  ///
363
  ///  * [MaterialTapTargetSize], for a description of how this affects tap targets.
364
  final MaterialTapTargetSize? materialTapTargetSize;
365 366 367 368

  /// The smallest horizontal extent that the button will occupy.
  ///
  /// Defaults to the value from the current [ButtonTheme].
369
  final double? minWidth;
370 371 372 373

  /// The vertical extent of the button.
  ///
  /// Defaults to the value from the current [ButtonTheme].
374
  final double? height;
375

376 377 378 379 380 381 382 383 384 385
  /// 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.
  ///
  /// See also:
  ///
  ///  * [Feedback] for providing platform-specific feedback to certain actions.
  final bool enableFeedback;

386 387
  @override
  Widget build(BuildContext context) {
388
    final ThemeData theme = Theme.of(context);
389 390 391 392
    final ButtonThemeData buttonTheme = ButtonTheme.of(context);

    return RawMaterialButton(
      onPressed: onPressed,
393
      onLongPress: onLongPress,
394
      enableFeedback: enableFeedback,
395
      onHighlightChanged: onHighlightChanged,
396
      mouseCursor: mouseCursor,
397
      fillColor: buttonTheme.getFillColor(this),
398
      textStyle: theme.textTheme.labelLarge!.copyWith(color: buttonTheme.getTextColor(this)),
399 400
      focusColor: focusColor ?? buttonTheme.getFocusColor(this),
      hoverColor: hoverColor ?? buttonTheme.getHoverColor(this),
401 402 403
      highlightColor: highlightColor ?? theme.highlightColor,
      splashColor: splashColor ?? theme.splashColor,
      elevation: buttonTheme.getElevation(this),
404 405
      focusElevation: buttonTheme.getFocusElevation(this),
      hoverElevation: buttonTheme.getHoverElevation(this),
406 407
      highlightElevation: buttonTheme.getHighlightElevation(this),
      padding: buttonTheme.getPadding(this),
408
      visualDensity: visualDensity ?? theme.visualDensity,
409 410 411 412
      constraints: buttonTheme.getConstraints(this).copyWith(
        minWidth: minWidth,
        minHeight: height,
      ),
413
      shape: buttonTheme.getShape(this),
414
      clipBehavior: clipBehavior,
415
      focusNode: focusNode,
416
      autofocus: autofocus,
417 418
      animationDuration: buttonTheme.getAnimationDuration(this),
      materialTapTargetSize: materialTapTargetSize ?? theme.materialTapTargetSize,
419
      disabledElevation: disabledElevation ?? 0.0,
420
      child: child,
421 422 423 424 425 426
    );
  }

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
427
    properties.add(FlagProperty('enabled', value: enabled, ifFalse: 'disabled'));
428
    properties.add(DiagnosticsProperty<ButtonTextTheme>('textTheme', textTheme, defaultValue: null));
429 430 431 432 433 434 435 436
    properties.add(ColorProperty('textColor', textColor, defaultValue: null));
    properties.add(ColorProperty('disabledTextColor', disabledTextColor, defaultValue: null));
    properties.add(ColorProperty('color', color, 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));
437 438
    properties.add(DiagnosticsProperty<Brightness>('colorBrightness', colorBrightness, defaultValue: null));
    properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding, defaultValue: null));
439
    properties.add(DiagnosticsProperty<VisualDensity>('visualDensity', visualDensity, defaultValue: null));
440 441 442
    properties.add(DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: null));
    properties.add(DiagnosticsProperty<FocusNode>('focusNode', focusNode, defaultValue: null));
    properties.add(DiagnosticsProperty<MaterialTapTargetSize>('materialTapTargetSize', materialTapTargetSize, defaultValue: null));
443 444 445
  }
}

446 447 448
/// The distinguished type of [MaterialButton].
///
/// This class is deprecated and will be removed in a future release.
449 450 451
///
/// This mixin only exists to give the "label and icon" button widgets a distinct
/// type for the sake of [ButtonTheme].
452 453 454 455 456 457
@Deprecated(
  'This was used to differentiate types of FlatButton, RaisedButton, and OutlineButton in ButtonTheme. '
  'These buttons have been replaced with TextButton, ElevatedButton, and OutlinedButton, each of which have their own respective themes now. '
  'Use one of these button classes instead. '
  'This feature was deprecated after v2.11.0-0.0.pre.',
)
458
mixin MaterialButtonWithIconMixin { }