material.dart 27.3 KB
Newer Older
1 2 3
// 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.
4
import 'dart:math' as math;
5

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

10
import 'colors.dart';
11
import 'constants.dart';
12
import 'theme.dart';
13

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

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

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

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

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

  /// A transparent piece of material that draws ink splashes and highlights.
40 41 42 43 44 45 46 47 48
  ///
  /// 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.
49
  transparency
50 51
}

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

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

73
  /// The ticker provider used by the controller.
74
  ///
75 76 77 78 79
  /// 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].
80
  ///
81
  /// The ink feature will paint as part of this controller.
82
  void addInkFeature(InkFeature feature);
83 84 85

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

88 89
/// A piece of material.
///
90 91
/// The Material widget is responsible for:
///
92 93 94
/// 1. Clipping: If [clipBehavior] is not [Clip.none], Material clips its widget
///    sub-tree to the shape specified by [shape], [type], and [borderRadius].
///    By default, [clipBehavior] is [Clip.none] for performance considerations.
95 96 97 98 99 100 101
/// 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
///
102 103 104
/// 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
105
/// shadows.
106 107 108 109 110 111
///
/// 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].
///
112
/// In general, the features of a [Material] should not change over time (e.g. a
113
/// [Material] should not change its [color], [shadowColor] or [type]).
114 115 116 117
/// 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].
118
///
119 120 121
///
/// ## Shape
///
122
/// The shape for material is determined by [shape], [type], and [borderRadius].
123
///
124 125 126 127 128
///  - 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:
129 130 131 132 133 134 135 136
///    - [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.
///
137 138 139 140
/// ## Border
///
/// If [shape] is not null, then its border will also be painted (if any).
///
141
/// ## Layout change notifications
142
///
Ian Hickson's avatar
Ian Hickson committed
143 144 145 146 147 148 149 150
/// 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.
151 152 153
///
/// See also:
///
154
///  * [MergeableMaterial], a piece of material that can split and re-merge.
155
///  * [Card], a wrapper for a [Material] of [type] [MaterialType.card].
156
///  * <https://material.io/design/>
157
class Material extends StatefulWidget {
158 159
  /// Creates a piece of material.
  ///
160 161 162
  /// The [type], [elevation], [shadowColor], [borderOnForeground] and
  /// [animationDuration] arguments must not be null. Additionally, [elevation]
  /// must be non-negative.
163
  ///
164
  /// If a [shape] is specified, then the [borderRadius] property must be
165 166 167 168
  /// 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.
169
  const Material({
170
    Key key,
171 172
    this.type = MaterialType.canvas,
    this.elevation = 0.0,
173
    this.color,
174
    this.shadowColor = const Color(0xFF000000),
175
    this.textStyle,
176
    this.borderRadius,
177
    this.shape,
178
    this.borderOnForeground = true,
179
    this.clipBehavior = Clip.none,
180
    this.animationDuration = kThemeChangeDuration,
181
    this.child,
182
  }) : assert(type != null),
183
       assert(elevation != null && elevation >= 0.0),
184
       assert(shadowColor != null),
185
       assert(!(shape != null && borderRadius != null)),
186
       assert(animationDuration != null),
187
       assert(!(identical(type, MaterialType.circle) && (borderRadius != null || shape != null))),
188
       assert(clipBehavior != null),
189
       assert(borderOnForeground != null),
190
       super(key: key);
191

192
  /// The widget below this widget in the tree.
193 194
  ///
  /// {@macro flutter.widgets.child}
195
  final Widget child;
196

197 198 199
  /// 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.
200
  final MaterialType type;
201

202
  /// {@template flutter.material.material.elevation}
203 204
  /// The z-coordinate at which to place this material relative to its parent.
  ///
205 206
  /// This controls the size of the shadow below the material and the opacity
  /// of the elevation overlay color if it is applied.
207
  ///
208
  /// If this is non-zero, the contents of the material are clipped, because the
209 210
  /// widget conceptually defines an independent printed piece of material.
  ///
211 212
  /// Defaults to 0. Changing this value will cause the shadow and the elevation
  /// overlay to animate over [animationDuration].
213 214
  ///
  /// The value is non-negative.
215 216 217 218 219 220 221
  ///
  /// See also:
  ///
  ///   * [ThemeData.applyElevationOverlayColor] which controls the whether
  ///     an overlay color will be applied to indicate elevation.
  ///   * [color] which may have an elevation overlay applied.
  ///
222
  /// {@endtemplate}
223
  final double elevation;
224

225
  /// The color to paint the material.
226 227 228
  ///
  /// Must be opaque. To create a transparent piece of material, use
  /// [MaterialType.transparency].
229
  ///
230 231 232 233 234
  /// To support dark themes, if the surrounding
  /// [ThemeData.applyElevationOverlayColor] is [true] and
  /// this color is [ThemeData.colorScheme.surface] then a semi-transparent
  /// white will be composited on top this color to indicate the elevation.
  ///
235
  /// By default, the color is derived from the [type] of material.
236
  final Color color;
237

238 239 240 241 242
  /// The color to paint the shadow below the material.
  ///
  /// Defaults to fully opaque black.
  final Color shadowColor;

243
  /// The typographical style to use for text within this material.
244
  final TextStyle textStyle;
245

246 247 248 249 250 251 252
  /// 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.
253 254
  final ShapeBorder shape;

255 256 257 258 259 260
  /// Whether to paint the [shape] border in front of the [child].
  ///
  /// The default value is true.
  /// If false, the border will be painted behind the [child].
  final bool borderOnForeground;

261 262 263 264 265 266 267 268
  /// {@template flutter.widgets.Clip}
  /// The content will be clipped (or not) according to this option.
  ///
  /// See the enum [Clip] for details of all possible options and their common
  /// use cases.
  /// {@endtemplate}
  final Clip clipBehavior;

269
  /// Defines the duration of animated changes for [shape], [elevation],
270
  /// [shadowColor] and the elevation overlay if it is applied.
271 272 273 274
  ///
  /// The default value is [kThemeChangeDuration].
  final Duration animationDuration;

275 276 277
  /// If non-null, the corners of this box are rounded by this
  /// [BorderRadiusGeometry] value.
  ///
278 279 280
  /// Otherwise, the corners specified for the current [type] of material are
  /// used.
  ///
281 282
  /// If [shape] is non null then the border radius is ignored.
  ///
283
  /// Must be null if [type] is [MaterialType.circle].
284
  final BorderRadiusGeometry borderRadius;
285

286 287
  /// The ink controller from the closest instance of this class that
  /// encloses the given context.
288 289 290 291 292 293
  ///
  /// Typical usage is as follows:
  ///
  /// ```dart
  /// MaterialInkController inkController = Material.of(context);
  /// ```
294
  static MaterialInkController of(BuildContext context) {
295
    final _RenderInkFeatures result = context.ancestorRenderObjectOfType(const TypeMatcher<_RenderInkFeatures>());
296 297 298
    return result;
  }

299
  @override
300
  _MaterialState createState() => _MaterialState();
301

302
  @override
303 304
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
305 306
    properties.add(EnumProperty<MaterialType>('type', type));
    properties.add(DoubleProperty('elevation', elevation, defaultValue: 0.0));
307 308
    properties.add(ColorProperty('color', color, defaultValue: null));
    properties.add(ColorProperty('shadowColor', shadowColor, defaultValue: const Color(0xFF000000)));
309
    textStyle?.debugFillProperties(properties, prefix: 'textStyle.');
310
    properties.add(DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: null));
