outline_button.dart 17.4 KB
Newer Older
1 2 3 4 5 6 7 8 9
// Copyright 2018 The Chromium 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 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';

import 'button_theme.dart';
import 'colors.dart';
10
import 'material_button.dart';
11
import 'material_state.dart';
12 13 14 15
import 'raised_button.dart';
import 'theme.dart';

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

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

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

  /// 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.
  ///
111
  /// The [highlightElevation] argument must be null or a positive value. The
112
  /// [icon], [label], [autofocus], and [clipBehavior] arguments must not be null.
113
  factory OutlineButton.icon({
114
    Key key,
115 116 117 118 119
    @required VoidCallback onPressed,
    ButtonTextTheme textTheme,
    Color textColor,
    Color disabledTextColor,
    Color color,
120 121
    Color focusColor,
    Color hoverColor,
122 123 124 125 126 127 128 129 130
    Color highlightColor,
    Color splashColor,
    double highlightElevation,
    Color highlightedBorderColor,
    Color disabledBorderColor,
    BorderSide borderSide,
    EdgeInsetsGeometry padding,
    ShapeBorder shape,
    Clip clipBehavior,
131
    FocusNode focusNode,
132
    bool autofocus,
133 134
    @required Widget icon,
    @required Widget label,
135
  }) = _OutlineButtonWithIcon;
136 137 138

  /// The outline border's color when the button is [enabled] and pressed.
  ///
139 140
  /// By default the border's color does not change when the button
  /// is pressed.
141 142
  ///
  /// This field is ignored if [borderSide.color] is a [MaterialStateProperty<Color>].
143 144 145 146
  final Color highlightedBorderColor;

  /// The outline border's color when the button is not [enabled].
  ///
147 148
  /// By default the outline border's color does not change when the
  /// button is disabled.
149 150
  ///
  /// This field is ignored if [borderSide.color] is a [MaterialStateProperty<Color>].
151 152
  final Color disabledBorderColor;

153 154
  /// Defines the color of the border when the button is enabled but not
  /// pressed, and the border outline's width and style in general.
155
  ///
156 157
  /// If the border side's [BorderSide.style] is [BorderStyle.none], then
  /// an outline is not drawn.
158
  ///
159
  /// If null the default border's style is [BorderStyle.solid], its
160
  /// [BorderSide.width] is 1.0, and its color is a light shade of grey.
161 162 163 164
  ///
  /// If [borderSide.color] is a [MaterialStateProperty<Color>], [MaterialStateProperty.resolve]
  /// is used in all states and both [highlightedBorderColor] and [disabledBorderColor]
  /// are ignored.
165
  final BorderSide borderSide;
166 167

  @override
168 169 170 171 172 173 174 175 176
  Widget build(BuildContext context) {
    final ButtonThemeData buttonTheme = ButtonTheme.of(context);
    return _OutlineButton(
      onPressed: onPressed,
      brightness: buttonTheme.getBrightness(this),
      textTheme: textTheme,
      textColor: buttonTheme.getTextColor(this),
      disabledTextColor: buttonTheme.getDisabledTextColor(this),
      color: color,
177 178
      focusColor: buttonTheme.getFocusColor(this),
      hoverColor: buttonTheme.getHoverColor(this),
179 180 181 182 183 184 185 186 187
      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),
      shape: buttonTheme.getShape(this),
      clipBehavior: clipBehavior,
188
      focusNode: focusNode,
189 190 191
      child: child,
    );
  }
192 193

  @override
194 195
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
196
    properties.add(DiagnosticsProperty<BorderSide>('borderSide', borderSide, defaultValue: null));
197 198
    properties.add(ColorProperty('disabledBorderColor', disabledBorderColor, defaultValue: null));
    properties.add(ColorProperty('highlightedBorderColor', highlightedBorderColor, defaultValue: null));
199 200 201
  }
}

