outlined_button.dart 19.3 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
///  * <https://material.io/design/components/buttons.html>
65
///  * <https://m3.material.io/components/buttons>
66 67 68 69 70
class OutlinedButton extends ButtonStyleButton {
  /// Create an OutlinedButton.
  ///
  /// The [autofocus] and [clipBehavior] arguments must not be null.
  const OutlinedButton({
71 72 73 74 75 76 77 78 79
    super.key,
    required super.onPressed,
    super.onLongPress,
    super.onHover,
    super.onFocusChange,
    super.style,
    super.focusNode,
    super.autofocus = false,
    super.clipBehavior = Clip.none,
80
    super.statesController,
81 82
    required Widget super.child,
  });
83 84 85 86 87 88 89 90 91

  /// 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({
92 93 94 95 96 97 98
    Key? key,
    required VoidCallback? onPressed,
    VoidCallback? onLongPress,
    ButtonStyle? style,
    FocusNode? focusNode,
    bool? autofocus,
    Clip? clipBehavior,
99 100
    required Widget icon,
    required Widget label,
101 102 103 104 105
  }) = _OutlinedButtonWithIcon;

  /// A static convenience method that constructs an outlined button
  /// [ButtonStyle] given simple values.
  ///
Jan Mewes's avatar
Jan Mewes committed
106
  /// The [primary], and [onSurface] colors are used to create a
107 108 109 110 111 112
  /// [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.
113 114
  ///
  /// Similarly, the [enabledMouseCursor] and [disabledMouseCursor]
115
  /// parameters are used to construct [ButtonStyle.mouseCursor].
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135
  ///
  /// 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({
136
    Color? foregroundColor,
137
    Color? backgroundColor,
138 139
    Color? disabledForegroundColor,
    Color? disabledBackgroundColor,
140
    Color? shadowColor,
141
    Color? surfaceTintColor,
142 143 144 145
    double? elevation,
    TextStyle? textStyle,
    EdgeInsetsGeometry? padding,
    Size? minimumSize,
146
    Size? fixedSize,
147
    Size? maximumSize,
148 149 150 151 152 153 154 155
    BorderSide? side,
    OutlinedBorder? shape,
    MouseCursor? enabledMouseCursor,
    MouseCursor? disabledMouseCursor,
    VisualDensity? visualDensity,
    MaterialTapTargetSize? tapTargetSize,
    Duration? animationDuration,
    bool? enableFeedback,
156
    AlignmentGeometry? alignment,
157
    InteractiveInkFeatureFactory? splashFactory,
158 159 160 161 162 163 164 165 166 167
    @Deprecated(
      'Use foregroundColor instead. '
      'This feature was deprecated after v3.1.0.'
    )
    Color? primary,
    @Deprecated(
      'Use disabledForegroundColor and disabledForegroundColor instead. '
      'This feature was deprecated after v3.1.0.'
    )
    Color? onSurface,