311
    properties.add(DiagnosticsProperty<bool>('borderOnForeground', borderOnForeground, defaultValue: true));
312
    properties.add(DiagnosticsProperty<BorderRadiusGeometry>('borderRadius', borderRadius, defaultValue: null));
313
  }
314 315 316

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

319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336
// Apply a semi-transparent white on surface colors to
// indicate the level of elevation.
Color _elevationOverlayColor(BuildContext context, Color background, double elevation) {
  final ThemeData theme = Theme.of(context);
  if (elevation > 0.0 &&
      theme.applyElevationOverlayColor &&
      background == theme.colorScheme.surface) {

    // Compute the opacity for the given elevation
    // This formula matches the values in the spec:
    // https://material.io/design/color/dark-theme.html#properties
    final double opacity = (4.5 * math.log(elevation + 1) + 2) / 100.0;
    final Color overlay = Colors.white.withOpacity(opacity);
    return Color.alphaBlend(overlay, background);
  }
  return background;
}

337
class _MaterialState extends State<Material> with TickerProviderStateMixin {
338
  final GlobalKey _inkFeatureRenderer = GlobalKey(debugLabel: 'ink renderer');
339

340
  Color _getBackgroundColor(BuildContext context) {
341 342 343 344 345 346 347 348 349 350 351 352 353
    final ThemeData theme = Theme.of(context);
    Color color = widget.color;
    if (color == null) {
      switch (widget.type) {
        case MaterialType.canvas:
          color = theme.canvasColor;
          break;
        case MaterialType.card:
          color = theme.cardColor;
          break;
        default:
          break;
      }
354
    }
355
    return color;
356 357
  }

358
  @override
359
  Widget build(BuildContext context) {
360
    final Color backgroundColor = _getBackgroundColor(context);
361 362 363 364 365 366 367
    assert(
      backgroundColor != null || widget.type == MaterialType.transparency,
      'If Material type is not MaterialType.transparency, a color must '
      'either be passed in through the `color` property, or be defined '
      'in the theme (ex. canvasColor != null if type is set to '
      'MaterialType.canvas)'
    );
368
    Widget contents = widget.child;
369
    if (contents != null) {
370
      contents = AnimatedDefaultTextStyle(
371
        style: widget.textStyle ?? Theme.of(context).textTheme.body1,
372
        duration: widget.animationDuration,
373
        child: contents,
374 375
      );
    }
376
    contents = NotificationListener<LayoutChangedNotification>(
377
      onNotification: (LayoutChangedNotification notification) {
378
        final _RenderInkFeatures renderer = _inkFeatureRenderer.currentContext.findRenderObject();
379
        renderer._didChangeLayout();
380
        return false;
381
      },
382
      child: _InkFeatures(
383 384
        key: _inkFeatureRenderer,
        color: backgroundColor,
385 386
        child: contents,
        vsync: this,
387
      ),
388
    );
389

Josh Soref's avatar
Josh Soref committed
390
    // PhysicalModel has a temporary workaround for a performance issue that
391 392
    // 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
393
    // Until the saveLayer performance issue is resolved, we're keeping this
394 395
    // 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
396
    // specified rectangles (e.g shape RoundedRectangleBorder with radius 0, but
397 398 399
    // 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) {
400
      return AnimatedPhysicalModel(
401
        curve: Curves.fastOutSlowIn,
402
        duration: widget.animationDuration,
403
        shape: BoxShape.rectangle,
404
        clipBehavior: widget.clipBehavior,
405 406
        borderRadius: BorderRadius.zero,
        elevation: widget.elevation,
407
        color: _elevationOverlayColor(context, backgroundColor, widget.elevation),
408 409 410 411 412 413
        shadowColor: widget.shadowColor,
        animateColor: false,
        child: contents,
      );
    }

414 415
    final ShapeBorder shape = _getShape();

416 417 418 419 420 421 422 423
    if (widget.type == MaterialType.transparency) {
      return _transparentInterior(
        context: context,
        shape: shape,
        clipBehavior: widget.clipBehavior,
        contents: contents,
      );
    }
424

425
    return _MaterialInterior(
426
      curve: Curves.fastOutSlowIn,
427
      duration: widget.animationDuration,
428
      shape: shape,
429
      borderOnForeground: widget.borderOnForeground,
430
      clipBehavior: widget.clipBehavior,
431 432 433 434 435 436 437
      elevation: widget.elevation,
      color: backgroundColor,
      shadowColor: widget.shadowColor,
      child: contents,
    );
  }

