material.dart 22.9 KB
Newer Older
1 2 3 4
// Copyright 2015 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.

5
import 'package:flutter/foundation.dart';
6
import 'package:flutter/rendering.dart';
7
import 'package:flutter/widgets.dart';
8

9
import 'constants.dart';
10
import 'theme.dart';
11

12 13
/// Signature for the callback used by ink effects to obtain the rectangle for the effect.
///
14
/// Used by [InkHighlight] and [InkSplash], for example.
15 16 17 18
typedef Rect RectCallback();

/// The various kinds of material in material design. Used to
/// configure the default behavior of [Material] widgets.
19 20
///
/// See also:
21 22
///
///  * [Material], in particular [Material.type]
23
///  * [kMaterialEdges]
24
enum MaterialType {
25
  /// Rectangle using default theme canvas color.
26 27 28 29
  canvas,

  /// Rounded edges, card theme color.
  card,
30

31 32 33
  /// A circle, no color by default (used for floating action buttons).
  circle,

34
  /// Rounded edges, no color by default (used for [MaterialButton] buttons).
35 36 37
  button,

  /// A transparent piece of material that draws ink splashes and highlights.
38 39 40 41 42 43 44 45 46
  ///
  /// While the material metaphor describes child widgets as printed on the
  /// material itself and do not hide ink effects, in practice the [Material]
  /// widget draws child widgets on top of the ink effects.
  /// A [Material] with type transparency can be placed on top of opaque widgets
  /// to show ink effects on top of them.
  ///
  /// Prefer using the [Ink] widget for showing ink effects on top of opaque
  /// widgets.
47
  transparency
48 49
}

50 51 52 53 54 55
/// The border radii used by the various kinds of material in material design.
///
/// See also:
///
///  * [MaterialType]
///  * [Material]
56
final Map<MaterialType, BorderRadius> kMaterialEdges = <MaterialType, BorderRadius> {
57
  MaterialType.canvas: null,
58
  MaterialType.card: new BorderRadius.circular(2.0),
59
  MaterialType.circle: null,
60
  MaterialType.button: new BorderRadius.circular(2.0),
61
  MaterialType.transparency: null,
62 63
};

64 65 66
/// An interface for creating [InkSplash]s and [InkHighlight]s on a material.
///
/// Typically obtained via [Material.of].
67
abstract class MaterialInkController {
68
  /// The color of the material.
69 70
  Color get color;

71
  /// The ticker provider used by the controller.
72
  ///
73 74 75 76 77
  /// Ink features that are added to this controller with [addInkFeature] should
  /// use this vsync to drive their animations.
  TickerProvider get vsync;

  /// Add an [InkFeature], such as an [InkSplash] or an [InkHighlight].
78
  ///
79
  /// The ink feature will paint as part of this controller.
80
  void addInkFeature(InkFeature feature);
81 82 83

  /// Notifies the controller that one of its ink features needs to repaint.
  void markNeedsPaint();
84 85
}

