outline_button.dart 18.7 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5
// 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';
6
import 'package:flutter/rendering.dart';
7 8 9 10
import 'package:flutter/widgets.dart';

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

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

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

25
/// Similar to a [FlatButton] with a thin grey rounded rectangle border.
26
///
27
/// ### This class is deprecated, please use [OutlinedButton] instead.
28 29
///
/// FlatButton, RaisedButton, and OutlineButton have been replaced by
30
/// [TextButton], [ElevatedButton], and [OutlinedButton] respectively.
31 32
/// ButtonTheme has been replaced by TextButtonTheme,
/// ElevatedButtonTheme, and OutlinedButtonTheme. The original classes
33
/// will eventually be removed, please migrate code that uses them.
34 35 36
/// 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).
37 38
@Deprecated(
  'Use OutlinedButton instead. See the migration guide in flutter.dev/go/material-button-migration-guide). '
39
  'This feature was deprecated after v1.26.0-18.0.pre.',
40
)
41
class OutlineButton extends MaterialButton {
42
  /// Create an outline button.
43
  ///
44
  /// The [highlightElevation] argument must be null or a positive value
45
  /// and the [autofocus] and [clipBehavior] arguments must not be null.
46 47
  @Deprecated(
    'Use OutlinedButton instead. See the migration guide in flutter.dev/go/material-button-migration-guide). '
48
    'This feature was deprecated after v1.26.0-18.0.pre.',
49
  )
50
  const OutlineButton({
51 52 53 54 55 56 57 58 59 60 61 62 63
    Key? key,
    required VoidCallback? onPressed,
    VoidCallback? onLongPress,
    MouseCursor? mouseCursor,
    ButtonTextTheme? textTheme,
    Color? textColor,
    Color? disabledTextColor,
    Color? color,
    Color? focusColor,
    Color? hoverColor,
    Color? highlightColor,
    Color? splashColor,
    double? highlightElevation,
64 65 66
    this.borderSide,
    this.disabledBorderColor,
    this.highlightedBorderColor,
67 68 69
    EdgeInsetsGeometry? padding,
    VisualDensity? visualDensity,
    ShapeBorder? shape,
70
    Clip clipBehavior = Clip.none,
71
    FocusNode? focusNode,
72
    bool autofocus = false,
73 74
    MaterialTapTargetSize? materialTapTargetSize,
    Widget? child,
75
  }) : assert(highlightElevation == null || highlightElevation >= 0.0),
76
       assert(clipBehavior != null),
77
       assert(autofocus != null),
78 79 80
       super(
         key: key,
         onPressed: onPressed,
81
         onLongPress: onLongPress,
82
         mouseCursor: mouseCursor,
83 84 85 86
         textTheme: textTheme,
         textColor: textColor,
         disabledTextColor: disabledTextColor,
         color: color,
87 88
         focusColor: focusColor,
         hoverColor: hoverColor,
89 90 91 92
         highlightColor: highlightColor,
         splashColor: splashColor,
         highlightElevation: highlightElevation,
         padding: padding,
93
         visualDensity: visualDensity,
94 95
         shape: shape,
         clipBehavior: clipBehavior,
96
         focusNode: focusNode,
97
         materialTapTargetSize: materialTapTargetSize,
98
         autofocus: autofocus,
99 100
         child: child,
       );
101 102 103 104 105 106 107

  /// 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.
  ///
108
  /// The [highlightElevation] argument must be null or a positive value. The
109
  /// [icon], [label], [autofocus], and [clipBehavior] arguments must not be null.
110 111
  @Deprecated(
    'Use OutlinedButton instead. See the migration guide in flutter.dev/go/material-button-migration-guide). '
112
    'This feature was deprecated after v1.26.0-18.0.pre.',
113
  )
114
  factory OutlineButton.icon({
115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133
    Key? key,
    required VoidCallback? onPressed,
    VoidCallback? onLongPress,
    MouseCursor? mouseCursor,
    ButtonTextTheme? textTheme,
    Color? textColor,
    Color? disabledTextColor,
    Color? color,
    Color? focusColor,
    Color? hoverColor,
    Color? highlightColor,
    Color? splashColor,
    double? highlightElevation,
    Color? highlightedBorderColor,
    Color? disabledBorderColor,
    BorderSide? borderSide,
    EdgeInsetsGeometry? padding,
    VisualDensity? visualDensity,
    ShapeBorder? shape,
134
    Clip clipBehavior,
135
    FocusNode? focusNode,
136
    bool autofocus,
137 138 139
    MaterialTapTargetSize? materialTapTargetSize,
    required Widget icon,
    required Widget label,
140
  }) = _OutlineButtonWithIcon;
141 142 143

