outline_button.dart 19.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
// @dart = 2.8

7
import 'package:flutter/foundation.dart';
8
import 'package:flutter/rendering.dart';
9 10 11 12
import 'package:flutter/widgets.dart';

import 'button_theme.dart';
import 'colors.dart';
13
import 'material_button.dart';
14
import 'material_state.dart';
15 16
import 'raised_button.dart';
import 'theme.dart';
17
import 'theme_data.dart';
18 19

// The total time to make the button's fill color opaque and change
20
// its elevation. Only applies when highlightElevation > 0.0.
21
const Duration _kPressDuration = Duration(milliseconds: 150);
22 23

// Half of _kPressDuration: just the time to change the button's
24
// elevation. Only applies when highlightElevation > 0.0.
25
const Duration _kElevationDuration = Duration(milliseconds: 75);
26

27
/// Similar to a [FlatButton] with a thin grey rounded rectangle border.
28
///
29 30 31 32 33 34 35 36 37 38 39
/// ### This class is obsolete, please use [OutlinedButton] instead.
///
/// FlatButton, RaisedButton, and OutlineButton have been replaced by
/// TextButton, ElevatedButton, and OutlinedButton respectively.
/// ButtonTheme has been replaced by TextButtonTheme,
/// ElevatedButtonTheme, and OutlinedButtonTheme. The original classes
/// will be deprecated soon, please migrate code that uses them.
/// There's a detailed migration guide for the new button and button
/// theme classes in
/// [flutter.dev/go/material-button-migration-guide](https://flutter.dev/go/material-button-migration-guide).
///
40 41 42 43 44
/// The outline button's border shape is defined by [shape]
/// and its appearance is defined by [borderSide], [disabledBorderColor],
/// and [highlightedBorderColor]. By default the border is a one pixel
/// wide grey rounded rectangle that does not change when the button is
/// pressed or disabled. By default the button's background is transparent.
45
///
46
/// If the [onPressed] or [onLongPress] callbacks are null, then the button will be disabled and by
47 48
/// default will resemble a flat button in the [disabledColor].
///
49 50 51 52 53 54 55
/// The button's [highlightElevation], which defines the size of the
/// drop shadow when the button is pressed, is 0.0 (no shadow) by default.
/// If [highlightElevation] is given a value greater than 0.0 then the button
/// becomes a cross between [RaisedButton] and [FlatButton]: a bordered
/// button whose elevation increases and whose background becomes opaque
/// when the button is pressed.
///
56 57 58
/// If you want an ink-splash effect for taps, but don't want to use a button,
/// consider using [InkWell] directly.
///
59
/// Outline buttons have a minimum size of 88.0 by 36.0 which can be overridden
60 61
/// with [ButtonTheme].
///
62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
/// {@tool dartpad --template=stateless_widget_scaffold_center}
///
/// Here is an example of a basic [OutlineButton].
///
/// ```dart
///   Widget build(BuildContext context) {
///     return OutlineButton(
///       onPressed: () {
///         print('Received click');
///       },
///       child: Text('Click Me'),
///     );
///   }
/// ```
/// {@end-tool}
///
78 79 80 81 82 83 84 85
/// See also:
///
///  * [RaisedButton], a filled material design button with a shadow.
///  * [FlatButton], a material design button without a shadow.
///  * [DropdownButton], a button that shows options to select from.
///  * [FloatingActionButton], the round button in material applications.
///  * [IconButton], to create buttons that just contain icons.
///  * [InkWell], which implements the ink splash part of a flat button.
86
///  * <https://material.io/design/components/buttons.html>
87
class OutlineButton extends MaterialButton {
88
  /// Create an outline button.
89
  ///
90
  /// The [highlightElevation] argument must be null or a positive value
91
  /// and the [autofocus] and [clipBehavior] arguments must not be null.
92 93
  const OutlineButton({
    Key key,
94
    @required VoidCallback onPressed,
95
    VoidCallback onLongPress,
96
    MouseCursor mouseCursor,
97 98 99 100
    ButtonTextTheme textTheme,
    Color textColor,
    Color disabledTextColor,
    Color color,
101 102
    Color focusColor,
    Color hoverColor,
103 104 105
    Color highlightColor,
    Color splashColor,
    double highlightElevation,
106 107 108
    this.borderSide,
    this.disabledBorderColor,
    this.highlightedBorderColor,
109
    EdgeInsetsGeometry padding,
110
    VisualDensity visualDensity,
111
    ShapeBorder shape,
112
    Clip clipBehavior = Clip.none,
113
    FocusNode focusNode,
114
    bool autofocus = false,
115
    MaterialTapTargetSize materialTapTargetSize,
116 117
    Widget child,
  }) : assert(highlightElevation == null || highlightElevation >= 0.0),
118
       assert(clipBehavior != null),
119
       assert(autofocus != null),
120 121 122
       super(
         key: key,
         onPressed: onPressed,
123
         onLongPress: onLongPress,
124
         mouseCursor: mouseCursor,
125 126 127 128
         textTheme: textTheme,
         textColor: textColor,
         disabledTextColor: disabledTextColor,
         color: color,
129 130
         focusColor: focusColor,
         hoverColor: hoverColor,
131 132 133 134
         highlightColor: highlightColor,
         splashColor: splashColor,
         highlightElevation: highlightElevation,
         padding: padding,
135
         visualDensity: visualDensity,
136 137
         shape: shape,
         clipBehavior: clipBehavior,
138
         focusNode: focusNode,
139
         materialTapTargetSize: materialTapTargetSize,
140
         autofocus: autofocus,
141 142
         child: child,
       );
143 144 145 146 147 148 149

