material.dart 34 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
Ian Hickson's avatar
Ian Hickson committed
4

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 'elevation_overlay.dart';
11
import 'theme.dart';
12

13 14 15
// Examples can assume:
// late BuildContext context;

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

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

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

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

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

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

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

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

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

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

90 91
/// A piece of material.
///
92 93
/// The Material widget is responsible for:
///
94 95 96
/// 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.
97
///    See [Ink] for an example of how this affects clipping [Ink] widgets.
98 99 100 101 102 103 104
/// 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
///
105
/// Material is the central metaphor in Material Design. Each piece of material
106 107
/// exists at a given elevation, which influences how that piece of material
/// visually relates to other pieces of material and how that material casts
108
/// shadows.
109 110 111 112 113 114
///
/// 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].
///
115
/// In general, the features of a [Material] should not change over time (e.g. a
116
/// [Material] should not change its [color], [shadowColor] or [type]).
117 118 119 120 121
/// Changes to [elevation], [shadowColor] and [surfaceTintColor] 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].
122
///
123 124
/// ## Shape
///
125
/// The shape for material is determined by [shape], [type], and [borderRadius].
126
///
127 128 129 130 131
///  - 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:
132 133 134 135 136 137 138 139
///    - [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.
///
140 141 142 143
/// ## Border
///
/// If [shape] is not null, then its border will also be painted (if any).
///
144
/// ## Layout change notifications
145
///
Ian Hickson's avatar
Ian Hickson committed
146 147 148 149 150 151 152 153
/// 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.
154
///
155 156 157 158 159 160 161 162 163 164
/// ## Painting over the material
///
/// Material widgets will often trigger reactions on their nearest material
/// ancestor. For example, [ListTile.hoverColor] triggers a reaction on the
/// tile's material when a pointer is hovering over it. These reactions will be
/// obscured if any widget in between them and the material paints in such a
/// way as to obscure the material (such as setting a [BoxDecoration.color] on
/// a [DecoratedBox]). To avoid this behavior, use [InkDecoration] to decorate
/// the material itself.
///
165 166
/// See also:
///
167
///  * [MergeableMaterial], a piece of material that can split and re-merge.
168
///  * [Card], a wrapper for a [Material] of [type] [MaterialType.card].
169
///  * <https://material.io/design/>
170
///  * <https://m3.material.io/styles/color/the-color-system/color-roles>
171
class Material extends StatefulWidget {
172 173
  /// Creates a piece of material.
  ///
174
  /// The [type], [elevation], [borderOnForeground],
175 176
  /// [clipBehavior], and [animationDuration] arguments must not be null.
  /// Additionally, [elevation] must be non-negative.
177
  ///
178
  /// If a [shape] is specified, then the [borderRadius] property must be
179 180 181 182
  /// 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.
183
  const Material({
184
    super.key,
185 186
    this.type = MaterialType.canvas,
    this.elevation = 0.0,
187
    this.color,
188
    this.shadowColor,
189
    this.surfaceTintColor,
190
    this.textStyle,
191
    this.borderRadius,
192
    this.shape,
193
    this.borderOnForeground = true,
194
    this.clipBehavior = Clip.none,
195
    this.animationDuration = kThemeChangeDuration,
196
    this.child,
197
  }) : assert(elevation >= 0.0),
198
       assert(!(shape != null && borderRadius != null)),
199
       assert(!(identical(type, MaterialType.circle) && (borderRadius != null || shape != null)));
200

201
  /// The widget below this widget in the tree.
202
  ///
203
  /// {@macro flutter.widgets.ProxyWidget.child}
204
  final Widget? child;
205

206 207 208
  /// 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.
209
  final MaterialType type;
210

211
  /// {@template flutter.material.material.elevation}
212 213
  /// The z-coordinate at which to place this material relative to its parent.
  ///
214 215
  /// This controls the size of the shadow below the material and the opacity
  /// of the elevation overlay color if it is applied.
216
  ///
217
  /// If this is non-zero, the contents of the material are clipped, because the
218 219
  /// widget conceptually defines an independent printed piece of material.
  ///
220
  /// Defaults to 0. Changing this value will cause the shadow and the elevation
221
  /// overlay or surface tint to animate over [Material.animationDuration].
222 223
  ///
  /// The value is non-negative.
224 225 226
  ///
  /// See also:
  ///
227 228
  ///  * [ThemeData.useMaterial3] which defines whether a surface tint or
  ///    elevation overlay is used to indicate elevation.
229 230
  ///  * [ThemeData.applyElevationOverlayColor] which controls the whether
  ///    an overlay color will be applied to indicate elevation.
231
  ///  * [Material.color] which may have an elevation overlay applied.
232 233 234
  ///  * [Material.shadowColor] which will be used for the color of a drop shadow.
  ///  * [Material.surfaceTintColor] which will be used as the overlay tint to
  ///    show elevation.
235
  /// {@endtemplate}
236
  final double elevation;
237

238
  /// The color to paint the material.
239 240 241
  ///
  /// Must be opaque. To create a transparent piece of material, use
  /// [MaterialType.transparency].
242
  ///
243 244 245 246 247 248 249 250
  /// If [ThemeData.useMaterial3] is true then an optional [surfaceTintColor]
  /// overlay may be applied on top of this color to indicate elevation.
  ///
  /// If [ThemeData.useMaterial3] is false and [ThemeData.applyElevationOverlayColor]
  /// is true and [ThemeData.brightness] is [Brightness.dark] then a
  /// semi-transparent overlay color will be composited on top of this
  /// color to indicate the elevation. This is no longer needed for Material
  /// Design 3, which uses [surfaceTintColor].
251
  ///
252
  /// By default, the color is derived from the [type] of material.
253
  final Color? color;
254

255 256
  /// The color to paint the shadow below the material.
  ///
257 258 259
  /// If null and [ThemeData.useMaterial3] is true then [ThemeData]'s
  /// [ColorScheme.shadow] will be used. If [ThemeData.useMaterial3] is false
  /// then [ThemeData.shadowColor] will be used.
260
  ///
261 262
  /// To remove the drop shadow when [elevation] is greater than 0, set
  /// [shadowColor] to [Colors.transparent].
263 264
  ///
  /// See also:
265 266
  ///  * [ThemeData.useMaterial3], which determines the default value for this
  ///    property if it is null.
267 268
  ///  * [ThemeData.applyElevationOverlayColor], which turns elevation overlay
  /// on or off for dark themes.
269
  final Color? shadowColor;
270

271 272 273 274 275 276 277 278 279 280
  /// The color of the surface tint overlay applied to the material color
  /// to indicate elevation.
  ///
  /// Material Design 3 introduced a new way for some components to indicate
  /// their elevation by using a surface tint color overlay on top of the
  /// base material [color]. This overlay is painted with an opacity that is
  /// related to the [elevation] of the material.
  ///
  /// If [ThemeData.useMaterial3] is false, then this property is not used.
  ///
281 282 283
  /// If [ThemeData.useMaterial3] is true and [surfaceTintColor] is not null and
  /// not [Colors.transparent], then it will be used to overlay the base [color]
  /// with an opacity based on the [elevation].
284 285 286 287 288 289 290 291 292 293 294 295
  ///
  /// Otherwise, no surface tint will be applied.
  ///
  /// See also:
  ///
  ///   * [ThemeData.useMaterial3], which turns this feature on.
  ///   * [ElevationOverlay.applySurfaceTint], which is used to implement the
  ///     tint.
  ///   * https://m3.material.io/styles/color/the-color-system/color-roles
  ///     which specifies how the overlay is applied.
  final Color? surfaceTintColor;

296
  /// The typographical style to use for text within this material.
297
  final TextStyle? textStyle;
298

299 300 301 302 303 304 305
  /// 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.
306
  final ShapeBorder? shape;
307

308 309 310 311 312 313
  /// 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;

314
  /// {@template flutter.material.Material.clipBehavior}
315 316 317 318 319
  /// 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}
320 321
  ///
  /// Defaults to [Clip.none], and must not be null.
322 323
  final Clip clipBehavior;

324
  /// Defines the duration of animated changes for [shape], [elevation],
325
  /// [shadowColor], [surfaceTintColor] and the elevation overlay if it is applied.
326 327 328 329
  ///
  /// The default value is [kThemeChangeDuration].
  final Duration animationDuration;

330 331 332
  /// If non-null, the corners of this box are rounded by this
  /// [BorderRadiusGeometry] value.
  ///
333 334 335
  /// Otherwise, the corners specified for the current [type] of material are
  /// used.
  ///
336 337
  /// If [shape] is non null then the border radius is ignored.
  ///
338
  /// Must be null if [type] is [MaterialType.circle].
339
  final BorderRadiusGeometry? borderRadius;
340

341
  /// The ink controller from the closest instance of this class that
342
  /// encloses the given context within the closest [LookupBoundary].
343 344 345 346
  ///
  /// Typical usage is as follows:
  ///
  /// ```dart
347
  /// MaterialInkController? inkController = Material.maybeOf(context);
348
  /// ```
349 350
  ///
  /// This method can be expensive (it walks the element tree).
351 352 353 354 355 356
  ///
  /// See also:
  ///
  /// * [Material.of], which is similar to this method, but asserts if
  ///   no [Material] ancestor is found.
  static MaterialInkController? maybeOf(BuildContext context) {
357
    return LookupBoundary.findAncestorRenderObjectOfType<_RenderInkFeatures>(context);
358 359
  }

360
  /// The ink controller from the closest instance of [Material] that encloses
361
  /// the given context within the closest [LookupBoundary].
362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381
  ///
  /// If no [Material] widget ancestor can be found then this method will assert
  /// in debug mode, and throw an exception in release mode.
  ///
  /// Typical usage is as follows:
  ///
  /// ```dart
  /// MaterialInkController inkController = Material.of(context);
  /// ```
  ///
  /// This method can be expensive (it walks the element tree).
  ///
  /// See also:
  ///
  /// * [Material.maybeOf], which is similar to this method, but returns null if
  ///   no [Material] ancestor is found.
  static MaterialInkController of(BuildContext context) {
    final MaterialInkController? controller = maybeOf(context);
    assert(() {
      if (controller == null) {
382 383 384 385 386 387 388 389 390 391
        if (LookupBoundary.debugIsHidingAncestorRenderObjectOfType<_RenderInkFeatures>(context)) {
          throw FlutterError(
            'Material.of() was called with a context that does not have access to a Material widget.\n'
            'The context provided to Material.of() does have a Material widget ancestor, but it is '
            'hidden by a LookupBoundary. This can happen because you are using a widget that looks '
            'for a Material ancestor, but no such ancestor exists within the closest LookupBoundary.\n'
            'The context used was:\n'
            '  $context',
          );
        }
392 393 394 395 396 397 398 399 400 401 402 403 404 405
        throw FlutterError(
          'Material.of() was called with a context that does not contain a Material widget.\n'
          'No Material widget ancestor could be found starting from the context that was passed to '
          'Material.of(). This can happen because you are using a widget that looks for a Material '
          'ancestor, but no such ancestor exists.\n'
          'The context used was:\n'
          '  $context',
        );
      }
      return true;
    }());
    return controller!;
  }

406
  @override
407
  State<Material> createState() => _MaterialState();
408

409
  @override
410 411
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
412 413
    properties.add(EnumProperty<MaterialType>('type', type));
    properties.add(DoubleProperty('elevation', elevation, defaultValue: 0.0));
414
    properties.add(ColorProperty('color', color, defaultValue: null));
415
    properties.add(ColorProperty('shadowColor', shadowColor, defaultValue: null));
416
    properties.add(ColorProperty('surfaceTintColor', surfaceTintColor, defaultValue: null));
417
    textStyle?.debugFillProperties(properties, prefix: 'textStyle.');
418
    properties.add(DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: null));
