outlined_button.dart 18.2 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
// 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 'colors.dart';
import 'constants.dart';
16 17
import 'ink_ripple.dart';
import 'ink_well.dart';
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
import 'material_state.dart';
import 'outlined_button_theme.dart';
import 'theme.dart';
import 'theme_data.dart';

/// A Material Design "Outlined Button"; essentially a [TextButton]
/// with an outlined border.
///
/// Outlined buttons are medium-emphasis buttons. They contain actions
/// that are important, but they aren’t the primary action in an app.
///
/// An outlined button is a label [child] displayed on a (zero
/// elevation) [Material] widget. The label's [Text] and [Icon]
/// widgets are displayed in the [style]'s
/// [ButtonStyle.foregroundColor] and the outline's weight and color
/// are defined by [ButtonStyle.side].  The button reacts to touches
34
/// by filling with the [style]'s [ButtonStyle.overlayColor].
35 36 37 38 39 40 41 42
///
/// The outlined button's default style is defined by [defaultStyleOf].
/// The style of this outline button can be overridden with its [style]
/// parameter. The style of all text buttons in a subtree can be
/// overridden with the [OutlinedButtonTheme] and the style of all of the
/// outlined buttons in an app can be overridden with the [Theme]'s
/// [ThemeData.outlinedButtonTheme] property.
///
43 44 45 46 47 48 49 50
/// Unlike [TextButton] or [ElevatedButton], outline buttons have a
/// default [ButtonStyle.side] which defines the appearance of the
/// outline.  Because the default `side` is non-null, it
/// unconditionally overrides the shape's [OutlinedBorder.side]. In
/// other words, to specify an outlined button's shape _and_ the
/// appearance of its outline, both the [ButtonStyle.shape] and
/// [ButtonStyle.side] properties must be specified.
///
51
/// {@tool dartpad}
52 53
/// Here is an example of a basic [OutlinedButton].
///
54
/// ** See code in examples/api/lib/material/outlined_button/outlined_button.0.dart **
55 56
/// {@end-tool}
///
57 58 59 60 61
/// The static [styleFrom] method is a convenient way to create a
/// outlined button [ButtonStyle] from simple values.
///
/// See also:
///
62 63
///  * [ElevatedButton], a filled Material Design button with a shadow.
///  * [TextButton], a Material Design button without a shadow.
64 65 66 67 68 69
///  * <https://material.io/design/components/buttons.html>
class OutlinedButton extends ButtonStyleButton {
  /// Create an OutlinedButton.
  ///
  /// The [autofocus] and [clipBehavior] arguments must not be null.
  const OutlinedButton({
70 71 72 73 74 75 76 77 78 79 80
    super.key,
    required super.onPressed,
    super.onLongPress,
    super.onHover,
    super.onFocusChange,
    super.style,
    super.focusNode,
    super.autofocus = false,
    super.clipBehavior = Clip.none,
    required Widget super.child,
  });
81 82 83 84 85 86 87 88 89

  /// Create a text button from a pair of widgets that serve as the button's
  /// [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.
  factory OutlinedButton.icon({
90 91 92 93 94 95 96
    Key? key,
    required VoidCallback? onPressed,
    VoidCallback? onLongPress,
    ButtonStyle? style,
    FocusNode? focusNode,
    bool? autofocus,
    Clip? clipBehavior,
97 98
    required Widget icon,
    required Widget label,
99 100 101 102 103
  }) = _OutlinedButtonWithIcon;