  /// Create an outline 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.
  ///
150
  /// The [highlightElevation] argument must be null or a positive value. The
151
  /// [icon], [label], [autofocus], and [clipBehavior] arguments must not be null.
152
  factory OutlineButton.icon({
153
    Key key,
154
    @required VoidCallback onPressed,
155
    VoidCallback onLongPress,
156
    MouseCursor mouseCursor,
157 158 159 160
    ButtonTextTheme textTheme,
    Color textColor,
    Color disabledTextColor,
    Color color,
161 162
    Color focusColor,
    Color hoverColor,
163 164 165 166 167 168 169
    Color highlightColor,
    Color splashColor,
    double highlightElevation,
    Color highlightedBorderColor,
    Color disabledBorderColor,
    BorderSide borderSide,
    EdgeInsetsGeometry padding,
170
    VisualDensity visualDensity,
171 172
    ShapeBorder shape,
    Clip clipBehavior,
173
    FocusNode focusNode,
174
    bool autofocus,
175
    MaterialTapTargetSize materialTapTargetSize,
176 177
    @required Widget icon,
    @required Widget label,
178
  }) = _OutlineButtonWithIcon;
179 180 181

  /// The outline border's color when the button is [enabled] and pressed.
  ///
182 183
  /// By default the border's color does not change when the button
  /// is pressed.
184
  ///
185
  /// This field is ignored if [BorderSide.color] is a [MaterialStateProperty<Color>].
186 187 188 189
  final Color highlightedBorderColor;

  /// The outline border's color when the button is not [enabled].
  ///
190 191
  /// By default the outline border's color does not change when the
  /// button is disabled.
192
  ///
193
  /// This field is ignored if [BorderSide.color] is a [MaterialStateProperty<Color>].
194 195
  final Color disabledBorderColor;

196 197
  /// Defines the color of the border when the button is enabled but not
  /// pressed, and the border outline's width and style in general.
198
  ///
199 200
  /// If the border side's [BorderSide.style] is [BorderStyle.none], then
  /// an outline is not drawn.
201
  ///
202
  /// If null the default border's style is [BorderStyle.solid], its
203
  /// [BorderSide.width] is 1.0, and its color is a light shade of grey.
204
  ///
205
  /// If [BorderSide.color] is a [MaterialStateProperty<Color>], [MaterialStateProperty.resolve]
206 207
  /// is used in all states and both [highlightedBorderColor] and [disabledBorderColor]
  /// are ignored.
208
  final BorderSide borderSide;
209 210

  @override
211 212 213
  Widget build(BuildContext context) {
    final ButtonThemeData buttonTheme = ButtonTheme.of(context);
    return _OutlineButton(
214
      autofocus: autofocus,
215
      onPressed: onPressed,
216
      onLongPress: onLongPress,
217
      mouseCursor: mouseCursor,
218 219 220 221 222
      brightness: buttonTheme.getBrightness(this),
      textTheme: textTheme,
      textColor: buttonTheme.getTextColor(this),
      disabledTextColor: buttonTheme.getDisabledTextColor(this),
      color: color,
223 224
      focusColor: buttonTheme.getFocusColor(this),
      hoverColor: buttonTheme.getHoverColor(this),
225 226 227 228 229 230 231
      highlightColor: buttonTheme.getHighlightColor(this),
      splashColor: buttonTheme.getSplashColor(this),
      highlightElevation: buttonTheme.getHighlightElevation(this),
      borderSide: borderSide,
      disabledBorderColor: disabledBorderColor,
      highlightedBorderColor: highlightedBorderColor ?? buttonTheme.colorScheme.primary,
      padding: buttonTheme.getPadding(this),
232
      visualDensity: visualDensity,
233 234
      shape: buttonTheme.getShape(this),
      clipBehavior: clipBehavior,
235
      focusNode: focusNode,
236
      materialTapTargetSize: materialTapTargetSize,
237 238 239
      child: child,
    );
  }
240 241

