material.dart 23.3 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
/// The Material widget is responsible for:
///
/// 1. Clipping: Material clips its widget sub-tree to the shape specified by
91
///    [shape], [type], and [borderRadius].
92 93 94 95 96 97 98
/// 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
/// [Material] should not change its [color], [shadowColor] or [type]).
111 112 113 114
/// Changes to [elevation] and [shadowColor] are animated for [animationDuration].
/// Changes to [shape] are animated if [type] is not [MaterialType.transparency]
/// and [ShapeBorder.lerp] between the previous and next [shape] values is
/// supported. Shape changes are also animated for [animationDuration].
115
///
116 117 118
///
/// ## Shape
///
119
/// The shape for material is determined by [shape], [type], and [borderRadius].
120
///
121 122 123 124 125
///  - 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:
126 127 128 129 130 131 132 133
///    - [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.
///
134 135 136 137
/// ## Border
///
/// If [shape] is not null, then its border will also be painted (if any).
///
138
/// ## Layout change notifications
139
///
Ian Hickson's avatar
Ian Hickson committed
140 141 142 143 144 145 146 147
/// 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.
148 149 150
///
/// See also:
///
151 152
/// * [MergeableMaterial], a piece of material that can split and remerge.
/// * [Card], a wrapper for a [Material] of [type] [MaterialType.card].
153
/// * <https://material.google.com/>
154
class Material extends StatefulWidget {
155 156
  /// Creates a piece of material.
  ///
157 158
  /// The [type], [elevation], [shadowColor], and [animationDuration] arguments
  /// must not be null.
159
  ///
160
  /// If a [shape] is specified, then the [borderRadius] property must be
161 162 163 164
  /// 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.
165
  const Material({
166
    Key key,
167
    this.type: MaterialType.canvas,
168
    this.elevation: 0.0,
169
    this.color,
170
    this.shadowColor: const Color(0xFF000000),
171
    this.textStyle,
172
    this.borderRadius,
173
    this.shape,
174
    this.animationDuration: kThemeChangeDuration,
175
    this.child,
176 177
  }) : assert(type != null),
       assert(elevation != null),
178
       assert(shadowColor != null),
179
       assert(!(shape != null && borderRadius != null)),
180
       assert(animationDuration != null),
181
       assert(!(identical(type, MaterialType.circle) && (borderRadius != null || shape != null))),
182
       super(key: key);
183

184
  /// The widget below this widget in the tree.
185 186
  ///
  /// {@macro flutter.widgets.child}
187
  final Widget child;
188

189 190 191
  /// 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.
192
  final MaterialType type;
193

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

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

212 213 214 215 216
  /// The color to paint the shadow below the material.
  ///
  /// Defaults to fully opaque black.
  final Color shadowColor;

217
  /// The typographical style to use for text within this material.
218
  final TextStyle textStyle;
219

220 221 222 223 224 225 226
  /// 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.
227 228
  final ShapeBorder shape;

229 230 231 232 233 234
  /// Defines the duration of animated changes for [shape], [elevation],
  /// and [shadowColor].
  ///
  /// The default value is [kThemeChangeDuration].
  final Duration animationDuration;

235 236 237 238
  /// 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.
  ///
239 240
  /// If [shape] is non null then the border radius is ignored.
  ///
241 242 243
  /// Must be null if [type] is [MaterialType.circle].
  final BorderRadius borderRadius;

244 245
  /// The ink controller from the closest instance of this class that
  /// encloses the given context.
246 247 248 249 250 251
  ///
  /// Typical usage is as follows:
  ///
  /// ```dart
  /// MaterialInkController inkController = Material.of(context);
  /// ```
252
  static MaterialInkController of(BuildContext context) {
253
    final _RenderInkFeatures result = context.ancestorRenderObjectOfType(const TypeMatcher<_RenderInkFeatures>());
254 255 256
    return result;
  }

257
  @override
258
  _MaterialState createState() => new _MaterialState();
259

260
  @override
261 262 263 264 265 266 267 268 269
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(new EnumProperty<MaterialType>('type', type));
    properties.add(new DoubleProperty('elevation', elevation, defaultValue: 0.0));
    properties.add(new DiagnosticsProperty<Color>('color', color, defaultValue: null));
    properties.add(new DiagnosticsProperty<Color>('shadowColor', shadowColor, defaultValue: const Color(0xFF000000)));
    textStyle?.debugFillProperties(properties, prefix: 'textStyle.');
    properties.add(new DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: null));
    properties.add(new EnumProperty<BorderRadius>('borderRadius', borderRadius, defaultValue: null));
270
  }
271 272 273

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