  /// A static convenience method that constructs an outlined button
  /// [ButtonStyle] given simple values.
  ///
Jan Mewes's avatar
Jan Mewes committed
104
  /// The [primary], and [onSurface] colors are used to create a
105 106 107 108 109 110
  /// [MaterialStateProperty] [ButtonStyle.foregroundColor] value in the same
  /// way that [defaultStyleOf] uses the [ColorScheme] colors with the same
  /// names. Specify a value for [primary] to specify the color of the button's
  /// text and icons as well as the overlay colors used to indicate the hover,
  /// focus, and pressed states. Use [onSurface] to specify the button's
  /// disabled text and icon color.
111 112
  ///
  /// Similarly, the [enabledMouseCursor] and [disabledMouseCursor]
113
  /// parameters are used to construct [ButtonStyle.mouseCursor].
114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133
  ///
  /// 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 shape and outline for an
  /// [OutlinedButton], one could write:
  ///
  /// ```dart
  /// OutlinedButton(
  ///   style: OutlinedButton.styleFrom(
  ///      shape: StadiumBorder(),
  ///      side: BorderSide(width: 2, color: Colors.green),
  ///   ),
  /// )
  /// ```
  static ButtonStyle styleFrom({
134 135 136 137
    Color? primary,
    Color? onSurface,
    Color? backgroundColor,
    Color? shadowColor,
138
    Color? surfaceTintColor,
139 140 141 142
    double? elevation,
    TextStyle? textStyle,
    EdgeInsetsGeometry? padding,
    Size? minimumSize,
143
    Size? fixedSize,
144
    Size? maximumSize,
145 146 147 148 149 150 151 152
    BorderSide? side,
    OutlinedBorder? shape,
    MouseCursor? enabledMouseCursor,
    MouseCursor? disabledMouseCursor,
    VisualDensity? visualDensity,
    MaterialTapTargetSize? tapTargetSize,
    Duration? animationDuration,
    bool? enableFeedback,
153
    AlignmentGeometry? alignment,
154
    InteractiveInkFeatureFactory? splashFactory,
155
  }) {
156
    final MaterialStateProperty<Color?>? foregroundColor = (onSurface == null && primary == null)
157 158
      ? null
      : _OutlinedButtonDefaultForeground(primary, onSurface);
159
    final MaterialStateProperty<Color?>? overlayColor = (primary == null)
160 161
      ? null
      : _OutlinedButtonDefaultOverlay(primary);
162
    final MaterialStateProperty<MouseCursor>? mouseCursor = (enabledMouseCursor == null && disabledMouseCursor == null)
163
      ? null
164
      : _OutlinedButtonDefaultMouseCursor(enabledMouseCursor!, disabledMouseCursor!);
165 166 167 168 169 170 171

    return ButtonStyle(
      textStyle: ButtonStyleButton.allOrNull<TextStyle>(textStyle),
      foregroundColor: foregroundColor,
      backgroundColor: ButtonStyleButton.allOrNull<Color>(backgroundColor),
      overlayColor: overlayColor,
      shadowColor: ButtonStyleButton.allOrNull<Color>(shadowColor),
172
      surfaceTintColor: ButtonStyleButton.allOrNull<Color>(surfaceTintColor),
173 174 175
      elevation: ButtonStyleButton.allOrNull<double>(elevation),
      padding: ButtonStyleButton.allOrNull<EdgeInsetsGeometry>(padding),
      minimumSize: ButtonStyleButton.allOrNull<Size>(minimumSize),
176
      fixedSize: ButtonStyleButton.allOrNull<Size>(fixedSize),
177
      maximumSize: ButtonStyleButton.allOrNull<Size>(maximumSize),
178 179 180 181 182 183 184
      side: ButtonStyleButton.allOrNull<BorderSide>(side),
      shape: ButtonStyleButton.allOrNull<OutlinedBorder>(shape),
      mouseCursor: mouseCursor,
      visualDensity: visualDensity,
      tapTargetSize: tapTargetSize,
      animationDuration: animationDuration,
      enableFeedback: enableFeedback,
185
      alignment: alignment,
186
      splashFactory: splashFactory,
187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205
    );
  }