438 439 440 441 442 443
  static Widget _transparentInterior({
    @required BuildContext context,
    @required ShapeBorder shape,
    @required Clip clipBehavior,
    @required Widget contents,
  }) {
444
    final _ShapeBorderPaint child = _ShapeBorderPaint(
445 446 447 448 449 450
      child: contents,
      shape: shape,
    );
    if (clipBehavior == Clip.none) {
      return child;
    }
451
    return ClipPath(
452
      child: child,
453 454 455 456
      clipper: ShapeBorderClipper(
        shape: shape,
        textDirection: Directionality.of(context),
      ),
457
      clipBehavior: clipBehavior,
458 459 460 461 462 463 464 465 466 467 468 469 470 471
    );
  }

  // 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)
472
      return RoundedRectangleBorder(borderRadius: widget.borderRadius);
473 474 475
    switch (widget.type) {
      case MaterialType.canvas:
      case MaterialType.transparency:
476
        return const RoundedRectangleBorder();
477 478 479

      case MaterialType.card:
      case MaterialType.button:
480
        return RoundedRectangleBorder(
481
          borderRadius: widget.borderRadius ?? kMaterialEdges[widget.type],
482
        );
483

484 485 486
      case MaterialType.circle:
        return const CircleBorder();
    }
487
    return const RoundedRectangleBorder();