86 87
/// A piece of material.
///
88 89 90 91 92 93 94 95 96 97 98
/// The Material widget is responsible for:
///
/// 1. Clipping: Material clips its widget sub-tree to the shape specified by
///    [type] and [borderRadius].
/// 2. Elevation: Material elevates its widget sub-tree on the Z axis by
///    [elevation] pixels, and draws the appropriate shadow.
/// 3. Ink effects: Material shows ink effects implemented by [InkFeature]s
///    like [InkSplash] and [InkHighlight] below its children.
///
/// ## The Material Metaphor
///
99 100 101
/// Material is the central metaphor in material design. Each piece of material
/// exists at a given elevation, which influences how that piece of material
/// visually relates to other pieces of material and how that material casts
102
/// shadows.
103 104 105 106 107 108
///
/// Most user interface elements are either conceptually printed on a piece of
/// material or themselves made of material. Material reacts to user input using
/// [InkSplash] and [InkHighlight] effects. To trigger a reaction on the
/// material, use a [MaterialInkController] obtained via [Material.of].
///
109
/// In general, the features of a [Material] should not change over time (e.g. a
110 111 112 113 114
/// [Material] should not change its [color], [shadowColor] or [type]).
/// Changes to [elevation] and [shadowColor] are animated. Changes to [shape] are
/// animated if [type] is not [MaterialType.transparency] and [ShapeBorder.lerp]
/// between the previous and next [shape] values is supported.
///
115 116 117 118 119
///
/// ## Shape
///
/// The shape for material is determined by [type] and [borderRadius].
///
120 121 122 123 124
///  - If [shape] is non null, it determines the shape.
///  - If [shape] is null and [borderRadius] is non null, the shape is a
///    rounded rectangle, with corners specified by [borderRadius].
///  - If [shape] and [borderRadius] are null, [type] determines the
///    shape as follows:
125 126 127 128 129 130 131 132
///    - [MaterialType.canvas]: the default material shape is a rectangle.
///    - [MaterialType.card]: the default material shape is a rectangle with
///      rounded edges. The edge radii is specified by [kMaterialEdges].
///    - [MaterialType.circle]: the default material shape is a circle.
///    - [MaterialType.button]: the default material shape is a rectangle with
///      rounded edges. The edge radii is specified by [kMaterialEdges].
///    - [MaterialType.transparency]: the default material shape is a rectangle.
///
133 134 135 136
/// ## Border
///
/// If [shape] is not null, then its border will also be painted (if any).
///
137
/// ## Layout change notifications
138
///
Ian Hickson's avatar
Ian Hickson committed
139 140 141 142 143 144 145 146
/// If the layout changes (e.g. because there's a list on the material, and it's
/// been scrolled), a [LayoutChangedNotification] must be dispatched at the
/// relevant subtree. This in particular means that transitions (e.g.
/// [SlideTransition]) should not be placed inside [Material] widgets so as to
/// move subtrees that contain [InkResponse]s, [InkWell]s, [Ink]s, or other
/// widgets that use the [InkFeature] mechanism. Otherwise, in-progress ink
/// features (e.g., ink splashes and ink highlights) won't move to account for
/// the new layout.
147 148 149
///
/// See also:
///
150 151
/// * [MergeableMaterial], a piece of material that can split and remerge.
/// * [Card], a wrapper for a [Material] of [type] [MaterialType.card].
152
/// * <https://material.google.com/>
153
class Material extends StatefulWidget {
154 155
  /// Creates a piece of material.
  ///
156
  /// The [type], [elevation] and [shadowColor] arguments must not be null.
157 158 159 160 161 162
  ///
  /// If a [shape] is specified, then the [borderRadius] property must not be
  /// null and the [type] property must not be [MaterialType.circle]. If the
  /// [borderRadius] is specified, then the [type] property must not be
  /// [MaterialType.circle]. In both cases, these restrictions are intended to
  /// catch likely errors.
163
  const Material({
164
    Key key,
165
    this.type: MaterialType.canvas,
166
    this.elevation: 0.0,
167
    this.color,
168
    this.shadowColor: const Color(0xFF000000),
169
    this.textStyle,
170
    this.borderRadius,
171
    this.shape,
172
    this.child,
173 174
  }) : assert(type != null),
       assert(elevation != null),
175
       assert(shadowColor != null),
176 177
       assert(!(shape != null && borderRadius != null)),
       assert(!(identical(type, MaterialType.circle) && (borderRadius != null || shape != null))),
178
       super(key: key);
179

180
  /// The widget below this widget in the tree.
181 182
  ///
  /// {@macro flutter.widgets.child}
183
  final Widget child;
184

185 186 187
  /// The kind of material to show (e.g., card or canvas). This
  /// affects the shape of the widget, the roundness of its corners if
  /// the shape is rectangular, and the default color.
188
  final MaterialType type;
189

190 191
  /// The z-coordinate at which to place this material. This controls the size
  /// of the shadow below the material.
192
  ///
193
  /// If this is non-zero, the contents of the material are clipped, because the
194 195 196 197
  /// widget conceptually defines an independent printed piece of material.
  ///
  /// Defaults to 0. Changing this value will cause the shadow to animate over
  /// [kThemeChangeDuration].
198
  final double elevation;
199

200
  /// The color to paint the material.
201 202 203
  ///
  /// Must be opaque. To create a transparent piece of material, use
  /// [MaterialType.transparency].
204 205
  ///
  /// By default, the color is derived from the [type] of material.
206
  final Color color;
207

208 209 210 211 212
  /// The color to paint the shadow below the material.
  ///
  /// Defaults to fully opaque black.
  final Color shadowColor;

213
  /// The typographical style to use for text within this material.
214
  final TextStyle textStyle;
215

216 217 218 219 220 221 222
  /// Defines the material's shape as well its shadow.
  ///
  /// If shape is non null, the [borderRadius] is ignored and the material's
  /// clip boundary and shadow are defined by the shape.
  ///
  /// A shadow is only displayed if the [elevation] is greater than
  /// zero.
223 224
  final ShapeBorder shape;

225 226 227 228
  /// If non-null, the corners of this box are rounded by this [BorderRadius].
  /// Otherwise, the corners specified for the current [type] of material are
  /// used.
  ///
229 230
  /// If [shape] is non null then the border radius is ignored.
  ///
231 232 233
  /// Must be null if [type] is [MaterialType.circle].
  final BorderRadius borderRadius;

234 235
  /// The ink controller from the closest instance of this class that
  /// encloses the given context.
236 237 238 239 240 241
  ///
  /// Typical usage is as follows:
  ///
  /// ```dart
  /// MaterialInkController inkController = Material.of(context);
  /// ```
242
  static MaterialInkController of(BuildContext context) {
243
    final _RenderInkFeatures result = context.ancestorRenderObjectOfType(const TypeMatcher<_RenderInkFeatures>());
244 245 246
    return result;
  }

247
  @override
248
  _MaterialState createState() => new _MaterialState();
249

250
  @override
251
  void debugFillProperties(DiagnosticPropertiesBuilder description) {
252 253
    super.debugFillProperties(description);
    description.add(new EnumProperty<MaterialType>('type', type));
254
    description.add(new DoubleProperty('elevation', elevation, defaultValue: 0.0));
255
    description.add(new DiagnosticsProperty<Color>('color', color, defaultValue: null));
256
    description.add(new DiagnosticsProperty<Color>('shadowColor', shadowColor, defaultValue: const Color(0xFF000000)));
257
    textStyle?.debugFillProperties(description, prefix: 'textStyle.');
258
    description.add(new DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: null));
259
    description.add(new EnumProperty<BorderRadius>('borderRadius', borderRadius, defaultValue: null));
260
  }