202
// The type of of OutlineButtons created with OutlineButton.icon.
203
//
204 205
// This class only exists to give OutlineButtons created with OutlineButton.icon
// a distinct class for the sake of ButtonTheme. It can not be instantiated.
206
class _OutlineButtonWithIcon extends OutlineButton with MaterialButtonWithIconMixin {
207 208 209 210 211 212 213
  _OutlineButtonWithIcon({
    Key key,
    @required VoidCallback onPressed,
    ButtonTextTheme textTheme,
    Color textColor,
    Color disabledTextColor,
    Color color,
214 215
    Color focusColor,
    Color hoverColor,
216 217 218 219 220 221 222 223
    Color highlightColor,
    Color splashColor,
    double highlightElevation,
    Color highlightedBorderColor,
    Color disabledBorderColor,
    BorderSide borderSide,
    EdgeInsetsGeometry padding,
    ShapeBorder shape,
224
    Clip clipBehavior = Clip.none,
225
    FocusNode focusNode,
226
    bool autofocus = false,
227 228 229
    @required Widget icon,
    @required Widget label,
  }) : assert(highlightElevation == null || highlightElevation >= 0.0),
230
       assert(clipBehavior != null),
231
       assert(autofocus != null),
232 233 234 235 236 237 238 239 240
       assert(icon != null),
       assert(label != null),
       super(
         key: key,
         onPressed: onPressed,
         textTheme: textTheme,
         textColor: textColor,
         disabledTextColor: disabledTextColor,
         color: color,
241 242
         focusColor: focusColor,
         hoverColor: hoverColor,
243 244 245 246 247 248 249 250 251
         highlightColor: highlightColor,
         splashColor: splashColor,
         highlightElevation: highlightElevation,
         disabledBorderColor: disabledBorderColor,
         highlightedBorderColor: highlightedBorderColor,
         borderSide: borderSide,
         padding: padding,
         shape: shape,
         clipBehavior: clipBehavior,
252
         focusNode: focusNode,
253
         autofocus: autofocus,
254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273
         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,
    this.brightness,
    this.textTheme,
    this.textColor,
    this.disabledTextColor,
    this.color,
274 275
    this.focusColor,
    this.hoverColor,
276 277 278 279 280 281 282 283
    this.highlightColor,
    this.splashColor,
    @required this.highlightElevation,
    this.borderSide,
    this.disabledBorderColor,
    @required this.highlightedBorderColor,
    this.padding,
    this.shape,
284
    this.clipBehavior = Clip.none,
285
    this.focusNode,
286
    this.autofocus = false,
287 288 289
    this.child,
  }) : assert(highlightElevation != null && highlightElevation >= 0.0),
       assert(highlightedBorderColor != null),
290
       assert(clipBehavior != null),
291
       assert(autofocus != null),
292 293 294 295 296 297 298 299 300
       super(key: key);

  final VoidCallback onPressed;
  final Brightness brightness;
  final ButtonTextTheme textTheme;
  final Color textColor;
  final Color disabledTextColor;
  final Color color;
  final Color splashColor;
301 302
  final Color focusColor;
  final Color hoverColor;
303 304 305 306 307 308 309 310
  final Color highlightColor;
  final double highlightElevation;
  final BorderSide borderSide;
  final Color disabledBorderColor;
  final Color highlightedBorderColor;
  final EdgeInsetsGeometry padding;
  final ShapeBorder shape;
  final Clip clipBehavior;
311
  final FocusNode focusNode;
312
  final bool autofocus;
313 314 315 316 317 318 319 320 321 322
  final Widget child;

  bool get enabled => onPressed != null;

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


class _OutlineButtonState extends State<_OutlineButton> with SingleTickerProviderStateMixin {
323 324 325 326 327 328 329 330 331
  AnimationController _controller;
  Animation<double> _fillAnimation;
  Animation<double> _elevationAnimation;
  bool _pressed = false;

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

332 333 334 335 336 337 338
    // 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.
339

340
    _controller = AnimationController(
341
      duration: _kPressDuration,
342
      vsync: this,
343
    );
344
    _fillAnimation = CurvedAnimation(
345 346 347 348 349
      parent: _controller,
      curve: const Interval(0.0, 0.5,
        curve: Curves.fastOutSlowIn,
      ),
    );
350
    _elevationAnimation = CurvedAnimation(
351 352 353 354 355 356
      parent: _controller,
      curve: const Interval(0.5, 0.5),
      reverseCurve: const Interval(1.0, 1.0),
    );
  }

357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377
  @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();
    });
  }

378 379 380 381 382 383
  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