  @override
242 243
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
244
    properties.add(DiagnosticsProperty<BorderSide>('borderSide', borderSide, defaultValue: null));
245 246
    properties.add(ColorProperty('disabledBorderColor', disabledBorderColor, defaultValue: null));
    properties.add(ColorProperty('highlightedBorderColor', highlightedBorderColor, defaultValue: null));
247 248 249
  }
}

Shi-Hao Hong's avatar
Shi-Hao Hong committed
250
// The type of OutlineButtons created with OutlineButton.icon.
251
//
252 253
// This class only exists to give OutlineButtons created with OutlineButton.icon
// a distinct class for the sake of ButtonTheme. It can not be instantiated.
254
class _OutlineButtonWithIcon extends OutlineButton with MaterialButtonWithIconMixin {
255 256 257
  _OutlineButtonWithIcon({
    Key key,
    @required VoidCallback onPressed,
258
    VoidCallback onLongPress,
259
    MouseCursor mouseCursor,
260 261 262 263
    ButtonTextTheme textTheme,
    Color textColor,
    Color disabledTextColor,
    Color color,
264 265
    Color focusColor,
    Color hoverColor,
266 267 268 269 270 271 272
    Color highlightColor,
    Color splashColor,
    double highlightElevation,
    Color highlightedBorderColor,
    Color disabledBorderColor,
    BorderSide borderSide,
    EdgeInsetsGeometry padding,
273
    VisualDensity visualDensity,
274
    ShapeBorder shape,
275
    Clip clipBehavior = Clip.none,
276
    FocusNode focusNode,
277
    bool autofocus = false,
278
    MaterialTapTargetSize materialTapTargetSize,
279 280 281
    @required Widget icon,
    @required Widget label,
  }) : assert(highlightElevation == null || highlightElevation >= 0.0),
282
       assert(clipBehavior != null),
283
       assert(autofocus != null),
284 285 286 287 288
       assert(icon != null),
       assert(label != null),
       super(
         key: key,
         onPressed: onPressed,
289
         onLongPress: onLongPress,
290
         mouseCursor: mouseCursor,
291 292 293 294
         textTheme: textTheme,
         textColor: textColor,
         disabledTextColor: disabledTextColor,
         color: color,
295 296
         focusColor: focusColor,
         hoverColor: hoverColor,
297 298 299 300 301 302 303
         highlightColor: highlightColor,
         splashColor: splashColor,
         highlightElevation: highlightElevation,
         disabledBorderColor: disabledBorderColor,
         highlightedBorderColor: highlightedBorderColor,
         borderSide: borderSide,
         padding: padding,
304
         visualDensity: visualDensity,
305 306
         shape: shape,
         clipBehavior: clipBehavior,
307
         focusNode: focusNode,
308
         autofocus: autofocus,
309
         materialTapTargetSize: materialTapTargetSize,
310 311 312 313 314 315 316 317 318 319 320 321 322 323 324
         child: Row(
           mainAxisSize: MainAxisSize.min,
           children: <Widget>[
             icon,
             const SizedBox(width: 8.0),
             label,
           ],
         ),
       );
}

class _OutlineButton extends StatefulWidget {
  const _OutlineButton({
    Key key,
    @required this.onPressed,
325
    this.onLongPress,
326
    this.mouseCursor,
327 328 329 330 331
    this.brightness,
    this.textTheme,
    this.textColor,
    this.disabledTextColor,
    this.color,
332 333
    this.focusColor,
    this.hoverColor,
334 335 336 337 338 339 340
    this.highlightColor,
    this.splashColor,
    @required this.highlightElevation,
    this.borderSide,
    this.disabledBorderColor,
    @required this.highlightedBorderColor,
    this.padding,
341
    this.visualDensity,
342
    this.shape,
343
    this.clipBehavior = Clip.none,
344
    this.focusNode,
345
    this.autofocus = false,
346
    this.child,
347
    this.materialTapTargetSize,
348 349
  }) : assert(highlightElevation != null && highlightElevation >= 0.0),
       assert(highlightedBorderColor != null),
350
       assert(clipBehavior != null),
351
       assert(autofocus != null),
352 353 354
       super(key: key);