  /// Defines the button's default appearance.
  ///
  /// With the exception of [ButtonStyle.side], which defines the
  /// outline, and [ButtonStyle.padding], the returned style is the
  /// same as for [TextButton].
  ///
  /// 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 and is transparent by default.
  ///
  /// 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
206
  /// properties that are not followed by a sublist have the same
207 208 209
  /// value for all states, otherwise the values are as specified for
  /// each state and "others" means all other states.
  ///
210
  /// The color of the [ButtonStyle.textStyle] is not used, the
211
  /// [ButtonStyle.foregroundColor] is used instead.
212
  ///
213 214
  /// ## Material 2 defaults
  ///
215 216 217 218 219 220 221 222
  /// * `textStyle` - Theme.textTheme.button
  /// * `backgroundColor` - transparent
  /// * `foregroundColor`
  ///   * disabled - Theme.colorScheme.onSurface(0.38)
  ///   * others - Theme.colorScheme.primary
  /// * `overlayColor`
  ///   * hovered - Theme.colorScheme.primary(0.04)
  ///   * focused or pressed - Theme.colorScheme.primary(0.12)
223
  /// * `shadowColor` - Theme.shadowColor
224 225 226 227 228 229 230
  /// * `elevation` - 0
  /// * `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)
231
  /// * `fixedSize` - null
232
  /// * `maximumSize` - Size.infinite
233 234 235
  /// * `side` - BorderSide(width: 1, color: Theme.colorScheme.onSurface(0.12))
  /// * `shape` - RoundedRectangleBorder(borderRadius: BorderRadius.circular(4))
  /// * `mouseCursor`
236
  ///   * disabled - SystemMouseCursors.basic
237 238 239 240 241
  ///   * others - SystemMouseCursors.click
  /// * `visualDensity` - theme.visualDensity
  /// * `tapTargetSize` - theme.materialTapTargetSize
  /// * `animationDuration` - kThemeChangeDuration
  /// * `enableFeedback` - true
242
  /// * `alignment` - Alignment.center
243
  /// * `splashFactory` - InkRipple.splashFactory
244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282
  ///
  /// ## Material 3 defaults
  ///
  /// If [ThemeData.useMaterial3] is set to true the following defaults will
  /// be used:
  ///
  /// * `textStyle` - Theme.textTheme.labelLarge
  /// * `backgroundColor` - transparent
  /// * `foregroundColor`
  ///   * disabled - Theme.colorScheme.onSurface(0.38)
  ///   * others - Theme.colorScheme.primary
  /// * `overlayColor`
  ///   * hovered - Theme.colorScheme.primary(0.08)
  ///   * focused or pressed - Theme.colorScheme.primary(0.12)
  ///   * others - null
  /// * `shadowColor` - null
  /// * `surfaceTintColor` - null
  /// * `elevation` - 0
  /// * `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, 40)
  /// * `fixedSize` - null
  /// * `maximumSize` - Size.infinite
  /// * `side`
  ///   * disabled - BorderSide(color: Theme.colorScheme.onSurface(0.12))
  ///   * others - BorderSide(color: Theme.colorScheme.outline)
  /// * `shape` - StadiumBorder()
  /// * `mouseCursor`
  ///   * disabled - SystemMouseCursors.basic
  ///   * others - SystemMouseCursors.click
  /// * `visualDensity` - theme.visualDensity
  /// * `tapTargetSize` - theme.materialTapTargetSize
  /// * `animationDuration` - kThemeChangeDuration
  /// * `enableFeedback` - true
  /// * `alignment` - Alignment.center
  /// * `splashFactory` - Theme.splashFactory
283 284
  @override
  ButtonStyle defaultStyleOf(BuildContext context) {
285
    final ThemeData theme = Theme.of(context);
286 287
    final ColorScheme colorScheme = theme.colorScheme;

288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312
    return Theme.of(context).useMaterial3
      ? _TokenDefaultsM3(context)
      : styleFrom(
          primary: colorScheme.primary,
          onSurface: colorScheme.onSurface,
          backgroundColor: Colors.transparent,
          shadowColor: theme.shadowColor,
          elevation: 0,
          textStyle: theme.textTheme.button,
          padding: _scaledPadding(context),
          minimumSize: const Size(64, 36),
          maximumSize: Size.infinite,
          side: BorderSide(
            color: Theme.of(context).colorScheme.onSurface.withOpacity(0.12),
          ),
          shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4))),
          enabledMouseCursor: SystemMouseCursors.click,
          disabledMouseCursor: SystemMouseCursors.basic,
          visualDensity: theme.visualDensity,
          tapTargetSize: theme.materialTapTargetSize,
          animationDuration: kThemeChangeDuration,
          enableFeedback: true,
          alignment: Alignment.center,
          splashFactory: InkRipple.splashFactory,
        );