261 262 263

  /// The default radius of an ink splash in logical pixels.
  static const double defaultSplashRadius = 35.0;
264 265
}

266
class _MaterialState extends State<Material> with TickerProviderStateMixin {
267 268
  final GlobalKey _inkFeatureRenderer = new GlobalKey(debugLabel: 'ink renderer');

269
  Color _getBackgroundColor(BuildContext context) {
270 271 272
    if (widget.color != null)
      return widget.color;
    switch (widget.type) {
273
      case MaterialType.canvas:
274
        return Theme.of(context).canvasColor;
275
      case MaterialType.card:
276
        return Theme.of(context).cardColor;
277 278
      default:
        return null;
279 280 281
    }
  }

282
  @override
283
  Widget build(BuildContext context) {
284
    final Color backgroundColor = _getBackgroundColor(context);
285 286
    assert(backgroundColor != null || widget.type == MaterialType.transparency);
    Widget contents = widget.child;
287
    if (contents != null) {
288
      contents = new AnimatedDefaultTextStyle(
289
        style: widget.textStyle ?? Theme.of(context).textTheme.body1,
290
        duration: kThemeChangeDuration,
291 292 293
        child: contents
      );
    }
294 295
    contents = new NotificationListener<LayoutChangedNotification>(
      onNotification: (LayoutChangedNotification notification) {
296
        final _RenderInkFeatures renderer = _inkFeatureRenderer.currentContext.findRenderObject();
297
        renderer._didChangeLayout();
298
        return true;
299
      },
300
      child: new _InkFeatures(
301 302
        key: _inkFeatureRenderer,
        color: backgroundColor,
303 304
        child: contents,
        vsync: this,
305
      )
306
    );
307

308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330
    // PhysicalModel has a temporary workaround for a perfomance issue that
    // speeds up rectangular non transparent material (the workaround is to
    // skip the call to ui.Canvas.saveLayer if the border radius is 0).
    // Until the saveLayer perfomance issue is resolved, we're keeping this
    // special case here for canvas material type that is using the default
    // shape (rectangle). We could go down this fast path for explicitly
    // specified rectangles (e.g shape RoundeRectangleBorder with radius 0, but
    // we choose not to as we want the change from the fast-path to the
    // slow-path to be noticeable in the construction site of Material.
    if (widget.type == MaterialType.canvas && widget.shape == null && widget.borderRadius == null) {
      return new AnimatedPhysicalModel(
        curve: Curves.fastOutSlowIn,
        duration: kThemeChangeDuration,
        shape: BoxShape.rectangle,
        borderRadius: BorderRadius.zero,
        elevation: widget.elevation,
        color: backgroundColor,
        shadowColor: widget.shadowColor,
        animateColor: false,
        child: contents,
      );
    }

331 332 333
    final ShapeBorder shape = _getShape();

    if (widget.type == MaterialType.transparency)
334
      return _transparentInterior(shape: shape, contents: contents);
335
    
336 337 338 339 340 341 342 343 344 345 346
    return new _MaterialInterior(
      curve: Curves.fastOutSlowIn,
      duration: kThemeChangeDuration,
      shape: shape,
      elevation: widget.elevation,
      color: backgroundColor,
      shadowColor: widget.shadowColor,
      child: contents,
    );
  }

347
  static Widget _transparentInterior({ShapeBorder shape, Widget contents}) {
348
    return new ClipPath(
349 350 351 352
      child: new _ShapeBorderPaint(
        child: contents,
        shape: shape,
      ),
353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373
      clipper: new ShapeBorderClipper(
        shape: shape,
      ),
    );
  }