488 489 490
  }
}

491
class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController {
492 493 494 495 496 497
  _RenderInkFeatures({
    RenderBox child,
    @required this.vsync,
    this.color,
  }) : assert(vsync != null),
       super(child);
498 499 500 501

  // 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.
502
  @override
503
  final TickerProvider vsync;
504 505 506 507

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

511
  List<InkFeature> _inkFeatures;
512

513
  @override
514 515
  void addInkFeature(InkFeature feature) {
    assert(!feature._debugDisposed);
516
    assert(feature._controller == this);
517
    _inkFeatures ??= <InkFeature>[];
518 519 520 521 522 523
    assert(!_inkFeatures.contains(feature));
    _inkFeatures.add(feature);
    markNeedsPaint();
  }

  void _removeFeature(InkFeature feature) {
524
    assert(_inkFeatures != null);
525 526 527 528
    _inkFeatures.remove(feature);
    markNeedsPaint();
  }

529
  void _didChangeLayout() {
530
    if (_inkFeatures != null && _inkFeatures.isNotEmpty)
531 532 533
      markNeedsPaint();
  }

534
  @override
535
  bool hitTestSelf(Offset position) => true;
536

537
  @override
538
  void paint(PaintingContext context, Offset offset) {
539
    if (_inkFeatures != null && _inkFeatures.isNotEmpty) {
540 541 542
      final Canvas canvas = context.canvas;
      canvas.save();
      canvas.translate(offset.dx, offset.dy);
543
      canvas.clipRect(Offset.zero & size);
544 545 546 547 548 549 550 551
      for (InkFeature inkFeature in _inkFeatures)
        inkFeature._paint(canvas);
      canvas.restore();
    }
    super.paint(context, offset);
  }
}

552
class _InkFeatures extends SingleChildRenderObjectWidget {
553 554 555 556 557 558
  const _InkFeatures({
    Key key,
    this.color,
    @required this.vsync,
    Widget child,
  }) : super(key: key, child: child);
559 560 561

  // 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.
562 563 564

  final Color color;

565 566
  final TickerProvider vsync;

567
  @override
568
  _RenderInkFeatures createRenderObject(BuildContext context) {
569
    return _RenderInkFeatures(
570
      color: color,
571
      vsync: vsync,
572 573
    );
  }
574

575
  @override
576
  void updateRenderObject(BuildContext context, _RenderInkFeatures renderObject) {
577
    renderObject.color = color;
578
    assert(vsync == renderObject.vsync);
579 580 581
  }
}

582 583
/// A visual reaction on a piece of [Material].
///
584 585 586
/// To add an ink feature to a piece of [Material], obtain the
/// [MaterialInkController] via [Material.of] and call
/// [MaterialInkController.addInkFeature].
587
abstract class InkFeature {
588
  /// Initializes fields for subclasses.
589
  InkFeature({
590 591
    @required MaterialInkController controller,
    @required this.referenceBox,
592
    this.onRemoved,
593 594 595
  }) : assert(controller != null),
       assert(referenceBox != null),
       _controller = controller;
596

597 598 599 600
  /// The [MaterialInkController] associated with this [InkFeature].
  ///
  /// Typically used by subclasses to call
  /// [MaterialInkController.markNeedsPaint] when they need to repaint.
601
  MaterialInkController get controller => _controller;
602
  final _RenderInkFeatures _controller;
603 604

  /// The render box whose visual position defines the frame of reference for this ink feature.
605
  final RenderBox referenceBox;
606 607

  /// Called when the ink feature is no longer visible on the material.
608 609 610 611
  final VoidCallback onRemoved;

