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'; |
15 | library; |
16 | |
17 | import 'package:flutter/foundation.dart'; |
18 | import 'package:flutter/rendering.dart'; |
19 | import 'package:flutter/widgets.dart'; |
20 | |
21 | import 'constants.dart'; |
22 | import 'elevation_overlay.dart'; |
23 | import '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. |
31 | typedef 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] |
40 | enum 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] |
72 | const 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]. |
83 | abstract 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> |
183 | class 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 | |
447 | class _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 | |
561 | class _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 | |
636 | class _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]. |
673 | abstract 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]. |
789 | class 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]. |
806 | class _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 | |
873 | class _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 | |
942 | class _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 | |
965 | class _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 |
Definitions
- MaterialType
- kMaterialEdges
- MaterialInkController
- color
- vsync
- addInkFeature
- markNeedsPaint
- Material
- Material
- maybeOf
- of
- createState
- debugFillProperties
- _MaterialState
- build
- _RenderInkFeatures
- _RenderInkFeatures
- debugInkFeatures
- addInkFeature
- _removeFeature
- _didChangeLayout
- hitTestSelf
- paint
- _InkFeatures
- _InkFeatures
- createRenderObject
- updateRenderObject
- InkFeature
- InkFeature
- controller
- dispose
- _getPaintTransform
- _paint
- paintFeature
- toString
- ShapeBorderTween
- ShapeBorderTween
- lerp
- _MaterialInterior
- _MaterialInterior
- createState
- debugFillProperties
- _MaterialInteriorState
- forEachTween
- build
- _ShapeBorderPaint
- _ShapeBorderPaint
- build
- _ShapeBorderPainter
- _ShapeBorderPainter
- paint
Learn more about Flutter for embedded and desktop on industrialflutter.com