  // Determines the shape for this Material.
  //
  // If a shape was specified, it will determine the shape.
  // If a borderRadius was specified, the shape is a rounded
  // rectangle.
  // Otherwise, the shape is determined by the widget type as described in the
  // Material class documentation.
  ShapeBorder _getShape() {
    if (widget.shape != null)
      return widget.shape;
    if (widget.borderRadius != null)
      return new RoundedRectangleBorder(borderRadius: widget.borderRadius);
    switch (widget.type) {
      case MaterialType.canvas:
      case MaterialType.transparency:
374
        return const RoundedRectangleBorder();
375 376 377 378

      case MaterialType.card:
      case MaterialType.button:
        return new RoundedRectangleBorder(
379
          borderRadius: widget.borderRadius ?? kMaterialEdges[widget.type],
380
        );
381

382 383 384
      case MaterialType.circle:
        return const CircleBorder();
    }
385
    return const RoundedRectangleBorder();
386 387 388
  }
}

389
class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController {
390 391 392 393 394 395
  _RenderInkFeatures({
    RenderBox child,
    @required this.vsync,
    this.color,
  }) : assert(vsync != null),
       super(child);
396 397 398 399

  // This class should exist in a 1:1 relationship with a MaterialState object,
  // since there's no current support for dynamically changing the ticker
  // provider.
400
  @override
401
  final TickerProvider vsync;
402 403 404 405

  // This is here to satisfy the MaterialInkController contract.
  // The actual painting of this color is done by a Container in the
  // MaterialState build method.
406
  @override
407 408
  Color color;

409
  List<InkFeature> _inkFeatures;
410

411
  @override
412 413
  void addInkFeature(InkFeature feature) {
    assert(!feature._debugDisposed);
414
    assert(feature._controller == this);
415
    _inkFeatures ??= <InkFeature>[];
416 417 418 419 420 421
    assert(!_inkFeatures.contains(feature));
    _inkFeatures.add(feature);
    markNeedsPaint();
  }