168
  }) {
169 170 171 172 173 174
    final Color? foreground = foregroundColor ?? primary;
    final Color? disabledForeground = disabledForegroundColor ?? onSurface?.withOpacity(0.38);
    final MaterialStateProperty<Color?>? foregroundColorProp = (foreground == null && disabledForeground == null)
      ? null
      : _OutlinedButtonDefaultColor(foreground, disabledForeground);
    final MaterialStateProperty<Color?>? backgroundColorProp = (backgroundColor == null && disabledBackgroundColor == null)
175
      ? null
176 177 178 179
      : disabledBackgroundColor == null
        ? ButtonStyleButton.allOrNull<Color?>(backgroundColor)
        : _OutlinedButtonDefaultColor(backgroundColor, disabledBackgroundColor);
    final MaterialStateProperty<Color?>? overlayColor = (foreground == null)
180
      ? null
181
      : _OutlinedButtonDefaultOverlay(foreground);
182
    final MaterialStateProperty<MouseCursor>? mouseCursor = (enabledMouseCursor == null && disabledMouseCursor == null)
183
      ? null
184
      : _OutlinedButtonDefaultMouseCursor(enabledMouseCursor!, disabledMouseCursor!);
185 186 187

    return ButtonStyle(
      textStyle: ButtonStyleButton.allOrNull<TextStyle>(textStyle),
188 189
      foregroundColor: foregroundColorProp,
      backgroundColor: backgroundColorProp,
190 191
      overlayColor: overlayColor,
      shadowColor: ButtonStyleButton.allOrNull<Color>(shadowColor),
192
      surfaceTintColor: ButtonStyleButton.allOrNull<Color>(surfaceTintColor),
193 194 195
      elevation: ButtonStyleButton.allOrNull<double>(elevation),
      padding: ButtonStyleButton.allOrNull<EdgeInsetsGeometry>(padding),
      minimumSize: ButtonStyleButton.allOrNull<Size>(minimumSize),
196
      fixedSize: ButtonStyleButton.allOrNull<Size>(fixedSize),
197
      maximumSize: ButtonStyleButton.allOrNull<Size>(maximumSize),
198 199 200 201 202 203 204
      side: ButtonStyleButton.allOrNull<BorderSide>(side),
      shape: ButtonStyleButton.allOrNull<OutlinedBorder>(shape),
      mouseCursor: mouseCursor,
      visualDensity: visualDensity,
      tapTargetSize: tapTargetSize,
      animationDuration: animationDuration,
      enableFeedback: enableFeedback,
205
      alignment: alignment,
206
      splashFactory: splashFactory,
207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225
    );
  }

  /// 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
226
  /// properties that are not followed by a sublist have the same
227 228 229
  /// value for all states, otherwise the values are as specified for
  /// each state and "others" means all other states.
  ///
230
  /// The color of the [ButtonStyle.textStyle] is not used, the
231
  /// [ButtonStyle.foregroundColor] is used instead.
232
  ///
233 234
  /// ## Material 2 defaults
  ///
235 236 237 238 239 240 241 242
  /// * `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)
243
  /// * `shadowColor` - Theme.shadowColor
244 245 246 247 248 249 250
  /// * `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)
251
  /// * `fixedSize` - null
252
  /// * `maximumSize` - Size.infinite
253 254 255
  /// * `side` - BorderSide(width: 1, color: Theme.colorScheme.onSurface(0.12))
  /// * `shape` - RoundedRectangleBorder(borderRadius: BorderRadius.circular(4))
  /// * `mouseCursor`
256
  ///   * disabled - SystemMouseCursors.basic
257 258 259 260 261
  ///   * others - SystemMouseCursors.click
  /// * `visualDensity` - theme.visualDensity
  /// * `tapTargetSize` - theme.materialTapTargetSize
  /// * `animationDuration` - kThemeChangeDuration
  /// * `enableFeedback` - true
262
  /// * `alignment` - Alignment.center
263
  /// * `splashFactory` - InkRipple.splashFactory
264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302
  ///
  /// ## 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
303 304
  @override
  ButtonStyle defaultStyleOf(BuildContext context) {
305
    final ThemeData theme = Theme.of(context);
306 307
    final ColorScheme colorScheme = theme.colorScheme;

308 309 310
    return Theme.of(context).useMaterial3
      ? _TokenDefaultsM3(context)
      : styleFrom(
311 312
          foregroundColor: colorScheme.primary,
          disabledForegroundColor: colorScheme.onSurface.withOpacity(0.38),
313
          backgroundColor: Colors.transparent,
314
          disabledBackgroundColor: Colors.transparent,
315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333
          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,
        );
334 335 336
  }

  @override
337 338
  ButtonStyle? themeStyleOf(BuildContext context) {
    return OutlinedButtonTheme.of(context).style;
339 340 341
  }
}

342 343 344 345 346 347 348 349 350
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,
  );
}

351
@immutable
352 353
class _OutlinedButtonDefaultColor extends MaterialStateProperty<Color?>  with Diagnosticable {
  _OutlinedButtonDefaultColor(this.color, this.disabled);
354

355 356
  final Color? color;
  final Color? disabled;
357 358