419
    properties.add(DiagnosticsProperty<bool>('borderOnForeground', borderOnForeground, defaultValue: true));
420
    properties.add(DiagnosticsProperty<BorderRadiusGeometry>('borderRadius', borderRadius, defaultValue: null));
421
  }
422 423 424

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

427
class _MaterialState extends State<Material> with TickerProviderStateMixin {
428
  final GlobalKey _inkFeatureRenderer = GlobalKey(debugLabel: 'ink renderer');
429

430
  Color? _getBackgroundColor(BuildContext context) {
431
    final ThemeData theme = Theme.of(context);
432
    Color? color = widget.color;
433 434 435 436 437 438 439 440
    if (color == null) {
      switch (widget.type) {
        case MaterialType.canvas:
          color = theme.canvasColor;
          break;
        case MaterialType.card:
          color = theme.cardColor;
          break;
441 442 443
        case MaterialType.button:
        case MaterialType.circle:
        case MaterialType.transparency:
444 445
          break;
      }
446
    }
447
    return color;
448 449
  }

450
  @override
451
  Widget build(BuildContext context) {
452
    final ThemeData theme = Theme.of(context);
453
    final Color? backgroundColor = _getBackgroundColor(context);
454
    final Color modelShadowColor = widget.shadowColor ?? (theme.useMaterial3 ? theme.colorScheme.shadow : theme.shadowColor);
455
    // If no shadow color is specified, use 0 for elevation in the model so a drop shadow won't be painted.
456
    final double modelElevation = widget.elevation;
457 458 459 460 461
    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 '
462
      'MaterialType.canvas)',
463
    );
464
    Widget? contents = widget.child;
465
    if (contents != null) {
466
      contents = AnimatedDefaultTextStyle(
467
        style: widget.textStyle ?? Theme.of(context).textTheme.bodyMedium!,
468
        duration: widget.animationDuration,
469
        child: contents,
470 471
      );
    }
472
    contents = NotificationListener<LayoutChangedNotification>(
473
      onNotification: (LayoutChangedNotification notification) {
474
        final _RenderInkFeatures renderer = _inkFeatureRenderer.currentContext!.findRenderObject()! as _RenderInkFeatures;
475
        renderer._didChangeLayout();
476
        return false;
477
      },
478
      child: _InkFeatures(
479
        key: _inkFeatureRenderer,
480
        absorbHitTest: widget.type != MaterialType.transparency,
481
        color: backgroundColor,
482
        vsync: this,
483
        child: contents,
484
      ),
485
    );
486

Josh Soref's avatar
Josh Soref committed
487
    // PhysicalModel has a temporary workaround for a performance issue that
488 489
    // 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
490
    // Until the saveLayer performance issue is resolved, we're keeping this
491 492
    // 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
493
    // specified rectangles (e.g shape RoundedRectangleBorder with radius 0, but
494 495 496
    // 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) {
497 498 499 500
      final Color color = Theme.of(context).useMaterial3
        ? ElevationOverlay.applySurfaceTint(backgroundColor!, widget.surfaceTintColor, widget.elevation)
        : ElevationOverlay.applyOverlay(context, backgroundColor!, widget.elevation);

501
      return AnimatedPhysicalModel(
502
        curve: Curves.fastOutSlowIn,
503
        duration: widget.animationDuration,
504
        shape: BoxShape.rectangle,
505
        clipBehavior: widget.clipBehavior,
506
        elevation: modelElevation,
507
        color: color,
508
        shadowColor: modelShadowColor,
509 510 511 512 513
        animateColor: false,
        child: contents,
      );
    }