276
class _MaterialState extends State<Material> with TickerProviderStateMixin {
277 278
  final GlobalKey _inkFeatureRenderer = new GlobalKey(debugLabel: 'ink renderer');

279
  Color _getBackgroundColor(BuildContext context) {
280 281 282
    if (widget.color != null)
      return widget.color;
    switch (widget.type) {
283
      case MaterialType.canvas:
284
        return Theme.of(context).canvasColor;
285
      case MaterialType.card:
286
        return Theme.of(context).cardColor;
287 288
      default:
        return null;
289 290 291
    }
  }

292
  @override
293
  Widget build(BuildContext context) {
294
    final Color backgroundColor = _getBackgroundColor(context);
295 296
    assert(backgroundColor != null || widget.type == MaterialType.transparency);
    Widget contents = widget.child;
297
    if (contents != null) {
298
      contents = new AnimatedDefaultTextStyle(
299
        style: widget.textStyle ?? Theme.of(context).textTheme.body1,
300
        duration: widget.animationDuration,
301 302 303
        child: contents
      );
    }
304 305
    contents = new NotificationListener<LayoutChangedNotification>(
      onNotification: (LayoutChangedNotification notification) {
306
        final _RenderInkFeatures renderer = _inkFeatureRenderer.currentContext.findRenderObject();
307
        renderer._didChangeLayout();
308
        return true;
309
      },
310
      child: new _InkFeatures(
311 312
        key: _inkFeatureRenderer,
        color: backgroundColor,
313 314
        child: contents,
        vsync: this,
315
      )
316
    );
317

Josh Soref's avatar
Josh Soref committed
318
    // PhysicalModel has a temporary workaround for a performance issue that
319 320
    // speeds up rectangular non transparent material (the workaround is to
    // skip the call to ui.Canvas.saveLayer if the border radius is 0).
Josh Soref's avatar
Josh Soref committed
321
    // Until the saveLayer performance issue is resolved, we're keeping this
322 323
    // special case here for canvas material type that is using the default
    // shape (rectangle). We could go down this fast path for explicitly
Josh Soref's avatar
Josh Soref committed
324
    // specified rectangles (e.g shape RoundedRectangleBorder with radius 0, but
325 326 327 328 329
    // 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,
330
        duration: widget.animationDuration,
331 332 333 334 335 336 337 338 339 340
        shape: BoxShape.rectangle,
        borderRadius: BorderRadius.zero,
        elevation: widget.elevation,
        color: backgroundColor,
        shadowColor: widget.shadowColor,
        animateColor: false,
        child: contents,
      );
    }

341 342 343
    final ShapeBorder shape = _getShape();

    if (widget.type == MaterialType.transparency)
344
      return _transparentInterior(shape: shape, contents: contents);
345

346 347
    return new _MaterialInterior(
      curve: Curves.fastOutSlowIn,
348
      duration: widget.animationDuration,
349 350 351 352 353 354 355 356
      shape: shape,
      elevation: widget.elevation,
      color: backgroundColor,
      shadowColor: widget.shadowColor,
      child: contents,
    );
  }

357
  static Widget _transparentInterior({ShapeBorder shape, Widget contents}) {
358
    return new ClipPath(
359 360 361 362
      child: new _ShapeBorderPaint(
        child: contents,
        shape: shape,
      ),
363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383
      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:
384
        return const RoundedRectangleBorder();
385 386 387 388

      case MaterialType.card:
      case MaterialType.button:
        return new RoundedRectangleBorder(
389
          borderRadius: widget.borderRadius ?? kMaterialEdges[widget.type],
390
        );
391

392 393 394
      case MaterialType.circle:
        return const CircleBorder();
    }
395
    return const RoundedRectangleBorder();
396 397 398
  }
}

399
class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController {
400 401 402 403 404 405
  _RenderInkFeatures({
    RenderBox child,
    @required this.vsync,
    this.color,
  }) : assert(vsync != null),
       super(child);
406 407 408 409

  // 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.
410
  @override
411
  final TickerProvider vsync;
412 413 414 415

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

419
  List<InkFeature> _inkFeatures;
420

421
  @override
422 423
  void addInkFeature(InkFeature feature) {
    assert(!feature._debugDisposed);
424
    assert(feature._controller == this);
425
    _inkFeatures ??= <InkFeature>[];
426 427 428 429 430 431
    assert(!_inkFeatures.contains(feature));
    _inkFeatures.add(feature);
    markNeedsPaint();
  }