  bool _debugDisposed = false;

612
  /// Free up the resources associated with this ink feature.
613
  @mustCallSuper
614 615
  void dispose() {
    assert(!_debugDisposed);
616
    assert(() { _debugDisposed = true; return true; }());
617
    _controller._removeFeature(this);
618 619 620 621 622 623 624 625
    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
626 627
    final List<RenderObject> descendants = <RenderObject>[referenceBox];
    RenderObject node = referenceBox;
628
    while (node != _controller) {
629 630
      node = node.parent;
      assert(node != null);
631
      descendants.add(node);
632 633
    }
    // determine the transform that gets our coordinate system to be like theirs
634
    final Matrix4 transform = Matrix4.identity();
635 636 637
    assert(descendants.length >= 2);
    for (int index = descendants.length - 1; index > 0; index -= 1)
      descendants[index].applyPaintTransform(descendants[index - 1], transform);
638 639 640
    paintFeature(canvas, transform);
  }

641 642 643
  /// Override this method to paint the ink feature.
  ///
  /// The transform argument gives the coordinate conversion from the coordinate
644
  /// system of the canvas to the coordinate system of the [referenceBox].
645
  @protected
646 647
  void paintFeature(Canvas canvas, Matrix4 transform);

648
  @override
649
  String toString() => describeIdentity(this);
650
}
651 652 653 654 655 656 657 658 659

/// 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.
660
  ShapeBorderTween({ShapeBorder begin, ShapeBorder end}) : super(begin: begin, end: end);
661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676

  /// 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,
677
    this.borderOnForeground = true,
678
    this.clipBehavior = Clip.none,
679 680 681
    @required this.elevation,
    @required this.color,
    @required this.shadowColor,
682
    Curve curve = Curves.linear,
683 684 685
    @required Duration duration,
  }) : assert(child != null),
       assert(shape != null),
686
       assert(clipBehavior != null),
687
       assert(elevation != null && elevation >= 0.0),
688 689 690 691 692 693 694 695 696 697 698 699 700 701 702
       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;

703 704 705 706 707 708
  /// Whether to paint the border in front of the child.
  ///
  /// The default value is true.
  /// If false, the border will be painted behind the child.
  final bool borderOnForeground;

709 710 711
  /// {@macro flutter.widgets.Clip}
  final Clip clipBehavior;

712 713 714 715
  /// The target z-coordinate at which to place this physical object relative
  /// to its parent.
  ///
  /// The value is non-negative.
716 717 718 719 720 721 722 723 724
  final double elevation;

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

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

  @override
725
  _MaterialInteriorState createState() => _MaterialInteriorState();
726 727 728 729

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder description) {
    super.debugFillProperties(description);
730 731
    description.add(DiagnosticsProperty<ShapeBorder>('shape', shape));
    description.add(DoubleProperty('elevation', elevation));
732 733
    description.add(ColorProperty('color', color));
    description.add(ColorProperty('shadowColor', shadowColor));
734 735 736 737 738 739 740 741 742 743
  }
}

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

  @override
  void forEachTween(TweenVisitor<dynamic> visitor) {
744 745 746
    _elevation = visitor(_elevation, widget.elevation, (dynamic value) => Tween<double>(begin: value));
    _shadowColor = visitor(_shadowColor, widget.shadowColor, (dynamic value) => ColorTween(begin: value));
    _border = visitor(_border, widget.shape, (dynamic value) => ShapeBorderTween(begin: value));
747 748 749 750
  }

  @override
  Widget build(BuildContext context) {
751
    final ShapeBorder shape = _border.evaluate(animation);
752
    final double elevation = _elevation.evaluate(animation);
753 754
    return PhysicalShape(
      child: _ShapeBorderPaint(
755 756
        child: widget.child,
        shape: shape,
757
        borderOnForeground: widget.borderOnForeground,
758
      ),
759
      clipper: ShapeBorderClipper(
760
        shape: shape,
761
        textDirection: Directionality.of(context),
762
      ),
763
      clipBehavior: widget.clipBehavior,
764 765
      elevation: elevation,
      color: _elevationOverlayColor(context, widget.color, elevation),
766 767 768 769
      shadowColor: _shadowColor.evaluate(animation),
    );
  }
}
770 771 772 773 774

class _ShapeBorderPaint extends StatelessWidget {
  const _ShapeBorderPaint({
    @required this.child,
    @required this.shape,
775
    this.borderOnForeground = true,
776 777 778 779
  });

  final Widget child;
  final ShapeBorder shape;
780
  final bool borderOnForeground;
781 782 783

  @override
  Widget build(BuildContext context) {
784
    return CustomPaint(
785
      child: child,
786 787
      painter: borderOnForeground ? null : _ShapeBorderPainter(shape, Directionality.of(context)),
      foregroundPainter: borderOnForeground ? _ShapeBorderPainter(shape, Directionality.of(context)) : null,
788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806
    );
  }
}

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