outline_button.dart 18.5 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
/// 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.
34
///
35
/// If the [onPressed] or [onLongPress] callbacks are null, then the button will be disabled and by
36 37
/// default will resemble a flat button in the [disabledColor].
///
38 39 40 41 42 43 44
/// 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.
///
45 46 47
/// If you want an ink-splash effect for taps, but don't want to use a button,
/// consider using [InkWell] directly.
///
48
/// Outline buttons have a minimum size of 88.0 by 36.0 which can be overridden
49 50 51 52 53 54 55 56 57 58
/// with [ButtonTheme].
///
/// 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.
59
///  * <https://material.io/design/components/buttons.html>
60
class OutlineButton extends MaterialButton {
61
  /// Create an outline button.
62
  ///
63
  /// The [highlightElevation] argument must be null or a positive value
64
  /// and the [autofocus] and [clipBehavior] arguments must not be null.
65 66
  const OutlineButton({
    Key key,
67
    @required VoidCallback onPressed,
68
    VoidCallback onLongPress,
69
    MouseCursor mouseCursor,
70 71 72 73
    ButtonTextTheme textTheme,
    Color textColor,
    Color disabledTextColor,
    Color color,
74 75
    Color focusColor,
    Color hoverColor,
76 77 78
    Color highlightColor,
    Color splashColor,
    double highlightElevation,
79 80 81
    this.borderSide,
    this.disabledBorderColor,
    this.highlightedBorderColor,
82
    EdgeInsetsGeometry padding,
83
    VisualDensity visualDensity,
84
    ShapeBorder shape,
85
    Clip clipBehavior = Clip.none,
86
    FocusNode focusNode,
87
    bool autofocus = false,
88 89
    Widget child,
  }) : assert(highlightElevation == null || highlightElevation >= 0.0),
90
       assert(clipBehavior != null),
91
       assert(autofocus != null),
92 93 94
       super(
         key: key,
         onPressed: onPressed,
95
         onLongPress: onLongPress,
96
         mouseCursor: mouseCursor,
97 98 99 100
         textTheme: textTheme,
         textColor: textColor,
         disabledTextColor: disabledTextColor,
         color: color,
101 102
         focusColor: focusColor,
         hoverColor: hoverColor,
103 104 105 106
         highlightColor: highlightColor,
         splashColor: splashColor,
         highlightElevation: highlightElevation,
         padding: padding,
107
         visualDensity: visualDensity,
108 109
         shape: shape,
         clipBehavior: clipBehavior,
110
         focusNode: focusNode,
111
         autofocus: autofocus,
112 113
         child: child,
       );
114 115 116 117 118 119 120

  /// 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.
  ///
121
  /// The [highlightElevation] argument must be null or a positive value. The
122
  /// [icon], [label], [autofocus], and [clipBehavior] arguments must not be null.
123
  factory OutlineButton.icon({
124
    Key key,
125
    @required VoidCallback onPressed,
126
    VoidCallback onLongPress,
127
    MouseCursor mouseCursor,
128 129 130 131
    ButtonTextTheme textTheme,
    Color textColor,
    Color disabledTextColor,
    Color color,
132 133
    Color focusColor,
    Color hoverColor,
134 135 136 137 138 139 140
    Color highlightColor,
    Color splashColor,
    double highlightElevation,
    Color highlightedBorderColor,
    Color disabledBorderColor,
    BorderSide borderSide,
    EdgeInsetsGeometry padding,
141
    VisualDensity visualDensity,
142 143
    ShapeBorder shape,
    Clip clipBehavior,
144
    FocusNode focusNode,
145
    bool autofocus,
146 147
    @required Widget icon,
    @required Widget label,
148
  }) = _OutlineButtonWithIcon;
149 150 151

  /// The outline border's color when the button is [enabled] and pressed.
  ///
152 153
  /// By default the border's color does not change when the button
  /// is pressed.
154 155
  ///
  /// This field is ignored if [borderSide.color] is a [MaterialStateProperty<Color>].
156 157 158 159
  final Color highlightedBorderColor;