514 515
    final ShapeBorder shape = _getShape();

516 517 518 519 520 521 522 523
    if (widget.type == MaterialType.transparency) {
      return _transparentInterior(
        context: context,
        shape: shape,
        clipBehavior: widget.clipBehavior,
        contents: contents,
      );
    }
524

525
    return _MaterialInterior(
526
      curve: Curves.fastOutSlowIn,
527
      duration: widget.animationDuration,
528
      shape: shape,
529
      borderOnForeground: widget.borderOnForeground,
530
      clipBehavior: widget.clipBehavior,
531
      elevation: widget.elevation,
532
      color: backgroundColor!,
533
      shadowColor: modelShadowColor,
534
      surfaceTintColor: widget.surfaceTintColor,
535 536 537 538
      child: contents,
    );
  }

539
  static Widget _transparentInterior({
540 541 542 543
    required BuildContext context,
    required ShapeBorder shape,
    required Clip clipBehavior,
    required Widget contents,
544
  }) {
545
    final _ShapeBorderPaint child = _ShapeBorderPaint(
546
      shape: shape,
547
      child: contents,
548
    );
549
    return ClipPath(
550 551
      clipper: ShapeBorderClipper(
        shape: shape,
552
        textDirection: Directionality.maybeOf(context),
553
      ),
554
      clipBehavior: clipBehavior,
555
      child: child,
556 557 558 559 560 561 562 563 564 565 566
    );
  }

  // 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() {