  final VoidCallback onPressed;
355
  final VoidCallback onLongPress;
356
  final MouseCursor mouseCursor;
357 358 359 360 361 362
  final Brightness brightness;
  final ButtonTextTheme textTheme;
  final Color textColor;
  final Color disabledTextColor;
  final Color color;
  final Color splashColor;
363 364
  final Color focusColor;
  final Color hoverColor;
365 366 367 368 369 370
  final Color highlightColor;
  final double highlightElevation;
  final BorderSide borderSide;
  final Color disabledBorderColor;
  final Color highlightedBorderColor;
  final EdgeInsetsGeometry padding;
371
  final VisualDensity visualDensity;
372 373
  final ShapeBorder shape;
  final Clip clipBehavior;
374
  final FocusNode focusNode;
375
  final bool autofocus;
376
  final Widget child;
377
  final MaterialTapTargetSize materialTapTargetSize;
378

379
  bool get enabled => onPressed != null || onLongPress != null;
380 381 382 383 384 385 386

  @override
  _OutlineButtonState createState() => _OutlineButtonState();
}


class _OutlineButtonState extends State<_OutlineButton> with SingleTickerProviderStateMixin {
387 388 389 390 391 392 393 394 395
  AnimationController _controller;
  Animation<double> _fillAnimation;
  Animation<double> _elevationAnimation;
  bool _pressed = false;

  @override
  void initState() {
    super.initState();

396 397 398 399 400 401 402
    // When highlightElevation > 0.0, the Material widget animates its
    // shape (which includes the outline border) and elevation over
    // _kElevationDuration. When pressed, the button makes its fill
    // color opaque white first, and then sets its
    // highlightElevation. We can't change the elevation while the
    // button's fill is translucent, because the shadow fills the
    // interior of the button.
403

404
    _controller = AnimationController(
405
      duration: _kPressDuration,
406
      vsync: this,
407
    );
408
    _fillAnimation = CurvedAnimation(
409 410 411 412 413
      parent: _controller,
      curve: const Interval(0.0, 0.5,
        curve: Curves.fastOutSlowIn,
      ),
    );
414
    _elevationAnimation = CurvedAnimation(
415 416 417 418 419 420
      parent: _controller,
      curve: const Interval(0.5, 0.5),
      reverseCurve: const Interval(1.0, 1.0),
    );
  }

421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441
  @override
  void didUpdateWidget(_OutlineButton oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (_pressed && !widget.enabled) {
      _pressed = false;
      _controller.reverse();
    }
  }

  void _handleHighlightChanged(bool value) {
    if (_pressed == value)
      return;
    setState(() {
      _pressed = value;
      if (value)
        _controller.forward();
      else
        _controller.reverse();
    });
  }

442 443 444 445 446 447
  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

448
  Color _getFillColor() {
449 450
    if (widget.highlightElevation == null || widget.highlightElevation == 0.0)
      return Colors.transparent;
451
    final Color color = widget.color ?? Theme.of(context).canvasColor;
452
    final Tween<Color> colorTween = ColorTween(
453 454 455 456 457 458
      begin: color.withAlpha(0x00),
      end: color.withAlpha(0xFF),
    );
    return colorTween.evaluate(_fillAnimation);
  }

459 460 461 462 463 464 465 466 467 468 469 470
  Color get _outlineColor {
    // If outline color is a `MaterialStateProperty`, it will be used in all
    // states, otherwise we determine the outline color in the current state.
    if (widget.borderSide?.color is MaterialStateProperty<Color>)
      return widget.borderSide.color;
    if (!widget.enabled)
      return widget.disabledBorderColor;
    if (_pressed)
      return widget.highlightedBorderColor;
    return widget.borderSide?.color;
  }

471
  BorderSide _getOutline() {
472 473 474
    if (widget.borderSide?.style == BorderStyle.none)
      return widget.borderSide;

475
    final Color themeColor = Theme.of(context).colorScheme.onSurface.withOpacity(0.12);
476

477
    return BorderSide(
478
      color: _outlineColor ?? themeColor,
479
      width: widget.borderSide?.width ?? 1.0,
480 481 482 483
    );
  }

  double _getHighlightElevation() {
484 485
    if (widget.highlightElevation == null || widget.highlightElevation == 0.0)
      return 0.0;
486
    return Tween<double>(
487
      begin: 0.0,
488
      end: widget.highlightElevation,
489 490 491 492 493
    ).evaluate(_elevationAnimation);
  }