  @override
359
  Color? resolve(Set<MaterialState> states) {
360
    if (states.contains(MaterialState.disabled)) {
361
      return disabled;
362
    }
363
    return color;
364 365 366 367
  }
}

@immutable
368
class _OutlinedButtonDefaultOverlay extends MaterialStateProperty<Color?> with Diagnosticable {
369
  _OutlinedButtonDefaultOverlay(this.foreground);
370

371
  final Color foreground;
372 373

  @override
374
  Color? resolve(Set<MaterialState> states) {
375
    if (states.contains(MaterialState.hovered)) {
376
      return foreground.withOpacity(0.04);
377 378
    }
    if (states.contains(MaterialState.focused) || states.contains(MaterialState.pressed)) {
379
      return foreground.withOpacity(0.12);
380
    }
381 382 383 384 385 386 387 388 389 390 391 392 393
    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) {
394
    if (states.contains(MaterialState.disabled)) {
395
      return disabledCursor;
396
    }
397 398 399 400 401 402
    return enabledCursor;
  }
}

class _OutlinedButtonWithIcon extends OutlinedButton {
  _OutlinedButtonWithIcon({
403 404 405 406 407
    super.key,
    required super.onPressed,
    super.onLongPress,
    super.style,
    super.focusNode,
408 409 410 411
    bool? autofocus,
    Clip? clipBehavior,
    required Widget icon,
    required Widget label,
412 413 414 415 416 417 418 419 420 421
  }) : assert(icon != null),
       assert(label != null),
       super(
         autofocus: autofocus ?? false,
         clipBehavior: clipBehavior ?? Clip.none,
         child: _OutlinedButtonWithIconChild(icon: icon, label: label),
      );
}

class _OutlinedButtonWithIconChild extends StatelessWidget {
422 423 424
  const _OutlinedButtonWithIconChild({
    required this.label,
    required this.icon,
425
  });
426 427 428 429 430 431

  final Widget label;
  final Widget icon;

  @override
  Widget build(BuildContext context) {
432
    final double scale = MediaQuery.maybeOf(context)?.textScaleFactor ?? 1;
433
    final double gap = scale <= 1 ? 8 : lerpDouble(8, 4, math.min(scale - 1, 1))!;
434 435
    return Row(
      mainAxisSize: MainAxisSize.min,
436
      children: <Widget>[icon, SizedBox(width: gap), Flexible(child: label)],
437 438 439
    );
  }
}
440 441 442 443 444 445 446

// 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.

447
// Generated version v0_101
448 449 450 451 452 453 454 455 456 457 458 459 460
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 =>
461
    MaterialStatePropertyAll<TextStyle?>(Theme.of(context).textTheme.labelLarge);
462 463 464 465 466 467 468 469

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

  @override
  MaterialStateProperty<Color?>? get foregroundColor =>
    MaterialStateProperty.resolveWith((Set<MaterialState> states) {
470
      if (states.contains(MaterialState.disabled)) {
471
        return _colors.onSurface.withOpacity(0.38);
472
      }
473 474 475 476 477 478
      return _colors.primary;
    });

  @override
  MaterialStateProperty<Color?>? get overlayColor =>
    MaterialStateProperty.resolveWith((Set<MaterialState> states) {
479
      if (states.contains(MaterialState.hovered)) {
480
        return _colors.primary.withOpacity(0.08);
481 482
      }
      if (states.contains(MaterialState.focused)) {
483
        return _colors.primary.withOpacity(0.12);
484 485
      }
      if (states.contains(MaterialState.pressed)) {
486
        return _colors.primary.withOpacity(0.12);
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 515
      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) {
516
    if (states.contains(MaterialState.disabled)) {
517
      return BorderSide(color: _colors.onSurface.withOpacity(0.12));
518
    }
519 520 521 522 523 524 525 526 527 528
    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) {
529
      if (states.contains(MaterialState.disabled)) {
530
        return SystemMouseCursors.basic;
531
      }
532 533 534 535 536 537 538 539 540 541 542 543 544 545
      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