  void _removeFeature(InkFeature feature) {
432
    assert(_inkFeatures != null);
433 434 435 436
    _inkFeatures.remove(feature);
    markNeedsPaint();
  }

437
  void _didChangeLayout() {
438
    if (_inkFeatures != null && _inkFeatures.isNotEmpty)
439 440 441
      markNeedsPaint();
  }

442
  @override
443
  bool hitTestSelf(Offset position) => true;
444

445
  @override
446
  void paint(PaintingContext context, Offset offset) {
447
    if (_inkFeatures != null && _inkFeatures.isNotEmpty) {
448 449 450
      final Canvas canvas = context.canvas;
      canvas.save();
      canvas.translate(offset.dx, offset.dy);
451
      canvas.clipRect(Offset.zero & size);
452 453 454 455 456 457 458 459
      for (InkFeature inkFeature in _inkFeatures)
        inkFeature._paint(canvas);
      canvas.restore();
    }
    super.paint(context, offset);
  }
}

460
class _InkFeatures extends SingleChildRenderObjectWidget {
461 462 463 464 465 466
  const _InkFeatures({
    Key key,
    this.color,
    @required this.vsync,
    Widget child,
  }) : super(key: key, child: child);
467 468 469

  // 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.
470 471 472

  final Color color;

473 474
  final TickerProvider vsync;

475
  @override
476 477 478
  _RenderInkFeatures createRenderObject(BuildContext context) {
    return new _RenderInkFeatures(
      color: color,
479
      vsync: vsync,
480 481
    );
  }
482

483
  @override
484
  void updateRenderObject(BuildContext context, _RenderInkFeatures renderObject) {
485
    renderObject.color = color;
486
    assert(vsync == renderObject.vsync);
487 488 489
  }
}

490 491
/// A visual reaction on a piece of [Material].
///
492 493 494
/// To add an ink feature to a piece of [Material], obtain the
/// [MaterialInkController] via [Material.of] and call
/// [MaterialInkController.addInkFeature].
495
abstract class InkFeature {
496
  /// Initializes fields for subclasses.
497
  InkFeature({
498 499
    @required MaterialInkController controller,
    @required this.referenceBox,
500
    this.onRemoved,
501 502 503
  }) : assert(controller != null),
       assert(referenceBox != null),
       _controller = controller;
504

505 506 507 508
  /// The [MaterialInkController] associated with this [InkFeature].
  ///
  /// Typically used by subclasses to call
  /// [MaterialInkController.markNeedsPaint] when they need to repaint.
509
  MaterialInkController get controller => _controller;
510
  _RenderInkFeatures _controller;
511 512

  /// The render box whose visual position defines the frame of reference for this ink feature.
513
  final RenderBox referenceBox;
514 515

  /// Called when the ink feature is no longer visible on the material.
516 517 518 519
  final VoidCallback onRemoved;

  bool _debugDisposed = false;

520
  /// Free up the resources associated with this ink feature.
521
  @mustCallSuper
522 523
  void dispose() {
    assert(!_debugDisposed);
524
    assert(() { _debugDisposed = true; return true; }());
525
    _controller._removeFeature(this);
526 527 528 529 530 531 532 533
    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
534 535
    final List<RenderObject> descendants = <RenderObject>[referenceBox];
    RenderObject node = referenceBox;
536
    while (node != _controller) {
537 538
      node = node.parent;
      assert(node != null);
539
      descendants.add(node);
540 541
    }
    // determine the transform that gets our coordinate system to be like theirs
542
    final Matrix4 transform = new Matrix4.identity();
543 544 545
    assert(descendants.length >= 2);
    for (int index = descendants.length - 1; index > 0; index -= 1)
      descendants[index].applyPaintTransform(descendants[index - 1], transform);
546 547 548
    paintFeature(canvas, transform);
  }

549 550 551
  /// Override this method to paint the ink feature.
  ///
  /// The transform argument gives the coordinate conversion from the coordinate
552
  /// system of the canvas to the coordinate system of the [referenceBox].
553
  @protected
554 555
  void paintFeature(Canvas canvas, Matrix4 transform);

556
  @override
557
  String toString() => describeIdentity(this);
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 634 635 636 637 638 639 640 641 642 643

/// 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) {
644
    final ShapeBorder shape = _border.evaluate(animation);
645
    return new PhysicalShape(
646 647 648 649
      child: new _ShapeBorderPaint(
        child: widget.child,
        shape: shape,
      ),
650
      clipper: new ShapeBorderClipper(
651
        shape: shape,
652 653 654 655 656 657 658 659
        textDirection: Directionality.of(context)
      ),
      elevation: _elevation.evaluate(animation),
      color: widget.color,
      shadowColor: _shadowColor.evaluate(animation),
    );
  }
}
660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693

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