567
    if (widget.shape != null) {
568
      return widget.shape!;
569 570
    }
    if (widget.borderRadius != null) {
571
      return RoundedRectangleBorder(borderRadius: widget.borderRadius!);
572
    }
573 574 575
    switch (widget.type) {
      case MaterialType.canvas:
      case MaterialType.transparency:
576
        return const RoundedRectangleBorder();
577 578 579

      case MaterialType.card:
      case MaterialType.button:
580
        return RoundedRectangleBorder(
581
          borderRadius: widget.borderRadius ?? kMaterialEdges[widget.type]!,
582
        );
583

584 585 586
      case MaterialType.circle:
        return const CircleBorder();
    }
587 588 589
  }
}

590
class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController {
591
  _RenderInkFeatures({
592 593 594
    RenderBox? child,
    required this.vsync,
    required this.absorbHitTest,
595
    this.color,
596
  }) : super(child);
597 598 599 600

  // 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.
601
  @override
602
  final TickerProvider vsync;
603 604 605 606

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

610 611
  bool absorbHitTest;

612 613
  @visibleForTesting
  List<InkFeature>? get debugInkFeatures {
614
    if (kDebugMode) {
615
      return _inkFeatures;
616
    }
617 618
    return null;
  }
619
  List<InkFeature>? _inkFeatures;
620

621
  @override
622 623
  void addInkFeature(InkFeature feature) {
    assert(!feature._debugDisposed);
624
    assert(feature._controller == this);
625
    _inkFeatures ??= <InkFeature>[];
626 627
    assert(!_inkFeatures!.contains(feature));
    _inkFeatures!.add(feature);
628 629 630 631
    markNeedsPaint();
  }