  /// The outline border's color when the button is not [enabled].
  ///
160 161
  /// By default the outline border's color does not change when the
  /// button is disabled.
162 163
  ///
  /// This field is ignored if [borderSide.color] is a [MaterialStateProperty<Color>].
164 165
  final Color disabledBorderColor;

166 167
  /// Defines the color of the border when the button is enabled but not
  /// pressed, and the border outline's width and style in general.
168
  ///
169 170
  /// If the border side's [BorderSide.style] is [BorderStyle.none], then
  /// an outline is not drawn.
171
  ///
172
  /// If null the default border's style is [BorderStyle.solid], its
173
  /// [BorderSide.width] is 1.0, and its color is a light shade of grey.
174 175 176 177
  ///
  /// If [borderSide.color] is a [MaterialStateProperty<Color>], [MaterialStateProperty.resolve]
  /// is used in all states and both [highlightedBorderColor] and [disabledBorderColor]
  /// are ignored.
178
  final BorderSide borderSide;
179 180

  @override
181 182 183
  Widget build(BuildContext context) {
    final ButtonThemeData buttonTheme = ButtonTheme.of(context);
    return _OutlineButton(
184
      autofocus: autofocus,
185
      onPressed: onPressed,
186
      onLongPress: onLongPress,
187
      mouseCursor: mouseCursor,
188 189 190 191 192
      brightness: buttonTheme.getBrightness(this),
      textTheme: textTheme,
      textColor: buttonTheme.getTextColor(this),
      disabledTextColor: buttonTheme.getDisabledTextColor(this),
      color: color,
193 194
      focusColor: buttonTheme.getFocusColor(this),
      hoverColor: buttonTheme.getHoverColor(this),
195 196 197 198 199 200 201
      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),
202
      visualDensity: visualDensity,
203 204
      shape: buttonTheme.getShape(this),
      clipBehavior: clipBehavior,
205
      focusNode: focusNode,
206 207 208
      child: child,
    );
  }
209 210

  @override
211 212
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
213
    properties.add(DiagnosticsProperty<BorderSide>('borderSide', borderSide, defaultValue: null));
214 215
    properties.add(ColorProperty('disabledBorderColor', disabledBorderColor, defaultValue: null));
    properties.add(ColorProperty('highlightedBorderColor', highlightedBorderColor, defaultValue: null));
216 217 218
  }
}

Shi-Hao Hong's avatar
Shi-Hao Hong committed
219
// The type of OutlineButtons created with OutlineButton.icon.
220
//
221 222
// This class only exists to give OutlineButtons created with OutlineButton.icon
// a distinct class for the sake of ButtonTheme. It can not be instantiated.
223
class _OutlineButtonWithIcon extends OutlineButton with MaterialButtonWithIconMixin {
224 225 226
  _OutlineButtonWithIcon({
    Key key,
    @required VoidCallback onPressed,
227
    VoidCallback onLongPress,
228
    MouseCursor mouseCursor,
229 230 231 232
    ButtonTextTheme textTheme,
    Color textColor,
    Color disabledTextColor,
    Color color,
233 234
    Color focusColor,
    Color hoverColor,
235 236 237 238 239 240 241
    Color highlightColor,
    Color splashColor,
    double highlightElevation,
    Color highlightedBorderColor,
    Color disabledBorderColor,
    BorderSide borderSide,
    EdgeInsetsGeometry padding,
242
    VisualDensity visualDensity,
243
    ShapeBorder shape,
244
    Clip clipBehavior = Clip.none,
245
    FocusNode focusNode,
246
    bool autofocus = false,
247 248 249
    @required Widget icon,
    @required Widget label,
  }) : assert(highlightElevation == null || highlightElevation >= 0.0),
250
       assert(clipBehavior != null),
251
       assert(autofocus != null),
252 253 254 255 256
       assert(icon != null),
       assert(label != null),
       super(
         key: key,
         onPressed: onPressed,
257
         onLongPress: onLongPress,
258
         mouseCursor: mouseCursor,
259 260 261 262
         textTheme: textTheme,
         textColor: textColor,
         disabledTextColor: disabledTextColor,
         color: color,
263 264
         focusColor: focusColor,
         hoverColor: hoverColor,
265 266 267 268 269 270 271
         highlightColor: highlightColor,
         splashColor: splashColor,
         highlightElevation: highlightElevation,
         disabledBorderColor: disabledBorderColor,
         highlightedBorderColor: highlightedBorderColor,
         borderSide: borderSide,
         padding: padding,
272
         visualDensity: visualDensity,
273 274
         shape: shape,
         clipBehavior: clipBehavior,
275
         focusNode: focusNode,
276
         autofocus: autofocus,
277 278 279 280 281 282 283 284 285 286 287 288 289 290 291
         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,
292
    this.onLongPress,
293
    this.mouseCursor,
294 295 296 297 298
    this.brightness,
    this.textTheme,
    this.textColor,
    this.disabledTextColor,
    this.color,
299 300
    this.focusColor,
    this.hoverColor,
301 302 303 304 305 306 307
    this.highlightColor,
    this.splashColor,
    @required this.highlightElevation,
    this.borderSide,
    this.disabledBorderColor,
    @required this.highlightedBorderColor,
    this.padding,
308
    this.visualDensity,
309
    this.shape,
310
    this.clipBehavior = Clip.none,
311
    this.focusNode,
312
    this.autofocus = false,
313 314 315
    this.child,
  }) : assert(highlightElevation != null && highlightElevation >= 0.0),
       assert(highlightedBorderColor != null),