  /// The outline border's color when the button is [enabled] and pressed.
  ///
144 145
  /// By default the border's color does not change when the button
  /// is pressed.
146
  ///
147
  /// This field is ignored if [BorderSide.color] is a [MaterialStateProperty<Color>].
148
  final Color? highlightedBorderColor;
149 150 151

  /// The outline border's color when the button is not [enabled].
  ///
152 153
  /// By default the outline border's color does not change when the
  /// button is disabled.
154
  ///
155
  /// This field is ignored if [BorderSide.color] is a [MaterialStateProperty<Color>].
156
  final Color? disabledBorderColor;
157

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

  @override
173 174 175
  Widget build(BuildContext context) {
    final ButtonThemeData buttonTheme = ButtonTheme.of(context);
    return _OutlineButton(
176
      autofocus: autofocus,
177
      onPressed: onPressed,
178
      onLongPress: onLongPress,
179
      mouseCursor: mouseCursor,
180 181 182 183 184
      brightness: buttonTheme.getBrightness(this),
      textTheme: textTheme,
      textColor: buttonTheme.getTextColor(this),
      disabledTextColor: buttonTheme.getDisabledTextColor(this),
      color: color,
185 186
      focusColor: buttonTheme.getFocusColor(this),
      hoverColor: buttonTheme.getHoverColor(this),
187 188 189 190 191
      highlightColor: buttonTheme.getHighlightColor(this),
      splashColor: buttonTheme.getSplashColor(this),
      highlightElevation: buttonTheme.getHighlightElevation(this),
      borderSide: borderSide,
      disabledBorderColor: disabledBorderColor,
192
      highlightedBorderColor: highlightedBorderColor ?? buttonTheme.colorScheme!.primary,
193
      padding: buttonTheme.getPadding(this),
194
      visualDensity: visualDensity,
195 196
      shape: buttonTheme.getShape(this),
      clipBehavior: clipBehavior,
197
      focusNode: focusNode,
198
      materialTapTargetSize: materialTapTargetSize,
199 200 201
      child: child,
    );
  }
202 203

  @override
204 205
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
206
    properties.add(DiagnosticsProperty<BorderSide>('borderSide', borderSide, defaultValue: null));
207 208
    properties.add(ColorProperty('disabledBorderColor', disabledBorderColor, defaultValue: null));
    properties.add(ColorProperty('highlightedBorderColor', highlightedBorderColor, defaultValue: null));
209 210 211
  }
}

Shi-Hao Hong's avatar
Shi-Hao Hong committed
212
// The type of OutlineButtons created with OutlineButton.icon.
213
//
214 215
// This class only exists to give OutlineButtons created with OutlineButton.icon
// a distinct class for the sake of ButtonTheme. It can not be instantiated.
216
class _OutlineButtonWithIcon extends OutlineButton with MaterialButtonWithIconMixin {
217
  _OutlineButtonWithIcon({
218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236
    Key? key,
    required VoidCallback? onPressed,
    VoidCallback? onLongPress,
    MouseCursor? mouseCursor,
    ButtonTextTheme? textTheme,
    Color? textColor,
    Color? disabledTextColor,
    Color? color,
    Color? focusColor,
    Color? hoverColor,
    Color? highlightColor,
    Color? splashColor,
    double? highlightElevation,
    Color? highlightedBorderColor,
    Color? disabledBorderColor,
    BorderSide? borderSide,
    EdgeInsetsGeometry? padding,
    VisualDensity? visualDensity,
    ShapeBorder? shape,
237
    Clip clipBehavior = Clip.none,
238
    FocusNode? focusNode,
239
    bool autofocus = false,
240 241 242
    MaterialTapTargetSize? materialTapTargetSize,
    required Widget icon,
    required Widget label,
243
  }) : assert(highlightElevation == null || highlightElevation >= 0.0),
244
       assert(clipBehavior != null),
245
       assert(autofocus != null),
246 247 248 249 250
       assert(icon != null),
       assert(label != null),
       super(
         key: key,
         onPressed: onPressed,
251
         onLongPress: onLongPress,
252
         mouseCursor: mouseCursor,
253 254 255 256
         textTheme: textTheme,
         textColor: textColor,
         disabledTextColor: disabledTextColor,
         color: color,
257 258
         focusColor: focusColor,
         hoverColor: hoverColor,
259 260 261 262 263 264 265
         highlightColor: highlightColor,
         splashColor: splashColor,
         highlightElevation: highlightElevation,
         disabledBorderColor: disabledBorderColor,
         highlightedBorderColor: highlightedBorderColor,
         borderSide: borderSide,
         padding: padding,
266
         visualDensity: visualDensity,
267 268
         shape: shape,
         clipBehavior: clipBehavior,
269
         focusNode: focusNode,
270
         autofocus: autofocus,
271
         materialTapTargetSize: materialTapTargetSize,
272 273 274 275 276
         child: Row(
           mainAxisSize: MainAxisSize.min,
           children: <Widget>[
             icon,
             const SizedBox(width: 8.0),
277
             label,
278 279 280 281 282 283 284
           ],
         ),
       );
}