  void _removeFeature(InkFeature feature) {
632
    assert(_inkFeatures != null);
633
    _inkFeatures!.remove(feature);
634 635 636
    markNeedsPaint();
  }

637
  void _didChangeLayout() {
638
    if (_inkFeatures != null && _inkFeatures!.isNotEmpty) {
639
      markNeedsPaint();
640
    }
641 642
  }

643
  @override
644
  bool hitTestSelf(Offset position) => absorbHitTest;
645

646
  @override
647
  void paint(PaintingContext context, Offset offset) {
648
    if (_inkFeatures != null && _inkFeatures!.isNotEmpty) {
649 650 651
      final Canvas canvas = context.canvas;
      canvas.save();
      canvas.translate(offset.dx, offset.dy);
652
      canvas.clipRect(Offset.zero & size);
653
      for (final InkFeature inkFeature in _inkFeatures!) {
654
        inkFeature._paint(canvas);
655
      }
656 657 658 659 660 661
      canvas.restore();
    }
    super.paint(context, offset);
  }
}

662
class _InkFeatures extends SingleChildRenderObjectWidget {
663
  const _InkFeatures({
664
    super.key,
665
    this.color,
666 667
    required this.vsync,
    required this.absorbHitTest,
668 669
    super.child,
  });
670 671 672

  // 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.
673

674
  final Color? color;
675

676 677
  final TickerProvider vsync;

678 679
  final bool absorbHitTest;

680
  @override
681
  _RenderInkFeatures createRenderObject(BuildContext context) {
682
    return _RenderInkFeatures(
683
      color: color,
684
      absorbHitTest: absorbHitTest,
685
      vsync: vsync,
686 687
    );
  }
688

689
  @override
690
  void updateRenderObject(BuildContext context, _RenderInkFeatures renderObject) {
691 692
    renderObject..color = color
                ..absorbHitTest = absorbHitTest;
693
    assert(vsync == renderObject.vsync);
694 695 696
  }
}