  @override
  Widget build(BuildContext context) {
494 495
    final ThemeData theme = Theme.of(context);

496
    return AnimatedBuilder(
497 498
      animation: _controller,
      builder: (BuildContext context, Widget child) {
499
        return RaisedButton(
500
          autofocus: widget.autofocus,
501
          textColor: widget.textColor,
502
          disabledTextColor: widget.disabledTextColor,
503 504
          color: _getFillColor(),
          splashColor: widget.splashColor,
505 506
          focusColor: widget.focusColor,
          hoverColor: widget.hoverColor,
507 508 509
          highlightColor: widget.highlightColor,
          disabledColor: Colors.transparent,
          onPressed: widget.onPressed,
510
          onLongPress: widget.onLongPress,
511
          mouseCursor: widget.mouseCursor,
512 513
          elevation: 0.0,
          disabledElevation: 0.0,
514 515
          focusElevation: 0.0,
          hoverElevation: 0.0,
516
          highlightElevation: _getHighlightElevation(),
517
          onHighlightChanged: _handleHighlightChanged,
518
          padding: widget.padding,
519
          visualDensity: widget.visualDensity ?? theme.visualDensity,
520
          shape: _OutlineBorder(
521 522
            shape: widget.shape,
            side: _getOutline(),
523
          ),
524
          clipBehavior: widget.clipBehavior,
525
          focusNode: widget.focusNode,
526
          animationDuration: _kElevationDuration,
527
          materialTapTargetSize: widget.materialTapTargetSize,
528 529 530 531 532 533 534 535 536
          child: widget.child,
        );
      },
    );
  }
}

// Render the button's outline border using using the OutlineButton's
// border parameters and the button or buttonTheme's shape.
537
class _OutlineBorder extends ShapeBorder implements MaterialStateProperty<ShapeBorder>{
538 539 540 541 542 543 544 545 546 547 548
  const _OutlineBorder({
    @required this.shape,
    @required this.side,
  }) : assert(shape != null),
       assert(side != null);

  final ShapeBorder shape;
  final BorderSide side;

  @override
  EdgeInsetsGeometry get dimensions {
549
    return EdgeInsets.all(side.width);
550 551 552 553
  }

  @override
  ShapeBorder scale(double t) {
554
    return _OutlineBorder(
555 556 557 558 559 560 561 562 563
      shape: shape.scale(t),
      side: side.scale(t),
    );
  }

  @override
  ShapeBorder lerpFrom(ShapeBorder a, double t) {
    assert(t != null);
    if (a is _OutlineBorder) {
564
      return _OutlineBorder(
565
        side: BorderSide.lerp(a.side, side, t),
566
        shape: ShapeBorder.lerp(a.shape, shape, t),
567 568 569 570 571 572 573 574 575
      );
    }
    return super.lerpFrom(a, t);
  }

  @override
  ShapeBorder lerpTo(ShapeBorder b, double t) {
    assert(t != null);
    if (b is _OutlineBorder) {
576
      return _OutlineBorder(
577
        side: BorderSide.lerp(side, b.side, t),
578
        shape: ShapeBorder.lerp(shape, b.shape, t),
579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599
      );
    }
    return super.lerpTo(b, t);
  }

  @override
  Path getInnerPath(Rect rect, { TextDirection textDirection }) {
    return shape.getInnerPath(rect.deflate(side.width), textDirection: textDirection);
  }

  @override
  Path getOuterPath(Rect rect, { TextDirection textDirection }) {
    return shape.getOuterPath(rect, textDirection: textDirection);
  }

  @override
  void paint(Canvas canvas, Rect rect, { TextDirection textDirection }) {
    switch (side.style) {
      case BorderStyle.none:
        break;
      case BorderStyle.solid:
600
        canvas.drawPath(shape.getOuterPath(rect, textDirection: textDirection), side.toPaint());
601 602 603 604
    }
  }

  @override
605
  bool operator ==(Object other) {
606 607
    if (identical(this, other))
      return true;
608
    if (other.runtimeType != runtimeType)
609
      return false;
610 611 612
    return other is _OutlineBorder
        && other.side == side
        && other.shape == shape;
613 614 615 616
  }

  @override
  int get hashCode => hashValues(side, shape);
617 618 619 620 621 622 623 624

  @override
  ShapeBorder resolve(Set<MaterialState> states) {
    return _OutlineBorder(
      shape: shape,
      side: side.copyWith(color: MaterialStateProperty.resolveAs<Color>(side.color, states),
    ));
  }
625
}