  void _removeFeature(InkFeature feature) {
422
    assert(_inkFeatures != null);
423 424 425 426
    _inkFeatures.remove(feature);
    markNeedsPaint();
  }

427
  void _didChangeLayout() {
428
    if (_inkFeatures != null && _inkFeatures.isNotEmpty)
429 430 431
      markNeedsPaint();
  }

432
  @override
433
  bool hitTestSelf(Offset position) => true;
434

435
  @override
436
  void paint(PaintingContext context, Offset offset) {
437
    if (_inkFeatures != null && _inkFeatures.isNotEmpty) {
438 439 440
      final Canvas canvas = context.canvas;
      canvas.save();
      canvas.translate(offset.dx, offset.dy);
441
      canvas.clipRect(Offset.zero & size);
442 443 444 445 446 447 448 449
      for (InkFeature inkFeature in _inkFeatures)
        inkFeature._paint(canvas);
      canvas.restore();
    }
    super.paint(context, offset);
  }
}

450
class _InkFeatures extends SingleChildRenderObjectWidget {
451 452 453 454 455 456
  const _InkFeatures({
    Key key,
    this.color,
    @required this.vsync,
    Widget child,
  }) : super(key: key, child: child);
457 458 459

  // This widget must be owned by a MaterialState, which must be provided as the vsync.
  // This relationship must be 1:1 and cannot change for the lifetime of the MaterialState.
460 461 462

  final Color color;

463 464
  final TickerProvider vsync;

465
  @override
466 467 468
  _RenderInkFeatures createRenderObject(BuildContext context) {
    return new _RenderInkFeatures(
      color: color,
469
      vsync: vsync,
470 471
    );
  }
472

473
  @override
474
  void updateRenderObject(BuildContext context, _RenderInkFeatures renderObject) {
475
    renderObject.color = color;
476
    assert(vsync == renderObject.vsync);
477 478 479
  }
}

480 481
/// A visual reaction on a piece of [Material].
///
482 483 484
/// To add an ink feature to a piece of [Material], obtain the
/// [MaterialInkController] via [Material.of] and call
/// [MaterialInkController.addInkFeature].
485
abstract class InkFeature {
486
  /// Initializes fields for subclasses.
487
  InkFeature({
488 489
    @required MaterialInkController controller,
    @required this.referenceBox,
490
    this.onRemoved,
491 492 493
  }) : assert(controller != null),
       assert(referenceBox != null),
       _controller = controller;
494

495 496 497 498
  /// The [MaterialInkController] associated with this [InkFeature].
  ///
  /// Typically used by subclasses to call
  /// [MaterialInkController.markNeedsPaint] when they need to repaint.
499
  MaterialInkController get controller => _controller;
500
  _RenderInkFeatures _controller;
501 502

  /// The render box whose visual position defines the frame of reference for this ink feature.
503
  final RenderBox referenceBox;
504 505

  /// Called when the ink feature is no longer visible on the material.
506 507 508 509
  final VoidCallback onRemoved;

  bool _debugDisposed = false;

510
  /// Free up the resources associated with this ink feature.
511
  @mustCallSuper
512 513
  void dispose() {
    assert(!_debugDisposed);
514
    assert(() { _debugDisposed = true; return true; }());
515
    _controller._removeFeature(this);
516 517 518 519 520 521 522 523
    if (onRemoved != null)
      onRemoved();
  }

  void _paint(Canvas canvas) {
    assert(referenceBox.attached);
    assert(!_debugDisposed);
    // find the chain of renderers from us to the feature's referenceBox
524 525
    final List<RenderObject> descendants = <RenderObject>[referenceBox];
    RenderObject node = referenceBox;
526
    while (node != _controller) {
527 528
      node = node.parent;
      assert(node != null);
529
      descendants.add(node);
530 531
    }
    // determine the transform that gets our coordinate system to be like theirs
532
    final Matrix4 transform = new Matrix4.identity();
533 534 535
    assert(descendants.length >= 2);
    for (int index = descendants.length - 1; index > 0; index -= 1)
      descendants[index].applyPaintTransform(descendants[index - 1], transform);
536 537 538
    paintFeature(canvas, transform);
  }

539 540 541
  /// Override this method to paint the ink feature.
  ///
  /// The transform argument gives the coordinate conversion from the coordinate
542
  /// system of the canvas to the coordinate system of the [referenceBox].
543
  @protected
544 545
  void paintFeature(Canvas canvas, Matrix4 transform);

546
  @override
547
  String toString() => describeIdentity(this);
548
}
549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633