697 698
/// A visual reaction on a piece of [Material].
///
699 700 701
/// To add an ink feature to a piece of [Material], obtain the
/// [MaterialInkController] via [Material.of] and call
/// [MaterialInkController.addInkFeature].
702
abstract class InkFeature {
703
  /// Initializes fields for subclasses.
704
  InkFeature({
705 706
    required MaterialInkController controller,
    required this.referenceBox,
707
    this.onRemoved,
708
  }) : _controller = controller as _RenderInkFeatures;
709

710 711 712 713
  /// The [MaterialInkController] associated with this [InkFeature].
  ///
  /// Typically used by subclasses to call
  /// [MaterialInkController.markNeedsPaint] when they need to repaint.
714
  MaterialInkController get controller => _controller;
715
  final _RenderInkFeatures _controller;
716 717

  /// The render box whose visual position defines the frame of reference for this ink feature.
718
  final RenderBox referenceBox;
719 720

  /// Called when the ink feature is no longer visible on the material.
721
  final VoidCallback? onRemoved;
722 723 724

  bool _debugDisposed = false;

725
  /// Free up the resources associated with this ink feature.
726
  @mustCallSuper
727 728
  void dispose() {
    assert(!_debugDisposed);
729 730 731 732
    assert(() {
      _debugDisposed = true;
      return true;
    }());
733
    _controller._removeFeature(this);
734
    onRemoved?.call();
735 736 737 738 739 740
  }

  void _paint(Canvas canvas) {
    assert(referenceBox.attached);
    assert(!_debugDisposed);
    // find the chain of renderers from us to the feature's referenceBox
741 742
    final List<RenderObject> descendants = <RenderObject>[referenceBox];
    RenderObject node = referenceBox;
743
    while (node != _controller) {
744
      final RenderObject childNode = node;
745
      node = node.parent! as RenderObject;
746 747 748 749 750 751 752 753
      if (!node.paintsChild(childNode)) {
        // Some node between the reference box and this would skip painting on
        // the reference box, so bail out early and avoid unnecessary painting.
        // Some cases where this can happen are the reference box being
        // offstage, in a fully transparent opacity node, or in a keep alive
        // bucket.
        return;
      }
754
      descendants.add(node);
755 756
    }
    // determine the transform that gets our coordinate system to be like theirs
757
    final Matrix4 transform = Matrix4.identity();
758
    assert(descendants.length >= 2);
759
    for (int index = descendants.length - 1; index > 0; index -= 1) {
760
      descendants[index].applyPaintTransform(descendants[index - 1], transform);
761
    }
762 763 764
    paintFeature(canvas, transform);
  }

765 766 767
  /// Override this method to paint the ink feature.
  ///
  /// The transform argument gives the coordinate conversion from the coordinate
768
  /// system of the canvas to the coordinate system of the [referenceBox].
769
  @protected
770 771
  void paintFeature(Canvas canvas, Matrix4 transform);

772
  @override
773
  String toString() => describeIdentity(this);
774
}
775 776 777 778

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

  /// Returns the value this tween has at the given animation clock value.
  @override
788
  ShapeBorder? lerp(double t) {
789 790 791 792 793 794 795 796
    return ShapeBorder.lerp(begin, end, t);
  }
}

/// The interior of non-transparent material.
///
/// Animates [elevation], [shadowColor], and [shape].
class _MaterialInterior extends ImplicitlyAnimatedWidget {
797 798 799 800 801
  /// Creates a const instance of [_MaterialInterior].
  ///
  /// The [child], [shape], [clipBehavior], [color], and [shadowColor] arguments
  /// must not be null. The [elevation] must be specified and greater than or
  /// equal to zero.
802
  const _MaterialInterior({
803 804
    required this.child,
    required this.shape,
805
    this.borderOnForeground = true,
806
    this.clipBehavior = Clip.none,
807 808 809
    required this.elevation,
    required this.color,
    required this.shadowColor,
810
    required this.surfaceTintColor,
811 812
    super.curve,
    required super.duration,
813
  }) : assert(elevation >= 0.0);
814 815 816

  /// The widget below this widget in the tree.
  ///
817
  /// {@macro flutter.widgets.ProxyWidget.child}
818 819 820 821 822 823 824 825
  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;

826 827 828 829 830 831
  /// 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;

832
  /// {@macro flutter.material.Material.clipBehavior}
833 834
  ///
  /// Defaults to [Clip.none], and must not be null.
835 836
  final Clip clipBehavior;

837 838 839 840
  /// The target z-coordinate at which to place this physical object relative
  /// to its parent.
  ///
  /// The value is non-negative.
841 842 843 844 845 846
  final double elevation;

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