313 314 315
  }

  @override
316 317
  ButtonStyle? themeStyleOf(BuildContext context) {
    return OutlinedButtonTheme.of(context).style;
318 319 320
  }
}

321 322 323 324 325 326 327 328 329
EdgeInsetsGeometry _scaledPadding(BuildContext context) {
  return ButtonStyleButton.scaledPadding(
    const EdgeInsets.symmetric(horizontal: 16),
    const EdgeInsets.symmetric(horizontal: 8),
    const EdgeInsets.symmetric(horizontal: 4),
    MediaQuery.maybeOf(context)?.textScaleFactor ?? 1,
  );
}

330
@immutable
331
class _OutlinedButtonDefaultForeground extends MaterialStateProperty<Color?>  with Diagnosticable {
332 333
  _OutlinedButtonDefaultForeground(this.primary, this.onSurface);

334 335
  final Color? primary;
  final Color? onSurface;
336 337

  @override
338
  Color? resolve(Set<MaterialState> states) {
339 340 341 342 343 344 345
    if (states.contains(MaterialState.disabled))
      return onSurface?.withOpacity(0.38);
    return primary;
  }
}

@immutable
346
class _OutlinedButtonDefaultOverlay extends MaterialStateProperty<Color?> with Diagnosticable {
347 348 349 350 351
  _OutlinedButtonDefaultOverlay(this.primary);

  final Color primary;

  @override
352
  Color? resolve(Set<MaterialState> states) {
353
    if (states.contains(MaterialState.hovered))
354
      return primary.withOpacity(0.04);
355
    if (states.contains(MaterialState.focused) || states.contains(MaterialState.pressed))
356
      return primary.withOpacity(0.12);
357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377
    return null;
  }
}

@immutable
class _OutlinedButtonDefaultMouseCursor extends MaterialStateProperty<MouseCursor> with Diagnosticable {
  _OutlinedButtonDefaultMouseCursor(this.enabledCursor, this.disabledCursor);

  final MouseCursor enabledCursor;
  final MouseCursor disabledCursor;

  @override
  MouseCursor resolve(Set<MaterialState> states) {
    if (states.contains(MaterialState.disabled))
      return disabledCursor;
    return enabledCursor;
  }
}

class _OutlinedButtonWithIcon extends OutlinedButton {
  _OutlinedButtonWithIcon({
378 379 380 381 382
    super.key,
    required super.onPressed,
    super.onLongPress,
    super.style,
    super.focusNode,
383 384 385 386
    bool? autofocus,
    Clip? clipBehavior,
    required Widget icon,
    required Widget label,
387 388 389 390 391 392 393 394 395 396
  }) : assert(icon != null),
       assert(label != null),
       super(
         autofocus: autofocus ?? false,
         clipBehavior: clipBehavior ?? Clip.none,
         child: _OutlinedButtonWithIconChild(icon: icon, label: label),
      );
}

class _OutlinedButtonWithIconChild extends StatelessWidget {
397 398 399
  const _OutlinedButtonWithIconChild({
    required this.label,
    required this.icon,
400
  });
401 402 403 404 405 406

  final Widget label;
  final Widget icon;

