outline_button.dart 16 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 12 13 14
import 'raised_button.dart';
import 'theme.dart';

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

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

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

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

  /// The outline border's color when the button is [enabled] and pressed.
  ///
133 134
  /// By default the border's color does not change when the button
  /// is pressed.
135 136 137 138
  final Color highlightedBorderColor;

  /// The outline border's color when the button is not [enabled].
  ///
139 140
  /// By default the outline border's color does not change when the
  /// button is disabled.
141 142
  final Color disabledBorderColor;

143 144
  /// Defines the color of the border when the button is enabled but not
  /// pressed, and the border outline's width and style in general.
145
  ///
146 147
  /// If the border side's [BorderSide.style] is [BorderStyle.none], then
  /// an outline is not drawn.
148
  ///
149
  /// If null the default border's style is [BorderStyle.solid], its
150
  /// [BorderSide.width] is 1.0, and its color is a light shade of grey.
151
  final BorderSide borderSide;
152 153

  @override
154 155 156 157 158 159 160 161 162
  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,
163 164
      focusColor: buttonTheme.getFocusColor(this),
      hoverColor: buttonTheme.getHoverColor(this),
165 166 167 168 169 170 171 172 173
      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,
174
      focusNode: focusNode,
175 176 177
      child: child,
    );
  }
178 179

  @override
180 181
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
182
    properties.add(DiagnosticsProperty<BorderSide>('borderSide', borderSide, defaultValue: null));
183 184
    properties.add(ColorProperty('disabledBorderColor', disabledBorderColor, defaultValue: null));
    properties.add(ColorProperty('highlightedBorderColor', highlightedBorderColor, defaultValue: null));
185 186 187
  }
}

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

  final VoidCallback onPressed;
  final Brightness brightness;
  final ButtonTextTheme textTheme;
  final Color textColor;
  final Color disabledTextColor;
  final Color color;
  final Color splashColor;
280 281
  final Color focusColor;
  final Color hoverColor;
282 283 284 285 286 287 288 289
  final Color highlightColor;
  final double highlightElevation;
  final BorderSide borderSide;
  final Color disabledBorderColor;
  final Color highlightedBorderColor;
  final EdgeInsetsGeometry padding;
  final ShapeBorder shape;
  final Clip clipBehavior;
290
  final FocusNode focusNode;
291 292 293 294 295 296 297 298 299 300
  final Widget child;

  bool get enabled => onPressed != null;

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


class _OutlineButtonState extends State<_OutlineButton> with SingleTickerProviderStateMixin {
301 302 303 304 305 306 307 308 309
  AnimationController _controller;
  Animation<double> _fillAnimation;
  Animation<double> _elevationAnimation;
  bool _pressed = false;

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

310 311 312 313 314 315 316
    // 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.
317

318
    _controller = AnimationController(
319
      duration: _kPressDuration,
320
      vsync: this,
321
    );
322
    _fillAnimation = CurvedAnimation(
323 324 325 326 327
      parent: _controller,
      curve: const Interval(0.0, 0.5,
        curve: Curves.fastOutSlowIn,
      ),
    );
328
    _elevationAnimation = CurvedAnimation(
329 330 331 332 333 334
      parent: _controller,
      curve: const Interval(0.5, 0.5),
      reverseCurve: const Interval(1.0, 1.0),
    );
  }

335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355
  @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();
    });
  }

356 357 358 359 360 361
  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

362
  Color _getFillColor() {
363 364
    if (widget.highlightElevation == null || widget.highlightElevation == 0.0)
      return Colors.transparent;
365
    final Color color = widget.color ?? Theme.of(context).canvasColor;
366
    final Tween<Color> colorTween = ColorTween(
367 368 369 370 371 372
      begin: color.withAlpha(0x00),
      end: color.withAlpha(0xFF),
    );
    return colorTween.evaluate(_fillAnimation);
  }

373
  BorderSide _getOutline() {
374 375 376
    if (widget.borderSide?.style == BorderStyle.none)
      return widget.borderSide;

377 378 379 380 381
    final Color specifiedColor = widget.enabled
      ? (_pressed ? widget.highlightedBorderColor : null) ?? widget.borderSide?.color
      : widget.disabledBorderColor;

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

383
    return BorderSide(
384 385
      color: specifiedColor ?? themeColor,
      width: widget.borderSide?.width ?? 1.0,
386 387 388 389
    );
  }

  double _getHighlightElevation() {
390 391
    if (widget.highlightElevation == null || widget.highlightElevation == 0.0)
      return 0.0;
392
    return Tween<double>(
393
      begin: 0.0,
394
      end: widget.highlightElevation,
395 396 397 398 399
    ).evaluate(_elevationAnimation);
  }

  @override
  Widget build(BuildContext context) {
400
    return AnimatedBuilder(
401 402
      animation: _controller,
      builder: (BuildContext context, Widget child) {
403
        return RaisedButton(
404
          textColor: widget.textColor,
405
          disabledTextColor: widget.disabledTextColor,
406 407
          color: _getFillColor(),
          splashColor: widget.splashColor,
408 409
          focusColor: widget.focusColor,
          hoverColor: widget.hoverColor,
410 411 412 413 414
          highlightColor: widget.highlightColor,
          disabledColor: Colors.transparent,
          onPressed: widget.onPressed,
          elevation: 0.0,
          disabledElevation: 0.0,
415 416
          focusElevation: 0.0,
          hoverElevation: 0.0,
417
          highlightElevation: _getHighlightElevation(),
418
          onHighlightChanged: _handleHighlightChanged,
419
          padding: widget.padding,
420
          shape: _OutlineBorder(
421 422
            shape: widget.shape,
            side: _getOutline(),
423
          ),
424
          clipBehavior: widget.clipBehavior,
425
          focusNode: widget.focusNode,
426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447
          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.
class _OutlineBorder extends ShapeBorder {
  const _OutlineBorder({
    @required this.shape,
    @required this.side,
  }) : assert(shape != null),
       assert(side != null);

  final ShapeBorder shape;
  final BorderSide side;

  @override
  EdgeInsetsGeometry get dimensions {
448
    return EdgeInsets.all(side.width);
449 450 451 452
  }

  @override
  ShapeBorder scale(double t) {
453
    return _OutlineBorder(
454 455 456 457 458 459 460 461 462
      shape: shape.scale(t),
      side: side.scale(t),
    );
  }

  @override
  ShapeBorder lerpFrom(ShapeBorder a, double t) {
    assert(t != null);
    if (a is _OutlineBorder) {
463
      return _OutlineBorder(
464
        side: BorderSide.lerp(a.side, side, t),
465
        shape: ShapeBorder.lerp(a.shape, shape, t),
466 467 468 469 470 471 472 473 474
      );
    }
    return super.lerpFrom(a, t);
  }

  @override
  ShapeBorder lerpTo(ShapeBorder b, double t) {
    assert(t != null);
    if (b is _OutlineBorder) {
475
      return _OutlineBorder(
476
        side: BorderSide.lerp(side, b.side, t),
477
        shape: ShapeBorder.lerp(shape, b.shape, t),
478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498
      );
    }
    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:
499
        canvas.drawPath(shape.getOuterPath(rect, textDirection: textDirection), side.toPaint());
500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515
    }
  }

  @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);
}