  /// The target shadow color.
847
  final Color? shadowColor;
848

849 850 851
  /// The target surface tint color.
  final Color? surfaceTintColor;

852
  @override
853
  _MaterialInteriorState createState() => _MaterialInteriorState();
854 855 856 857

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder description) {
    super.debugFillProperties(description);
858 859
    description.add(DiagnosticsProperty<ShapeBorder>('shape', shape));
    description.add(DoubleProperty('elevation', elevation));
860 861
    description.add(ColorProperty('color', color));
    description.add(ColorProperty('shadowColor', shadowColor));
862 863 864 865
  }
}

class _MaterialInteriorState extends AnimatedWidgetBaseState<_MaterialInterior> {
866
  Tween<double>? _elevation;
867
  ColorTween? _surfaceTintColor;
868 869
  ColorTween? _shadowColor;
  ShapeBorderTween? _border;
870 871 872

  @override
  void forEachTween(TweenVisitor<dynamic> visitor) {
873 874 875 876
    _elevation = visitor(
      _elevation,
      widget.elevation,
      (dynamic value) => Tween<double>(begin: value as double),
877
    ) as Tween<double>?;
878 879 880 881 882 883 884
    _shadowColor =  widget.shadowColor != null
      ? visitor(
          _shadowColor,
          widget.shadowColor,
          (dynamic value) => ColorTween(begin: value as Color),
        ) as ColorTween?
      : null;
885 886 887 888 889 890 891
    _surfaceTintColor = widget.surfaceTintColor != null
      ? visitor(
          _surfaceTintColor,
          widget.surfaceTintColor,
              (dynamic value) => ColorTween(begin: value as Color),
        ) as ColorTween?
      : null;
892 893 894 895
    _border = visitor(
      _border,
      widget.shape,
      (dynamic value) => ShapeBorderTween(begin: value as ShapeBorder),
896
    ) as ShapeBorderTween?;
897 898 899 900
  }

  @override
  Widget build(BuildContext context) {
901 902
    final ShapeBorder shape = _border!.evaluate(animation)!;
    final double elevation = _elevation!.evaluate(animation);
903 904 905
    final Color color = Theme.of(context).useMaterial3
      ? ElevationOverlay.applySurfaceTint(widget.color, _surfaceTintColor?.evaluate(animation), elevation)
      : ElevationOverlay.applyOverlay(context, widget.color, elevation);
906 907 908
    // If no shadow color is specified, use 0 for elevation in the model so a drop shadow won't be painted.
    final double modelElevation = widget.shadowColor != null ? elevation : 0;
    final Color shadowColor = _shadowColor?.evaluate(animation) ?? const Color(0x00000000);
909 910
    return PhysicalShape(
      clipper: ShapeBorderClipper(
911
        shape: shape,
912
        textDirection: Directionality.maybeOf(context),
913
      ),
914
      clipBehavior: widget.clipBehavior,
915
      elevation: modelElevation,
916
      color: color,
917
      shadowColor: shadowColor,
918 919 920 921 922
      child: _ShapeBorderPaint(
        shape: shape,
        borderOnForeground: widget.borderOnForeground,
        child: widget.child,
      ),
923 924 925
    );
  }
}
926 927 928

class _ShapeBorderPaint extends StatelessWidget {
  const _ShapeBorderPaint({
929 930
    required this.child,
    required this.shape,
931
    this.borderOnForeground = true,
932 933 934 935
  });

  final Widget child;
  final ShapeBorder shape;
936
  final bool borderOnForeground;
937 938 939

  @override
  Widget build(BuildContext context) {
940
    return CustomPaint(
941 942
      painter: borderOnForeground ? null : _ShapeBorderPainter(shape, Directionality.maybeOf(context)),
      foregroundPainter: borderOnForeground ? _ShapeBorderPainter(shape, Directionality.maybeOf(context)) : null,
943
      child: child,
944 945 946 947 948 949 950
    );
  }
}

class _ShapeBorderPainter extends CustomPainter {
  _ShapeBorderPainter(this.border, this.textDirection);
  final ShapeBorder border;
951
  final TextDirection? textDirection;
952 953 954 955 956 957 958 959 960 961 962

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

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