316
       assert(clipBehavior != null),
317
       assert(autofocus != null),
318 319 320
       super(key: key);

  final VoidCallback onPressed;
321
  final VoidCallback onLongPress;
322
  final MouseCursor mouseCursor;
323 324 325 326 327 328
  final Brightness brightness;
  final ButtonTextTheme textTheme;
  final Color textColor;
  final Color disabledTextColor;
  final Color color;
  final Color splashColor;
329 330
  final Color focusColor;
  final Color hoverColor;
331 332 333 334 335 336
  final Color highlightColor;
  final double highlightElevation;
  final BorderSide borderSide;
  final Color disabledBorderColor;
  final Color highlightedBorderColor;
  final EdgeInsetsGeometry padding;
337
  final VisualDensity visualDensity;
338 339
  final ShapeBorder shape;
  final Clip clipBehavior;
340
  final FocusNode focusNode;
341
  final bool autofocus;
342 343
  final Widget child;

344
  bool get enabled => onPressed != null || onLongPress != null;
345 346 347 348 349 350 351

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


class _OutlineButtonState extends State<_OutlineButton> with SingleTickerProviderStateMixin {
352 353 354 355 356 357 358 359 360
  AnimationController _controller;
  Animation<double> _fillAnimation;
  Animation<double> _elevationAnimation;
  bool _pressed = false;

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

361 362 363 364 365 366 367
    // 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.
368

369
    _controller = AnimationController(
370
      duration: _kPressDuration,
371
      vsync: this,
372
    );
373
    _fillAnimation = CurvedAnimation(
374 375 376 377 378
      parent: _controller,
      curve: const Interval(0.0, 0.5,
        curve: Curves.fastOutSlowIn,
      ),
    );
379
    _elevationAnimation = CurvedAnimation(
380 381 382 383 384 385
      parent: _controller,
      curve: const Interval(0.5, 0.5),
      reverseCurve: const Interval(1.0, 1.0),
    );
  }

386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406
  @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();
    });
  }

407 408 409 410 411 412
  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

413
  Color _getFillColor() {
414 415
    if (widget.highlightElevation == null || widget.highlightElevation == 0.0)
      return Colors.transparent;
416
    final Color color = widget.color ?? Theme.of(context).canvasColor;
417
    final Tween<Color> colorTween = ColorTween(
418 419 420 421 422 423
      begin: color.withAlpha(0x00),
      end: color.withAlpha(0xFF),
    );
    return colorTween.evaluate(_fillAnimation);
  }

424 425 426 427 428 429 430 431 432 433 434 435
  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;
  }