/// An interpolation between two [ShapeBorder]s.
///
/// This class specializes the interpolation of [Tween] to use [ShapeBorder.lerp].
class ShapeBorderTween extends Tween<ShapeBorder> {
  /// Creates a [ShapeBorder] tween.
  ///
  /// the [begin] and [end] properties may be null; see [ShapeBorder.lerp] for
  /// the null handling semantics.
  ShapeBorderTween({ShapeBorder begin, ShapeBorder end}): super(begin: begin, end: end);

  /// Returns the value this tween has at the given animation clock value.
  @override
  ShapeBorder lerp(double t) {
    return ShapeBorder.lerp(begin, end, t);
  }
}

/// The interior of non-transparent material.
///
/// Animates [elevation], [shadowColor], and [shape].
class _MaterialInterior extends ImplicitlyAnimatedWidget {
  const _MaterialInterior({
    Key key,
    @required this.child,
    @required this.shape,
    @required this.elevation,
    @required this.color,
    @required this.shadowColor,
    Curve curve: Curves.linear,
    @required Duration duration,
  }) : assert(child != null),
       assert(shape != null),
       assert(elevation != null),
       assert(color != null),
       assert(shadowColor != null),
       super(key: key, curve: curve, duration: duration);

  /// The widget below this widget in the tree.
  ///
  /// {@macro flutter.widgets.child}
  final Widget child;

  /// The border of the widget.
  ///
  /// This border will be painted, and in addition the outer path of the border
  /// determines the physical shape.
  final ShapeBorder shape;

  /// The target z-coordinate at which to place this physical object.
  final double elevation;

  /// The target background color.
  final Color color;

  /// The target shadow color.
  final Color shadowColor;

  @override
  _MaterialInteriorState createState() => new _MaterialInteriorState();

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder description) {
    super.debugFillProperties(description);
    description.add(new DiagnosticsProperty<ShapeBorder>('shape', shape));
    description.add(new DoubleProperty('elevation', elevation));
    description.add(new DiagnosticsProperty<Color>('color', color));
    description.add(new DiagnosticsProperty<Color>('shadowColor', shadowColor));
  }
}

class _MaterialInteriorState extends AnimatedWidgetBaseState<_MaterialInterior> {
  Tween<double> _elevation;
  ColorTween _shadowColor;
  ShapeBorderTween _border;

  @override
  void forEachTween(TweenVisitor<dynamic> visitor) {
    _elevation = visitor(_elevation, widget.elevation, (dynamic value) => new Tween<double>(begin: value));
    _shadowColor = visitor(_shadowColor, widget.shadowColor, (dynamic value) => new ColorTween(begin: value));
    _border = visitor(_border, widget.shape, (dynamic value) => new ShapeBorderTween(begin: value));
  }

  @override
  Widget build(BuildContext context) {
634
    final ShapeBorder shape = _border.evaluate(animation);
635
    return new PhysicalShape(
636 637 638 639
      child: new _ShapeBorderPaint(
        child: widget.child,
        shape: shape,
      ),
640
      clipper: new ShapeBorderClipper(
641
        shape: shape,
642 643 644 645 646 647 648 649
        textDirection: Directionality.of(context)
      ),
      elevation: _elevation.evaluate(animation),
      color: widget.color,
      shadowColor: _shadowColor.evaluate(animation),
    );
  }
}
650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683

class _ShapeBorderPaint extends StatelessWidget {
  const _ShapeBorderPaint({
    @required this.child,
    @required this.shape,
  });

  final Widget child;
  final ShapeBorder shape;

  @override
  Widget build(BuildContext context) {
    return new CustomPaint(
      child: child,
      foregroundPainter: new _ShapeBorderPainter(shape, Directionality.of(context)),
    );
  }
}

class _ShapeBorderPainter extends CustomPainter {
  _ShapeBorderPainter(this.border, this.textDirection);
  final ShapeBorder border;
  final TextDirection textDirection;

  @override
  void paint(Canvas canvas, Size size) {
    border.paint(canvas, Offset.zero & size, textDirection: textDirection);
  }

  @override
  bool shouldRepaint(_ShapeBorderPainter oldDelegate) {
    return oldDelegate.border != border;
  }
}