class _OutlineButton extends StatefulWidget {
  const _OutlineButton({
285 286
    Key? key,
    required this.onPressed,
287
    this.onLongPress,
288
    this.mouseCursor,
289
    required this.brightness,
290
    this.textTheme,
291 292
    required this.textColor,
    required this.disabledTextColor,
293
    this.color,
294 295 296 297 298
    required this.focusColor,
    required this.hoverColor,
    required this.highlightColor,
    required this.splashColor,
    required this.highlightElevation,
299 300
    this.borderSide,
    this.disabledBorderColor,
301 302
    required this.highlightedBorderColor,
    required this.padding,
303
    this.visualDensity,
304
    required this.shape,
305
    this.clipBehavior = Clip.none,
306
    this.focusNode,
307
    this.autofocus = false,
308
    this.child,
309
    this.materialTapTargetSize,
310 311
  }) : assert(highlightElevation != null && highlightElevation >= 0.0),
       assert(highlightedBorderColor != null),
312
       assert(clipBehavior != null),
313
       assert(autofocus != null),
314 315
       super(key: key);

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

341
  bool get enabled => onPressed != null || onLongPress != null;
342 343 344 345 346 347 348

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


class _OutlineButtonState extends State<_OutlineButton> with SingleTickerProviderStateMixin {
349 350 351
  late AnimationController _controller;
  late Animation<double> _fillAnimation;
  late Animation<double> _elevationAnimation;
352 353 354 355 356 357
  bool _pressed = false;

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

358 359 360 361 362 363 364
    // 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.
365

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

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

404 405 406 407 408 409
  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

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

421
  Color? get _outlineColor {
422 423
    // If outline color is a `MaterialStateProperty`, it will be used in all
    // states, otherwise we determine the outline color in the current state.
424 425
    if (widget.borderSide?.color is MaterialStateProperty<Color?>)
      return widget.borderSide!.color;
426 427 428 429 430 431 432
    if (!widget.enabled)
      return widget.disabledBorderColor;
    if (_pressed)
      return widget.highlightedBorderColor;
    return widget.borderSide?.color;
  }

433
  BorderSide _getOutline() {
434
    if (widget.borderSide?.style == BorderStyle.none)
435
      return widget.borderSide!;
436

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

439
    return BorderSide(
440
      color: _outlineColor ?? themeColor,
441
      width: widget.borderSide?.width ?? 1.0,
442 443 444 445
    );
  }

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

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

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

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

  final ShapeBorder shape;
  final BorderSide side;

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

  @override
  ShapeBorder scale(double t) {
516
    return _OutlineBorder(
517 518 519 520 521 522
      shape: shape.scale(t),
      side: side.scale(t),
    );
  }

  @override
523
  ShapeBorder? lerpFrom(ShapeBorder? a, double t) {
524 525
    assert(t != null);
    if (a is _OutlineBorder) {
526
      return _OutlineBorder(
527
        side: BorderSide.lerp(a.side, side, t),
528
        shape: ShapeBorder.lerp(a.shape, shape, t)!,
529 530 531 532 533 534
      );
    }
    return super.lerpFrom(a, t);
  }

  @override
535
  ShapeBorder? lerpTo(ShapeBorder? b, double t) {
536 537
    assert(t != null);
    if (b is _OutlineBorder) {
538
      return _OutlineBorder(
539
        side: BorderSide.lerp(side, b.side, t),
540
        shape: ShapeBorder.lerp(shape, b.shape, t)!,
541 542 543 544 545 546
      );
    }
    return super.lerpTo(b, t);
  }

  @override
547
  Path getInnerPath(Rect rect, { TextDirection? textDirection }) {
548 549 550 551
    return shape.getInnerPath(rect.deflate(side.width), textDirection: textDirection);
  }

  @override
552
  Path getOuterPath(Rect rect, { TextDirection? textDirection }) {
553 554 555 556
    return shape.getOuterPath(rect, textDirection: textDirection);
  }

  @override
557
  void paint(Canvas canvas, Rect rect, { TextDirection? textDirection }) {
558 559 560 561
    switch (side.style) {
      case BorderStyle.none:
        break;
      case BorderStyle.solid:
562
        canvas.drawPath(shape.getOuterPath(rect, textDirection: textDirection), side.toPaint());
563 564 565 566
    }
  }

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

  @override
  int get hashCode => hashValues(side, shape);
579 580 581 582 583

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