436
  BorderSide _getOutline() {
437 438 439
    if (widget.borderSide?.style == BorderStyle.none)
      return widget.borderSide;

440
    final Color themeColor = Theme.of(context).colorScheme.onSurface.withOpacity(0.12);
441

442
    return BorderSide(
443
      color: _outlineColor ?? themeColor,
444
      width: widget.borderSide?.width ?? 1.0,
445 446 447 448
    );
  }

  double _getHighlightElevation() {
449 450
    if (widget.highlightElevation == null || widget.highlightElevation == 0.0)
      return 0.0;
451
    return Tween<double>(
452
      begin: 0.0,
453
      end: widget.highlightElevation,
454 455 456 457 458
    ).evaluate(_elevationAnimation);
  }

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

461
    return AnimatedBuilder(
462 463
      animation: _controller,
      builder: (BuildContext context, Widget child) {
464
        return RaisedButton(
465
          autofocus: widget.autofocus,
466
          textColor: widget.textColor,
467
          disabledTextColor: widget.disabledTextColor,
468 469
          color: _getFillColor(),
          splashColor: widget.splashColor,
470 471
          focusColor: widget.focusColor,
          hoverColor: widget.hoverColor,
472 473 474
          highlightColor: widget.highlightColor,
          disabledColor: Colors.transparent,
          onPressed: widget.onPressed,
475
          onLongPress: widget.onLongPress,
476
          mouseCursor: widget.mouseCursor,
477 478
          elevation: 0.0,
          disabledElevation: 0.0,
479 480
          focusElevation: 0.0,
          hoverElevation: 0.0,
481
          highlightElevation: _getHighlightElevation(),
482
          onHighlightChanged: _handleHighlightChanged,
483
          padding: widget.padding,
484
          visualDensity: widget.visualDensity ?? theme.visualDensity,
485
          shape: _OutlineBorder(
486 487
            shape: widget.shape,
            side: _getOutline(),
488
          ),
489
          clipBehavior: widget.clipBehavior,
490
          focusNode: widget.focusNode,
491 492 493 494 495 496 497 498 499 500
          animationDuration: _kElevationDuration,
          child: widget.child,
        );
      },
    );
  }
}

// Render the button's outline border using using the OutlineButton's
// border parameters and the button or buttonTheme's shape.
501
class _OutlineBorder extends ShapeBorder implements MaterialStateProperty<ShapeBorder>{
502 503 504 505 506 507 508 509 510 511 512
  const _OutlineBorder({
    @required this.shape,
    @required this.side,
  }) : assert(shape != null),
       assert(side != null);

  final ShapeBorder shape;
  final BorderSide side;

  @override
  EdgeInsetsGeometry get dimensions {
513
    return EdgeInsets.all(side.width);
514 515 516 517
  }

  @override
  ShapeBorder scale(double t) {
518
    return _OutlineBorder(
519 520 521 522 523 524 525 526 527
      shape: shape.scale(t),
      side: side.scale(t),
    );
  }

  @override
  ShapeBorder lerpFrom(ShapeBorder a, double t) {
    assert(t != null);
    if (a is _OutlineBorder) {
528
      return _OutlineBorder(
529
        side: BorderSide.lerp(a.side, side, t),
530
        shape: ShapeBorder.lerp(a.shape, shape, t),
531 532 533 534 535 536 537 538 539
      );
    }
    return super.lerpFrom(a, t);
  }

  @override
  ShapeBorder lerpTo(ShapeBorder b, double t) {
    assert(t != null);
    if (b is _OutlineBorder) {
540
      return _OutlineBorder(
541
        side: BorderSide.lerp(side, b.side, t),
542
        shape: ShapeBorder.lerp(shape, b.shape, t),
543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563
      );
    }
    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:
564
        canvas.drawPath(shape.getOuterPath(rect, textDirection: textDirection), side.toPaint());
565 566 567 568
    }
  }

  @override
569
  bool operator ==(Object other) {
570 571
    if (identical(this, other))
      return true;
572
    if (other.runtimeType != runtimeType)
573
      return false;
574 575 576
    return other is _OutlineBorder
        && other.side == side
        && other.shape == shape;
577 578 579 580
  }

  @override
  int get hashCode => hashValues(side, shape);
581 582 583 584 585 586 587 588

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