1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/// @docImport 'card.dart';
6/// @docImport 'color_scheme.dart';
7/// @docImport 'colors.dart';
8/// @docImport 'ink_decoration.dart';
9/// @docImport 'ink_highlight.dart';
10/// @docImport 'ink_splash.dart';
11/// @docImport 'ink_well.dart';
12/// @docImport 'list_tile.dart';
13/// @docImport 'material_button.dart';
14/// @docImport 'mergeable_material.dart';
15library;
16
17import 'package:flutter/foundation.dart';
18import 'package:flutter/rendering.dart';
19import 'package:flutter/widgets.dart';
20
21import 'constants.dart';
22import 'elevation_overlay.dart';
23import 'theme.dart';
24
25// Examples can assume:
26// late BuildContext context;
27
28/// Signature for the callback used by ink effects to obtain the rectangle for the effect.
29///
30/// Used by [InkHighlight] and [InkSplash], for example.
31typedef RectCallback = Rect Function();
32
33/// The various kinds of material in Material Design. Used to
34/// configure the default behavior of [Material] widgets.
35///
36/// See also:
37///
38/// * [Material], in particular [Material.type].
39/// * [kMaterialEdges]
40enum MaterialType {
41 /// Rectangle using default theme canvas color.
42 canvas,
43
44 /// Rounded edges, card theme color.
45 card,
46
47 /// A circle, no color by default (used for floating action buttons).
48 circle,
49
50 /// Rounded edges, no color by default (used for [MaterialButton] buttons).
51 button,
52
53 /// A transparent piece of material that draws ink splashes and highlights.
54 ///
55 /// While the material metaphor describes child widgets as printed on the
56 /// material itself and do not hide ink effects, in practice the [Material]
57 /// widget draws child widgets on top of the ink effects.
58 /// A [Material] with type transparency can be placed on top of opaque widgets
59 /// to show ink effects on top of them.
60 ///
61 /// Prefer using the [Ink] widget for showing ink effects on top of opaque
62 /// widgets.
63 transparency,
64}
65
66/// The border radii used by the various kinds of material in Material Design.
67///
68/// See also:
69///
70/// * [MaterialType]
71/// * [Material]
72const Map<MaterialType, BorderRadius?> kMaterialEdges = <MaterialType, BorderRadius?>{
73 MaterialType.canvas: null,
74 MaterialType.card: BorderRadius.all(Radius.circular(2.0)),
75 MaterialType.circle: null,
76 MaterialType.button: BorderRadius.all(Radius.circular(2.0)),
77 MaterialType.transparency: null,
78};
79
80/// An interface for creating [InkSplash]s and [InkHighlight]s on a [Material].
81///
82/// Typically obtained via [Material.of].
83abstract class MaterialInkController {
84 /// The color of the material.
85 Color? get color;
86
87 /// The ticker provider used by the controller.
88 ///
89 /// Ink features that are added to this controller with [addInkFeature] should
90 /// use this vsync to drive their animations.
91 TickerProvider get vsync;
92
93 /// Add an [InkFeature], such as an [InkSplash] or an [InkHighlight].
94 ///
95 /// The ink feature will paint as part of this controller.
96 void addInkFeature(InkFeature feature);
97
98 /// Notifies the controller that one of its ink features needs to repaint.
99 void markNeedsPaint();
100}
101
102/// A piece of material.
103///
104/// The Material widget is responsible for:
105///
106/// 1. Clipping: If [clipBehavior] is not [Clip.none], Material clips its widget
107/// sub-tree to the shape specified by [shape], [type], and [borderRadius].
108/// By default, [clipBehavior] is [Clip.none] for performance considerations.
109/// See [Ink] for an example of how this affects clipping [Ink] widgets.
110/// 2. Elevation: Material elevates its widget sub-tree on the Z axis by
111/// [elevation] pixels, and draws the appropriate shadow.
112/// 3. Ink effects: Material shows ink effects implemented by [InkFeature]s
113/// like [InkSplash] and [InkHighlight] below its children.
114///
115/// ## The Material Metaphor
116///
117/// Material is the central metaphor in Material Design. Each piece of material
118/// exists at a given elevation, which influences how that piece of material
119/// visually relates to other pieces of material and how that material casts
120/// shadows.
121///
122/// Most user interface elements are either conceptually printed on a piece of
123/// material or themselves made of material. Material reacts to user input using
124/// [InkSplash] and [InkHighlight] effects. To trigger a reaction on the
125/// material, use a [MaterialInkController] obtained via [Material.of].
126///
127/// In general, the features of a [Material] should not change over time (e.g. a
128/// [Material] should not change its [color], [shadowColor] or [type]).
129/// Changes to [elevation], [shadowColor] and [surfaceTintColor] are animated
130/// for [animationDuration]. Changes to [shape] are animated if [type] is
131/// not [MaterialType.transparency] and [ShapeBorder.lerp] between the previous
132/// and next [shape] values is supported. Shape changes are also animated
133/// for [animationDuration].
134///
135/// ## Shape
136///
137/// The shape for material is determined by [shape], [type], and [borderRadius].
138///
139/// - If [shape] is non null, it determines the shape.
140/// - If [shape] is null and [borderRadius] is non null, the shape is a
141/// rounded rectangle, with corners specified by [borderRadius].
142/// - If [shape] and [borderRadius] are null, [type] determines the
143/// shape as follows:
144/// - [MaterialType.canvas]: the default material shape is a rectangle.
145/// - [MaterialType.card]: the default material shape is a rectangle with
146/// rounded edges. The edge radii is specified by [kMaterialEdges].
147/// - [MaterialType.circle]: the default material shape is a circle.
148/// - [MaterialType.button]: the default material shape is a rectangle with
149/// rounded edges. The edge radii is specified by [kMaterialEdges].
150/// - [MaterialType.transparency]: the default material shape is a rectangle.
151///
152/// ## Border
153///
154/// If [shape] is not null, then its border will also be painted (if any).
155///
156/// ## Layout change notifications
157///
158/// If the layout changes (e.g. because there's a list on the material, and it's
159/// been scrolled), a [LayoutChangedNotification] must be dispatched at the
160/// relevant subtree. This in particular means that transitions (e.g.
161/// [SlideTransition]) should not be placed inside [Material] widgets so as to
162/// move subtrees that contain [InkResponse]s, [InkWell]s, [Ink]s, or other
163/// widgets that use the [InkFeature] mechanism. Otherwise, in-progress ink
164/// features (e.g., ink splashes and ink highlights) won't move to account for
165/// the new layout.
166///
167/// ## Painting over the material
168///
169/// Material widgets will often trigger reactions on their nearest material
170/// ancestor. For example, [ListTile.hoverColor] triggers a reaction on the
171/// tile's material when a pointer is hovering over it. These reactions will be
172/// obscured if any widget in between them and the material paints in such a
173/// way as to obscure the material (such as setting a [BoxDecoration.color] on
174/// a [DecoratedBox]). To avoid this behavior, use [InkDecoration] to decorate
175/// the material itself.
176///
177/// See also:
178///
179/// * [MergeableMaterial], a piece of material that can split and re-merge.
180/// * [Card], a wrapper for a [Material] of [type] [MaterialType.card].
181/// * <https://material.io/design/>
182/// * <https://m3.material.io/styles/color/the-color-system/color-roles>
183class Material extends StatefulWidget {
184 /// Creates a piece of material.
185 ///
186 /// The [elevation] must be non-negative.
187 ///
188 /// If a [shape] is specified, then the [borderRadius] property must be
189 /// null and the [type] property must not be [MaterialType.circle]. If the
190 /// [borderRadius] is specified, then the [type] property must not be
191 /// [MaterialType.circle]. In both cases, these restrictions are intended to
192 /// catch likely errors.
193 const Material({
194 super.key,
195 this.type = MaterialType.canvas,
196 this.elevation = 0.0,
197 this.color,
198 this.shadowColor,
199 this.surfaceTintColor,
200 this.textStyle,
201 this.borderRadius,
202 this.shape,
203 this.borderOnForeground = true,
204 this.clipBehavior = Clip.none,
205 this.animationDuration = kThemeChangeDuration,
206 this.child,
207 }) : assert(elevation >= 0.0),
208 assert(!(shape != null && borderRadius != null)),
209 assert(!(identical(type, MaterialType.circle) && (borderRadius != null || shape != null)));
210
211 /// The widget below this widget in the tree.
212 ///
213 /// {@macro flutter.widgets.ProxyWidget.child}
214 final Widget? child;
215
216 /// The kind of material to show (e.g., card or canvas). This
217 /// affects the shape of the widget, the roundness of its corners if
218 /// the shape is rectangular, and the default color.
219 final MaterialType type;
220
221 /// {@template flutter.material.material.elevation}
222 /// The z-coordinate at which to place this material relative to its parent.
223 ///
224 /// This controls the size of the shadow below the material and the opacity
225 /// of the elevation overlay color if it is applied.
226 ///
227 /// If this is non-zero, the contents of the material are clipped, because the
228 /// widget conceptually defines an independent printed piece of material.
229 ///
230 /// Defaults to 0. Changing this value will cause the shadow and the elevation
231 /// overlay or surface tint to animate over [Material.animationDuration].
232 ///
233 /// The value is non-negative.
234 ///
235 /// See also:
236 ///
237 /// * [ThemeData.useMaterial3] which defines whether a surface tint or
238 /// elevation overlay is used to indicate elevation.
239 /// * [ThemeData.applyElevationOverlayColor] which controls the whether
240 /// an overlay color will be applied to indicate elevation.
241 /// * [Material.color] which may have an elevation overlay applied.
242 /// * [Material.shadowColor] which will be used for the color of a drop shadow.
243 /// * [Material.surfaceTintColor] which will be used as the overlay tint to
244 /// show elevation.
245 /// {@endtemplate}
246 final double elevation;
247
248 /// The color to paint the material.
249 ///
250 /// Must be opaque. To create a transparent piece of material, use
251 /// [MaterialType.transparency].
252 ///
253 /// If [ThemeData.useMaterial3] is true then an optional [surfaceTintColor]
254 /// overlay may be applied on top of this color to indicate elevation.
255 ///
256 /// If [ThemeData.useMaterial3] is false and [ThemeData.applyElevationOverlayColor]
257 /// is true and [ThemeData.brightness] is [Brightness.dark] then a
258 /// semi-transparent overlay color will be composited on top of this
259 /// color to indicate the elevation. This is no longer needed for Material
260 /// Design 3, which uses [surfaceTintColor].
261 ///
262 /// By default, the color is derived from the [type] of material.
263 final Color? color;
264
265 /// The color to paint the shadow below the material.
266 ///
267 /// {@template flutter.material.material.shadowColor}
268 /// If null and [ThemeData.useMaterial3] is true then [ThemeData]'s
269 /// [ColorScheme.shadow] will be used. If [ThemeData.useMaterial3] is false
270 /// then [ThemeData.shadowColor] will be used.
271 ///
272 /// To remove the drop shadow when [elevation] is greater than 0, set
273 /// [shadowColor] to [Colors.transparent].
274 ///
275 /// See also:
276 /// * [ThemeData.useMaterial3], which determines the default value for this
277 /// property if it is null.
278 /// * [ThemeData.applyElevationOverlayColor], which turns elevation overlay
279 /// on or off for dark themes.
280 /// {@endtemplate}
281 final Color? shadowColor;
282
283 /// The color of the surface tint overlay applied to the material color
284 /// to indicate elevation.
285 ///
286 /// {@template flutter.material.material.surfaceTintColor}
287 /// Material Design 3 introduced a new way for some components to indicate
288 /// their elevation by using a surface tint color overlay on top of the
289 /// base material [color]. This overlay is painted with an opacity that is
290 /// related to the [elevation] of the material.
291 ///
292 /// If [ThemeData.useMaterial3] is false, then this property is not used.
293 ///
294 /// If [ThemeData.useMaterial3] is true and [surfaceTintColor] is not null and
295 /// not [Colors.transparent], then it will be used to overlay the base [color]
296 /// with an opacity based on the [elevation].
297 ///
298 /// Otherwise, no surface tint will be applied.
299 ///
300 /// See also:
301 ///
302 /// * [ThemeData.useMaterial3], which turns this feature on.
303 /// * [ElevationOverlay.applySurfaceTint], which is used to implement the
304 /// tint.
305 /// * https://m3.material.io/styles/color/the-color-system/color-roles
306 /// which specifies how the overlay is applied.
307 /// {@endtemplate}
308 final Color? surfaceTintColor;
309
310 /// The typographical style to use for text within this material.
311 final TextStyle? textStyle;
312
313 /// Defines the material's shape as well its shadow.
314 ///
315 /// {@template flutter.material.material.shape}
316 /// If shape is non null, the [borderRadius] is ignored and the material's
317 /// clip boundary and shadow are defined by the shape.
318 ///
319 /// A shadow is only displayed if the [elevation] is greater than
320 /// zero.
321 /// {@endtemplate}
322 final ShapeBorder? shape;
323
324 /// Whether to paint the [shape] border in front of the [child].
325 ///
326 /// The default value is true.
327 /// If false, the border will be painted behind the [child].
328 final bool borderOnForeground;
329
330 /// {@template flutter.material.Material.clipBehavior}
331 /// The content will be clipped (or not) according to this option.
332 ///
333 /// See the enum [Clip] for details of all possible options and their common
334 /// use cases.
335 /// {@endtemplate}
336 ///
337 /// Defaults to [Clip.none].
338 final Clip clipBehavior;
339
340 /// Defines the duration of animated changes for [shape], [elevation],
341 /// [shadowColor], [surfaceTintColor] and the elevation overlay if it is applied.
342 ///
343 /// The default value is [kThemeChangeDuration].
344 final Duration animationDuration;
345
346 /// If non-null, the corners of this box are rounded by this
347 /// [BorderRadiusGeometry] value.
348 ///
349 /// Otherwise, the corners specified for the current [type] of material are
350 /// used.
351 ///
352 /// If [shape] is non null then the border radius is ignored.
353 ///
354 /// Must be null if [type] is [MaterialType.circle].
355 final BorderRadiusGeometry? borderRadius;
356
357 /// The ink controller from the closest instance of this class that
358 /// encloses the given context within the closest [LookupBoundary].
359 ///
360 /// Typical usage is as follows:
361 ///
362 /// ```dart
363 /// MaterialInkController? inkController = Material.maybeOf(context);
364 /// ```
365 ///
366 /// This method can be expensive (it walks the element tree).
367 ///
368 /// See also:
369 ///
370 /// * [Material.of], which is similar to this method, but asserts if
371 /// no [Material] ancestor is found.
372 static MaterialInkController? maybeOf(BuildContext context) {
373 return LookupBoundary.findAncestorRenderObjectOfType<_RenderInkFeatures>(context);
374 }
375
376 /// The ink controller from the closest instance of [Material] that encloses
377 /// the given context within the closest [LookupBoundary].
378 ///
379 /// If no [Material] widget ancestor can be found then this method will assert
380 /// in debug mode, and throw an exception in release mode.
381 ///
382 /// Typical usage is as follows:
383 ///
384 /// ```dart
385 /// MaterialInkController inkController = Material.of(context);
386 /// ```
387 ///
388 /// This method can be expensive (it walks the element tree).
389 ///
390 /// See also:
391 ///
392 /// * [Material.maybeOf], which is similar to this method, but returns null if
393 /// no [Material] ancestor is found.
394 static MaterialInkController of(BuildContext context) {
395 final MaterialInkController? controller = maybeOf(context);
396 assert(() {
397 if (controller == null) {
398 if (LookupBoundary.debugIsHidingAncestorRenderObjectOfType<_RenderInkFeatures>(context)) {
399 throw FlutterError(
400 'Material.of() was called with a context that does not have access to a Material widget.\n'
401 'The context provided to Material.of() does have a Material widget ancestor, but it is '
402 'hidden by a LookupBoundary. This can happen because you are using a widget that looks '
403 'for a Material ancestor, but no such ancestor exists within the closest LookupBoundary.\n'
404 'The context used was:\n'
405 ' $context',
406 );
407 }
408 throw FlutterError(
409 'Material.of() was called with a context that does not contain a Material widget.\n'
410 'No Material widget ancestor could be found starting from the context that was passed to '
411 'Material.of(). This can happen because you are using a widget that looks for a Material '
412 'ancestor, but no such ancestor exists.\n'
413 'The context used was:\n'
414 ' $context',
415 );
416 }
417 return true;
418 }());
419 return controller!;
420 }
421
422 @override
423 State<Material> createState() => _MaterialState();
424
425 @override
426 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
427 super.debugFillProperties(properties);
428 properties.add(EnumProperty<MaterialType>('type', type));
429 properties.add(DoubleProperty('elevation', elevation, defaultValue: 0.0));
430 properties.add(ColorProperty('color', color, defaultValue: null));
431 properties.add(ColorProperty('shadowColor', shadowColor, defaultValue: null));
432 properties.add(ColorProperty('surfaceTintColor', surfaceTintColor, defaultValue: null));
433 textStyle?.debugFillProperties(properties, prefix: 'textStyle.');
434 properties.add(DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: null));
435 properties.add(
436 DiagnosticsProperty<bool>('borderOnForeground', borderOnForeground, defaultValue: true),
437 );
438 properties.add(
439 DiagnosticsProperty<BorderRadiusGeometry>('borderRadius', borderRadius, defaultValue: null),
440 );
441 }
442
443 /// The default radius of an ink splash in logical pixels.
444 static const double defaultSplashRadius = 35.0;
445}
446
447class _MaterialState extends State<Material> with TickerProviderStateMixin {
448 final GlobalKey _inkFeatureRenderer = GlobalKey(debugLabel: 'ink renderer');
449
450 @override
451 Widget build(BuildContext context) {
452 final ThemeData theme = Theme.of(context);
453 final Color? backgroundColor =
454 widget.color ??
455 switch (widget.type) {
456 MaterialType.canvas => theme.canvasColor,
457 MaterialType.card => theme.cardColor,
458 MaterialType.button || MaterialType.circle || MaterialType.transparency => null,
459 };
460 final Color modelShadowColor =
461 widget.shadowColor ?? (theme.useMaterial3 ? theme.colorScheme.shadow : theme.shadowColor);
462 assert(
463 backgroundColor != null || widget.type == MaterialType.transparency,
464 'If Material type is not MaterialType.transparency, a color must '
465 'either be passed in through the `color` property, or be defined '
466 'in the theme (ex. canvasColor != null if type is set to '
467 'MaterialType.canvas)',
468 );
469
470 Widget? contents = widget.child;
471 if (contents != null) {
472 contents = AnimatedDefaultTextStyle(
473 style: widget.textStyle ?? Theme.of(context).textTheme.bodyMedium!,
474 duration: widget.animationDuration,
475 child: contents,
476 );
477 }
478 contents = NotificationListener<LayoutChangedNotification>(
479 onNotification: (LayoutChangedNotification notification) {
480 final _RenderInkFeatures renderer =
481 _inkFeatureRenderer.currentContext!.findRenderObject()! as _RenderInkFeatures;
482 renderer._didChangeLayout();
483 return false;
484 },
485 child: _InkFeatures(
486 key: _inkFeatureRenderer,
487 absorbHitTest: widget.type != MaterialType.transparency,
488 color: backgroundColor,
489 vsync: this,
490 child: contents,
491 ),
492 );
493
494 ShapeBorder? shape =
495 widget.borderRadius != null
496 ? RoundedRectangleBorder(borderRadius: widget.borderRadius!)
497 : widget.shape;
498
499 // PhysicalModel has a temporary workaround for a performance issue that
500 // speeds up rectangular non transparent material (the workaround is to
501 // skip the call to ui.Canvas.saveLayer if the border radius is 0).
502 // Until the saveLayer performance issue is resolved, we're keeping this
503 // special case here for canvas material type that is using the default
504 // shape (rectangle). We could go down this fast path for explicitly
505 // specified rectangles (e.g shape RoundedRectangleBorder with radius 0, but
506 // we choose not to as we want the change from the fast-path to the
507 // slow-path to be noticeable in the construction site of Material.
508 if (widget.type == MaterialType.canvas && shape == null) {
509 final Color color =
510 theme.useMaterial3
511 ? ElevationOverlay.applySurfaceTint(
512 backgroundColor!,
513 widget.surfaceTintColor,
514 widget.elevation,
515 )
516 : ElevationOverlay.applyOverlay(context, backgroundColor!, widget.elevation);
517
518 return AnimatedPhysicalModel(
519 curve: Curves.fastOutSlowIn,
520 duration: widget.animationDuration,
521 clipBehavior: widget.clipBehavior,
522 elevation: widget.elevation,
523 color: color,
524 shadowColor: modelShadowColor,
525 animateColor: false,
526 child: contents,
527 );
528 }
529
530 shape ??= switch (widget.type) {
531 MaterialType.circle => const CircleBorder(),
532 MaterialType.canvas || MaterialType.transparency => const RoundedRectangleBorder(),
533 MaterialType.card || MaterialType.button => const RoundedRectangleBorder(
534 borderRadius: BorderRadius.all(Radius.circular(2.0)),
535 ),
536 };
537
538 if (widget.type == MaterialType.transparency) {
539 return ClipPath(
540 clipper: ShapeBorderClipper(shape: shape, textDirection: Directionality.maybeOf(context)),
541 clipBehavior: widget.clipBehavior,
542 child: _ShapeBorderPaint(shape: shape, child: contents),
543 );
544 }
545
546 return _MaterialInterior(
547 curve: Curves.fastOutSlowIn,
548 duration: widget.animationDuration,
549 shape: shape,
550 borderOnForeground: widget.borderOnForeground,
551 clipBehavior: widget.clipBehavior,
552 elevation: widget.elevation,
553 color: backgroundColor!,
554 shadowColor: modelShadowColor,
555 surfaceTintColor: widget.surfaceTintColor,
556 child: contents,
557 );
558 }
559}
560
561class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController {
562 _RenderInkFeatures({
563 RenderBox? child,
564 required this.vsync,
565 required this.absorbHitTest,
566 this.color,
567 }) : super(child);
568
569 // This class should exist in a 1:1 relationship with a MaterialState object,
570 // since there's no current support for dynamically changing the ticker
571 // provider.
572 @override
573 final TickerProvider vsync;
574
575 // This is here to satisfy the MaterialInkController contract.
576 // The actual painting of this color is done by a Container in the
577 // MaterialState build method.
578 @override
579 Color? color;
580
581 bool absorbHitTest;
582
583 @visibleForTesting
584 List<InkFeature>? get debugInkFeatures {
585 if (kDebugMode) {
586 return _inkFeatures;
587 }
588 return null;
589 }
590
591 List<InkFeature>? _inkFeatures;
592
593 @override
594 void addInkFeature(InkFeature feature) {
595 assert(!feature._debugDisposed);
596 assert(feature._controller == this);
597 _inkFeatures ??= <InkFeature>[];
598 assert(!_inkFeatures!.contains(feature));
599 _inkFeatures!.add(feature);
600 markNeedsPaint();
601 }
602
603 void _removeFeature(InkFeature feature) {
604 assert(_inkFeatures != null);
605 _inkFeatures!.remove(feature);
606 markNeedsPaint();
607 }
608
609 void _didChangeLayout() {
610 if (_inkFeatures?.isNotEmpty ?? false) {
611 markNeedsPaint();
612 }
613 }
614
615 @override
616 bool hitTestSelf(Offset position) => absorbHitTest;
617
618 @override
619 void paint(PaintingContext context, Offset offset) {
620 final List<InkFeature>? inkFeatures = _inkFeatures;
621 if (inkFeatures != null && inkFeatures.isNotEmpty) {
622 final Canvas canvas = context.canvas;
623 canvas.save();
624 canvas.translate(offset.dx, offset.dy);
625 canvas.clipRect(Offset.zero & size);
626 for (final InkFeature inkFeature in inkFeatures) {
627 inkFeature._paint(canvas);
628 }
629 canvas.restore();
630 }
631 assert(inkFeatures == _inkFeatures);
632 super.paint(context, offset);
633 }
634}
635
636class _InkFeatures extends SingleChildRenderObjectWidget {
637 const _InkFeatures({
638 super.key,
639 this.color,
640 required this.vsync,
641 required this.absorbHitTest,
642 super.child,
643 });
644
645 // This widget must be owned by a MaterialState, which must be provided as the vsync.
646 // This relationship must be 1:1 and cannot change for the lifetime of the MaterialState.
647
648 final Color? color;
649
650 final TickerProvider vsync;
651
652 final bool absorbHitTest;
653
654 @override
655 _RenderInkFeatures createRenderObject(BuildContext context) {
656 return _RenderInkFeatures(color: color, absorbHitTest: absorbHitTest, vsync: vsync);
657 }
658
659 @override
660 void updateRenderObject(BuildContext context, _RenderInkFeatures renderObject) {
661 renderObject
662 ..color = color
663 ..absorbHitTest = absorbHitTest;
664 assert(vsync == renderObject.vsync);
665 }
666}
667
668/// A visual reaction on a piece of [Material].
669///
670/// To add an ink feature to a piece of [Material], obtain the
671/// [MaterialInkController] via [Material.of] and call
672/// [MaterialInkController.addInkFeature].
673abstract class InkFeature {
674 /// Initializes fields for subclasses.
675 InkFeature({
676 required MaterialInkController controller,
677 required this.referenceBox,
678 this.onRemoved,
679 }) : _controller = controller as _RenderInkFeatures {
680 assert(debugMaybeDispatchCreated('material', 'InkFeature', this));
681 }
682
683 /// The [MaterialInkController] associated with this [InkFeature].
684 ///
685 /// Typically used by subclasses to call
686 /// [MaterialInkController.markNeedsPaint] when they need to repaint.
687 MaterialInkController get controller => _controller;
688 final _RenderInkFeatures _controller;
689
690 /// The render box whose visual position defines the frame of reference for this ink feature.
691 final RenderBox referenceBox;
692
693 /// Called when the ink feature is no longer visible on the material.
694 final VoidCallback? onRemoved;
695
696 bool _debugDisposed = false;
697
698 /// Free up the resources associated with this ink feature.
699 @mustCallSuper
700 void dispose() {
701 assert(!_debugDisposed);
702 assert(() {
703 _debugDisposed = true;
704 return true;
705 }());
706 assert(debugMaybeDispatchDisposed(this));
707 _controller._removeFeature(this);
708 onRemoved?.call();
709 }
710
711 // Returns the paint transform that allows `fromRenderObject` to perform paint
712 // in `toRenderObject`'s coordinate space.
713 //
714 // Returns null if either `fromRenderObject` or `toRenderObject` is not in the
715 // same render tree, or either of them is in an offscreen subtree (see
716 // RenderObject.paintsChild).
717 static Matrix4? _getPaintTransform(RenderObject fromRenderObject, RenderObject toRenderObject) {
718 // The paths to fromRenderObject and toRenderObject's common ancestor.
719 final List<RenderObject> fromPath = <RenderObject>[fromRenderObject];
720 final List<RenderObject> toPath = <RenderObject>[toRenderObject];
721
722 RenderObject from = fromRenderObject;
723 RenderObject to = toRenderObject;
724
725 while (!identical(from, to)) {
726 final int fromDepth = from.depth;
727 final int toDepth = to.depth;
728
729 if (fromDepth >= toDepth) {
730 final RenderObject? fromParent = from.parent;
731 // Return early if the 2 render objects are not in the same render tree,
732 // or either of them is offscreen and thus won't get painted.
733 if (fromParent is! RenderObject || !fromParent.paintsChild(from)) {
734 return null;
735 }
736 fromPath.add(fromParent);
737 from = fromParent;
738 }
739
740 if (fromDepth <= toDepth) {
741 final RenderObject? toParent = to.parent;
742 if (toParent is! RenderObject || !toParent.paintsChild(to)) {
743 return null;
744 }
745 toPath.add(toParent);
746 to = toParent;
747 }
748 }
749 assert(identical(from, to));
750
751 final Matrix4 transform = Matrix4.identity();
752 final Matrix4 inverseTransform = Matrix4.identity();
753
754 for (int index = toPath.length - 1; index > 0; index -= 1) {
755 toPath[index].applyPaintTransform(toPath[index - 1], transform);
756 }
757 for (int index = fromPath.length - 1; index > 0; index -= 1) {
758 fromPath[index].applyPaintTransform(fromPath[index - 1], inverseTransform);
759 }
760
761 final double det = inverseTransform.invert();
762 return det != 0 ? (inverseTransform..multiply(transform)) : null;
763 }
764
765 void _paint(Canvas canvas) {
766 assert(referenceBox.attached);
767 assert(!_debugDisposed);
768 // determine the transform that gets our coordinate system to be like theirs
769 final Matrix4? transform = _getPaintTransform(_controller, referenceBox);
770 if (transform != null) {
771 paintFeature(canvas, transform);
772 }
773 }
774
775 /// Override this method to paint the ink feature.
776 ///
777 /// The transform argument gives the coordinate conversion from the coordinate
778 /// system of the canvas to the coordinate system of the [referenceBox].
779 @protected
780 void paintFeature(Canvas canvas, Matrix4 transform);
781
782 @override
783 String toString() => describeIdentity(this);
784}
785
786/// An interpolation between two [ShapeBorder]s.
787///
788/// This class specializes the interpolation of [Tween] to use [ShapeBorder.lerp].
789class ShapeBorderTween extends Tween<ShapeBorder?> {
790 /// Creates a [ShapeBorder] tween.
791 ///
792 /// the [begin] and [end] properties may be null; see [ShapeBorder.lerp] for
793 /// the null handling semantics.
794 ShapeBorderTween({super.begin, super.end});
795
796 /// Returns the value this tween has at the given animation clock value.
797 @override
798 ShapeBorder? lerp(double t) {
799 return ShapeBorder.lerp(begin, end, t);
800 }
801}
802
803/// The interior of non-transparent material.
804///
805/// Animates [elevation], [shadowColor], and [shape].
806class _MaterialInterior extends ImplicitlyAnimatedWidget {
807 /// Creates a const instance of [_MaterialInterior].
808 ///
809 /// The [elevation] must be specified and greater than or equal to zero.
810 const _MaterialInterior({
811 required this.child,
812 required this.shape,
813 this.borderOnForeground = true,
814 this.clipBehavior = Clip.none,
815 required this.elevation,
816 required this.color,
817 required this.shadowColor,
818 required this.surfaceTintColor,
819 super.curve,
820 required super.duration,
821 }) : assert(elevation >= 0.0);
822
823 /// The widget below this widget in the tree.
824 ///
825 /// {@macro flutter.widgets.ProxyWidget.child}
826 final Widget child;
827
828 /// The border of the widget.
829 ///
830 /// This border will be painted, and in addition the outer path of the border
831 /// determines the physical shape.
832 final ShapeBorder shape;
833
834 /// Whether to paint the border in front of the child.
835 ///
836 /// The default value is true.
837 /// If false, the border will be painted behind the child.
838 final bool borderOnForeground;
839
840 /// {@macro flutter.material.Material.clipBehavior}
841 ///
842 /// Defaults to [Clip.none].
843 final Clip clipBehavior;
844
845 /// The target z-coordinate at which to place this physical object relative
846 /// to its parent.
847 ///
848 /// The value is non-negative.
849 final double elevation;
850
851 /// The target background color.
852 final Color color;
853
854 /// The target shadow color.
855 final Color shadowColor;
856
857 /// The target surface tint color.
858 final Color? surfaceTintColor;
859
860 @override
861 _MaterialInteriorState createState() => _MaterialInteriorState();
862
863 @override
864 void debugFillProperties(DiagnosticPropertiesBuilder description) {
865 super.debugFillProperties(description);
866 description.add(DiagnosticsProperty<ShapeBorder>('shape', shape));
867 description.add(DoubleProperty('elevation', elevation));
868 description.add(ColorProperty('color', color));
869 description.add(ColorProperty('shadowColor', shadowColor));
870 }
871}
872
873class _MaterialInteriorState extends AnimatedWidgetBaseState<_MaterialInterior> {
874 Tween<double>? _elevation;
875 ColorTween? _surfaceTintColor;
876 ColorTween? _shadowColor;
877 ShapeBorderTween? _border;
878
879 @override
880 void forEachTween(TweenVisitor<dynamic> visitor) {
881 _elevation =
882 visitor(
883 _elevation,
884 widget.elevation,
885 (dynamic value) => Tween<double>(begin: value as double),
886 )
887 as Tween<double>?;
888 _shadowColor =
889 visitor(
890 _shadowColor,
891 widget.shadowColor,
892 (dynamic value) => ColorTween(begin: value as Color),
893 )
894 as ColorTween?;
895 _surfaceTintColor =
896 widget.surfaceTintColor != null
897 ? visitor(
898 _surfaceTintColor,
899 widget.surfaceTintColor,
900 (dynamic value) => ColorTween(begin: value as Color),
901 )
902 as ColorTween?
903 : null;
904 _border =
905 visitor(
906 _border,
907 widget.shape,
908 (dynamic value) => ShapeBorderTween(begin: value as ShapeBorder),
909 )
910 as ShapeBorderTween?;
911 }
912
913 @override
914 Widget build(BuildContext context) {
915 final ShapeBorder shape = _border!.evaluate(animation)!;
916 final double elevation = _elevation!.evaluate(animation);
917 final Color color =
918 Theme.of(context).useMaterial3
919 ? ElevationOverlay.applySurfaceTint(
920 widget.color,
921 _surfaceTintColor?.evaluate(animation),
922 elevation,
923 )
924 : ElevationOverlay.applyOverlay(context, widget.color, elevation);
925 final Color shadowColor = _shadowColor!.evaluate(animation)!;
926
927 return PhysicalShape(
928 clipper: ShapeBorderClipper(shape: shape, textDirection: Directionality.maybeOf(context)),
929 clipBehavior: widget.clipBehavior,
930 elevation: elevation,
931 color: color,
932 shadowColor: shadowColor,
933 child: _ShapeBorderPaint(
934 shape: shape,
935 borderOnForeground: widget.borderOnForeground,
936 child: widget.child,
937 ),
938 );
939 }
940}
941
942class _ShapeBorderPaint extends StatelessWidget {
943 const _ShapeBorderPaint({
944 required this.child,
945 required this.shape,
946 this.borderOnForeground = true,
947 });
948
949 final Widget child;
950 final ShapeBorder shape;
951 final bool borderOnForeground;
952
953 @override
954 Widget build(BuildContext context) {
955 return CustomPaint(
956 painter:
957 borderOnForeground ? null : _ShapeBorderPainter(shape, Directionality.maybeOf(context)),
958 foregroundPainter:
959 borderOnForeground ? _ShapeBorderPainter(shape, Directionality.maybeOf(context)) : null,
960 child: child,
961 );
962 }
963}
964
965class _ShapeBorderPainter extends CustomPainter {
966 _ShapeBorderPainter(this.border, this.textDirection);
967 final ShapeBorder border;
968 final TextDirection? textDirection;
969
970 @override
971 void paint(Canvas canvas, Size size) {
972 border.paint(canvas, Offset.zero & size, textDirection: textDirection);
973 }
974
975 @override
976 bool shouldRepaint(_ShapeBorderPainter oldDelegate) {
977 return oldDelegate.border != border;
978 }
979}
980

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com