  @override
  Widget build(BuildContext context) {
407
    final double scale = MediaQuery.maybeOf(context)?.textScaleFactor ?? 1;
408
    final double gap = scale <= 1 ? 8 : lerpDouble(8, 4, math.min(scale - 1, 1))!;
409 410
    return Row(
      mainAxisSize: MainAxisSize.min,
411
      children: <Widget>[icon, SizedBox(width: gap), Flexible(child: label)],
412 413 414
    );
  }
}
415 416 417 418 419 420 421

// BEGIN GENERATED TOKEN PROPERTIES

// Generated code to the end of this file. Do not edit by hand.
// These defaults are generated from the Material Design Token
// database by the script dev/tools/gen_defaults/bin/gen_defaults.dart.

422
// Generated version v0_98
423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 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 508 509 510 511 512 513 514
class _TokenDefaultsM3 extends ButtonStyle {
  _TokenDefaultsM3(this.context)
   : super(
       animationDuration: kThemeChangeDuration,
       enableFeedback: true,
       alignment: Alignment.center,
     );

  final BuildContext context;
  late final ColorScheme _colors = Theme.of(context).colorScheme;

  @override
  MaterialStateProperty<TextStyle?> get textStyle =>
    MaterialStateProperty.all<TextStyle?>(Theme.of(context).textTheme.labelLarge);

  @override
  MaterialStateProperty<Color?>? get backgroundColor =>
    ButtonStyleButton.allOrNull<Color>(Colors.transparent);

  @override
  MaterialStateProperty<Color?>? get foregroundColor =>
    MaterialStateProperty.resolveWith((Set<MaterialState> states) {
      if (states.contains(MaterialState.disabled))
        return _colors.onSurface.withOpacity(0.38);
      return _colors.primary;
    });

  @override
  MaterialStateProperty<Color?>? get overlayColor =>
    MaterialStateProperty.resolveWith((Set<MaterialState> states) {
      if (states.contains(MaterialState.hovered))
        return _colors.primary.withOpacity(0.08);
      if (states.contains(MaterialState.focused))
        return _colors.primary.withOpacity(0.12);
      if (states.contains(MaterialState.pressed))
        return _colors.primary.withOpacity(0.12);
      return null;
    });

  // No default shadow color

  // No default surface tint color

  @override
  MaterialStateProperty<double>? get elevation =>
    ButtonStyleButton.allOrNull<double>(0.0);

  @override
  MaterialStateProperty<EdgeInsetsGeometry>? get padding =>
    ButtonStyleButton.allOrNull<EdgeInsetsGeometry>(_scaledPadding(context));

  @override
  MaterialStateProperty<Size>? get minimumSize =>
    ButtonStyleButton.allOrNull<Size>(const Size(64.0, 40.0));

  // No default fixedSize

  @override
  MaterialStateProperty<Size>? get maximumSize =>
    ButtonStyleButton.allOrNull<Size>(Size.infinite);

  @override
  MaterialStateProperty<BorderSide>? get side =>
    MaterialStateProperty.resolveWith((Set<MaterialState> states) {
    if (states.contains(MaterialState.disabled))
      return BorderSide(color: _colors.onSurface.withOpacity(0.12));
    return BorderSide(color: _colors.outline);
  });

  @override
  MaterialStateProperty<OutlinedBorder>? get shape =>
    ButtonStyleButton.allOrNull<OutlinedBorder>(const StadiumBorder());

  @override
  MaterialStateProperty<MouseCursor?>? get mouseCursor =>
    MaterialStateProperty.resolveWith((Set<MaterialState> states) {
      if (states.contains(MaterialState.disabled))
        return SystemMouseCursors.basic;
      return SystemMouseCursors.click;
    });

  @override
  VisualDensity? get visualDensity => Theme.of(context).visualDensity;

  @override
  MaterialTapTargetSize? get tapTargetSize => Theme.of(context).materialTapTargetSize;

  @override
  InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory;
}

// END GENERATED TOKEN PROPERTIES