384
  Color _getFillColor() {
385 386
    if (widget.highlightElevation == null || widget.highlightElevation == 0.0)
      return Colors.transparent;
387
    final Color color = widget.color ?? Theme.of(context).canvasColor;
388
    final Tween<Color> colorTween = ColorTween(
389 390 391 392 393 394
      begin: color.withAlpha(0x00),
      end: color.withAlpha(0xFF),
    );
    return colorTween.evaluate(_fillAnimation);
  }

395 396 397 398 399 400 401 402 403 404 405 406
  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;
  }

407
  BorderSide _getOutline() {
408 409 410
    if (widget.borderSide?.style == BorderStyle.none)
      return widget.borderSide;

411
    final Color themeColor = Theme.of(context).colorScheme.onSurface.withOpacity(0.12);
412

413
    return BorderSide(
414
      color: _outlineColor ?? themeColor,
415
      width: widget.borderSide?.width ?? 1.0,
416 417 418 419
    );
  }

  double _getHighlightElevation() {
420 421
    if (widget.highlightElevation == null || widget.highlightElevation == 0.0)
      return 0.0;
422
    return Tween<double>(
423
      begin: 0.0,
424
      end: widget.highlightElevation,
425 426 427 428 429
    ).evaluate(_elevationAnimation);
  }

  @override
  Widget build(BuildContext context) {
430
    return AnimatedBuilder(
431 432
      animation: _controller,
      builder: (BuildContext context, Widget child) {
433
        return RaisedButton(
434
          textColor: widget.textColor,
435
          disabledTextColor: widget.disabledTextColor,
436 437
          color: _getFillColor(),
          splashColor: widget.splashColor,
438 439
          focusColor: widget.focusColor,
          hoverColor: widget.hoverColor,
440 441 442 443 444
          highlightColor: widget.highlightColor,
          disabledColor: Colors.transparent,
          onPressed: widget.onPressed,
          elevation: 0.0,
          disabledElevation: 0.0,
445 446
          focusElevation: 0.0,
          hoverElevation: 0.0,
447
          highlightElevation: _getHighlightElevation(),
448
          onHighlightChanged: _handleHighlightChanged,
449
          padding: widget.padding,
450
          shape: _OutlineBorder(
451 452
            shape: widget.shape,
            side: _getOutline(),
453
          ),
454
          clipBehavior: widget.clipBehavior,
455
          focusNode: widget.focusNode,
456 457 458 459 460 461 462 463 464 465
          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.
466
class _OutlineBorder extends ShapeBorder implements MaterialStateProperty<ShapeBorder>{
467 468 469 470 471 472 473 474 475 476 477
  const _OutlineBorder({
    @required this.shape,
    @required this.side,
  }) : assert(shape != null),
       assert(side != null);

  final ShapeBorder shape;
  final BorderSide side;

  @override
  EdgeInsetsGeometry get dimensions {
478
    return EdgeInsets.all(side.width);
479 480 481 482
  }

  @override
  ShapeBorder scale(double t) {
483
    return _OutlineBorder(
484 485 486 487 488 489 490 491 492
      shape: shape.scale(t),
      side: side.scale(t),
    );
  }

  @override
  ShapeBorder lerpFrom(ShapeBorder a, double t) {
    assert(t != null);
    if (a is _OutlineBorder) {
493
      return _OutlineBorder(
494
        side: BorderSide.lerp(a.side, side, t),
495
        shape: ShapeBorder.lerp(a.shape, shape, t),
496 497 498 499 500 501 502 503 504
      );
    }
    return super.lerpFrom(a, t);
  }

  @override
  ShapeBorder lerpTo(ShapeBorder b, double t) {
    assert(t != null);
    if (b is _OutlineBorder) {
505
      return _OutlineBorder(
506
        side: BorderSide.lerp(side, b.side, t),
507
        shape: ShapeBorder.lerp(shape, b.shape, t),
508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528
      );
    }
    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:
529
        canvas.drawPath(shape.getOuterPath(rect, textDirection: textDirection), side.toPaint());
530 531 532 533 534 535 536 537 538 539 540 541 542 543 544
    }
  }

  @override
  bool operator ==(dynamic other) {
    if (identical(this, other))
      return true;
    if (runtimeType != other.runtimeType)
      return false;
    final _OutlineBorder typedOther = other;
    return side == typedOther.side && shape == typedOther.shape;
  }

  @override
  int get hashCode => hashValues(side, shape);
545 546 547 548 549 550 551 552

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