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 'package:flutter/material.dart'; |
6 | library; |
7 | |
8 | import 'package:flutter/foundation.dart'; |
9 | |
10 | import 'basic_types.dart'; |
11 | import 'borders.dart'; |
12 | import 'box_border.dart'; |
13 | import 'box_decoration.dart'; |
14 | import 'box_shadow.dart'; |
15 | import 'circle_border.dart'; |
16 | import 'colors.dart'; |
17 | import 'debug.dart'; |
18 | import 'decoration.dart'; |
19 | import 'decoration_image.dart'; |
20 | import 'edge_insets.dart'; |
21 | import 'gradient.dart'; |
22 | import 'image_provider.dart'; |
23 | import 'rounded_rectangle_border.dart'; |
24 | |
25 | /// An immutable description of how to paint an arbitrary shape. |
26 | /// |
27 | /// The [ShapeDecoration] class provides a way to draw a [ShapeBorder], |
28 | /// optionally filling it with a color or a gradient, optionally painting an |
29 | /// image into it, and optionally casting a shadow. |
30 | /// |
31 | /// {@tool snippet} |
32 | /// |
33 | /// The following example uses the [Container] widget from the widgets layer to |
34 | /// draw a white rectangle with a 24-pixel multicolor outline, with the text |
35 | /// "RGB" inside it: |
36 | /// |
37 | /// ```dart |
38 | /// Container( |
39 | /// decoration: ShapeDecoration( |
40 | /// color: Colors.white, |
41 | /// shape: Border.all( |
42 | /// color: Colors.red, |
43 | /// width: 8.0, |
44 | /// ) + Border.all( |
45 | /// color: Colors.green, |
46 | /// width: 8.0, |
47 | /// ) + Border.all( |
48 | /// color: Colors.blue, |
49 | /// width: 8.0, |
50 | /// ), |
51 | /// ), |
52 | /// child: const Text('RGB', textAlign: TextAlign.center), |
53 | /// ) |
54 | /// ``` |
55 | /// {@end-tool} |
56 | /// |
57 | /// See also: |
58 | /// |
59 | /// * [DecoratedBox] and [Container], widgets that can be configured with |
60 | /// [ShapeDecoration] objects. |
61 | /// * [BoxDecoration], a similar [Decoration] that is optimized for rectangles |
62 | /// specifically. |
63 | /// * [ShapeBorder], the base class for the objects that are used in the |
64 | /// [shape] property. |
65 | class ShapeDecoration extends Decoration { |
66 | /// Creates a shape decoration. |
67 | /// |
68 | /// * If [color] is null, this decoration does not paint a background color. |
69 | /// * If [gradient] is null, this decoration does not paint gradients. |
70 | /// * If [image] is null, this decoration does not paint a background image. |
71 | /// * If [shadows] is null, this decoration does not paint a shadow. |
72 | /// |
73 | /// The [color] and [gradient] properties are mutually exclusive, one (or |
74 | /// both) of them must be null. |
75 | const ShapeDecoration({this.color, this.image, this.gradient, this.shadows, required this.shape}) |
76 | : assert(!(color != null && gradient != null)); |
77 | |
78 | /// Creates a shape decoration configured to match a [BoxDecoration]. |
79 | /// |
80 | /// The [BoxDecoration] class is more efficient for shapes that it can |
81 | /// describe than the [ShapeDecoration] class is for those same shapes, |
82 | /// because [ShapeDecoration] has to be more general as it can support any |
83 | /// shape. However, having a [ShapeDecoration] is sometimes necessary, for |
84 | /// example when calling [ShapeDecoration.lerp] to transition between |
85 | /// different shapes (e.g. from a [CircleBorder] to a |
86 | /// [RoundedRectangleBorder]; the [BoxDecoration] class cannot animate the |
87 | /// transition from a [BoxShape.circle] to [BoxShape.rectangle]). |
88 | factory ShapeDecoration.fromBoxDecoration(BoxDecoration source) { |
89 | final ShapeBorder shape; |
90 | switch (source.shape) { |
91 | case BoxShape.circle: |
92 | if (source.border != null) { |
93 | assert(source.border!.isUniform); |
94 | shape = CircleBorder(side: source.border!.top); |
95 | } else { |
96 | shape = const CircleBorder(); |
97 | } |
98 | case BoxShape.rectangle: |
99 | if (source.borderRadius != null) { |
100 | assert(source.border == null || source.border!.isUniform); |
101 | shape = RoundedRectangleBorder( |
102 | side: source.border?.top ?? BorderSide.none, |
103 | borderRadius: source.borderRadius!, |
104 | ); |
105 | } else { |
106 | shape = source.border ?? const Border(); |
107 | } |
108 | } |
109 | return ShapeDecoration( |
110 | color: source.color, |
111 | image: source.image, |
112 | gradient: source.gradient, |
113 | shadows: source.boxShadow, |
114 | shape: shape, |
115 | ); |
116 | } |
117 | |
118 | @override |
119 | Path getClipPath(Rect rect, TextDirection textDirection) { |
120 | return shape.getOuterPath(rect, textDirection: textDirection); |
121 | } |
122 | |
123 | /// The color to fill in the background of the shape. |
124 | /// |
125 | /// The color is under the [image]. |
126 | /// |
127 | /// If a [gradient] is specified, [color] must be null. |
128 | final Color? color; |
129 | |
130 | /// A gradient to use when filling the shape. |
131 | /// |
132 | /// The gradient is under the [image]. |
133 | /// |
134 | /// If a [color] is specified, [gradient] must be null. |
135 | final Gradient? gradient; |
136 | |
137 | /// An image to paint inside the shape (clipped to its outline). |
138 | /// |
139 | /// The image is drawn over the [color] or [gradient]. |
140 | final DecorationImage? image; |
141 | |
142 | /// A list of shadows cast by the [shape]. |
143 | /// |
144 | /// See also: |
145 | /// |
146 | /// * [kElevationToShadow], for some predefined shadows used in Material |
147 | /// Design. |
148 | /// * [PhysicalModel], a widget for showing shadows. |
149 | final List<BoxShadow>? shadows; |
150 | |
151 | /// The shape to fill the [color], [gradient], and [image] into and to cast as |
152 | /// the [shadows]. |
153 | /// |
154 | /// Shapes can be stacked (using the `+` operator). The color, gradient, and |
155 | /// image are drawn into the inner-most shape specified. |
156 | /// |
157 | /// The [shape] property specifies the outline (border) of the decoration. |
158 | /// |
159 | /// ## Directionality-dependent shapes |
160 | /// |
161 | /// Some [ShapeBorder] subclasses are sensitive to the [TextDirection]. The |
162 | /// direction that is provided to the border (e.g. for its [ShapeBorder.paint] |
163 | /// method) is the one specified in the [ImageConfiguration] |
164 | /// ([ImageConfiguration.textDirection]) provided to the [BoxPainter] (via its |
165 | /// [BoxPainter.paint method). The [BoxPainter] is obtained when |
166 | /// [createBoxPainter] is called. |
167 | /// |
168 | /// When a [ShapeDecoration] is used with a [Container] widget or a |
169 | /// [DecoratedBox] widget (which is what [Container] uses), the |
170 | /// [TextDirection] specified in the [ImageConfiguration] is obtained from the |
171 | /// ambient [Directionality], using [createLocalImageConfiguration]. |
172 | final ShapeBorder shape; |
173 | |
174 | /// The inset space occupied by the [shape]'s border. |
175 | /// |
176 | /// This value may be misleading. See the discussion at [ShapeBorder.dimensions]. |
177 | @override |
178 | EdgeInsetsGeometry get padding => shape.dimensions; |
179 | |
180 | @override |
181 | bool get isComplex => shadows != null; |
182 | |
183 | @override |
184 | ShapeDecoration? lerpFrom(Decoration? a, double t) { |
185 | return switch (a) { |
186 | BoxDecoration() => ShapeDecoration.lerp(ShapeDecoration.fromBoxDecoration(a), this, t), |
187 | ShapeDecoration? _ => ShapeDecoration.lerp(a, this, t), |
188 | _ => super.lerpFrom(a, t) as ShapeDecoration?, |
189 | }; |
190 | } |
191 | |
192 | @override |
193 | ShapeDecoration? lerpTo(Decoration? b, double t) { |
194 | return switch (b) { |
195 | BoxDecoration() => ShapeDecoration.lerp(this, ShapeDecoration.fromBoxDecoration(b), t), |
196 | ShapeDecoration? _ => ShapeDecoration.lerp(this, b, t), |
197 | _ => super.lerpTo(b, t) as ShapeDecoration?, |
198 | }; |
199 | } |
200 | |
201 | /// Linearly interpolate between two shapes. |
202 | /// |
203 | /// Interpolates each parameter of the decoration separately. |
204 | /// |
205 | /// If both values are null, this returns null. Otherwise, it returns a |
206 | /// non-null value, with null arguments treated like a [ShapeDecoration] whose |
207 | /// fields are all null (including the [shape], which cannot normally be |
208 | /// null). |
209 | /// |
210 | /// {@macro dart.ui.shadow.lerp} |
211 | /// |
212 | /// See also: |
213 | /// |
214 | /// * [Decoration.lerp], which can interpolate between any two types of |
215 | /// [Decoration]s, not just [ShapeDecoration]s. |
216 | /// * [lerpFrom] and [lerpTo], which are used to implement [Decoration.lerp] |
217 | /// and which use [ShapeDecoration.lerp] when interpolating two |
218 | /// [ShapeDecoration]s or a [ShapeDecoration] to or from null. |
219 | static ShapeDecoration? lerp(ShapeDecoration? a, ShapeDecoration? b, double t) { |
220 | if (identical(a, b)) { |
221 | return a; |
222 | } |
223 | if (a != null && b != null) { |
224 | if (t == 0.0) { |
225 | return a; |
226 | } |
227 | if (t == 1.0) { |
228 | return b; |
229 | } |
230 | } |
231 | return ShapeDecoration( |
232 | color: Color.lerp(a?.color, b?.color, t), |
233 | gradient: Gradient.lerp(a?.gradient, b?.gradient, t), |
234 | image: DecorationImage.lerp(a?.image, b?.image, t), |
235 | shadows: BoxShadow.lerpList(a?.shadows, b?.shadows, t), |
236 | shape: ShapeBorder.lerp(a?.shape, b?.shape, t)!, |
237 | ); |
238 | } |
239 | |
240 | @override |
241 | bool operator ==(Object other) { |
242 | if (identical(this, other)) { |
243 | return true; |
244 | } |
245 | if (other.runtimeType != runtimeType) { |
246 | return false; |
247 | } |
248 | return other is ShapeDecoration && |
249 | other.color == color && |
250 | other.gradient == gradient && |
251 | other.image == image && |
252 | listEquals<BoxShadow>(other.shadows, shadows) && |
253 | other.shape == shape; |
254 | } |
255 | |
256 | @override |
257 | int get hashCode => |
258 | Object.hash(color, gradient, image, shape, shadows == null ? null : Object.hashAll(shadows!)); |
259 | |
260 | @override |
261 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
262 | super.debugFillProperties(properties); |
263 | properties.defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.whitespace; |
264 | properties.add(ColorProperty('color', color, defaultValue: null)); |
265 | properties.add(DiagnosticsProperty<Gradient>('gradient', gradient, defaultValue: null)); |
266 | properties.add(DiagnosticsProperty<DecorationImage>('image', image, defaultValue: null)); |
267 | properties.add( |
268 | IterableProperty<BoxShadow>( |
269 | 'shadows', |
270 | shadows, |
271 | defaultValue: null, |
272 | style: DiagnosticsTreeStyle.whitespace, |
273 | ), |
274 | ); |
275 | properties.add(DiagnosticsProperty<ShapeBorder>('shape', shape)); |
276 | } |
277 | |
278 | @override |
279 | bool hitTest(Size size, Offset position, {TextDirection? textDirection}) { |
280 | return shape.getOuterPath(Offset.zero & size, textDirection: textDirection).contains(position); |
281 | } |
282 | |
283 | @override |
284 | BoxPainter createBoxPainter([VoidCallback? onChanged]) { |
285 | assert(onChanged != null || image == null); |
286 | return _ShapeDecorationPainter(this, onChanged!); |
287 | } |
288 | } |
289 | |
290 | /// An object that paints a [ShapeDecoration] into a canvas. |
291 | class _ShapeDecorationPainter extends BoxPainter { |
292 | _ShapeDecorationPainter(this._decoration, VoidCallback onChanged) : super(onChanged); |
293 | |
294 | final ShapeDecoration _decoration; |
295 | |
296 | Rect? _lastRect; |
297 | TextDirection? _lastTextDirection; |
298 | late Path _outerPath; |
299 | Path? _innerPath; |
300 | Paint? _interiorPaint; |
301 | int? _shadowCount; |
302 | late List<Rect> _shadowBounds; |
303 | late List<Path> _shadowPaths; |
304 | late List<Paint> _shadowPaints; |
305 | |
306 | @override |
307 | VoidCallback get onChanged => super.onChanged!; |
308 | |
309 | void _precache(Rect rect, TextDirection? textDirection) { |
310 | if (rect == _lastRect && textDirection == _lastTextDirection) { |
311 | return; |
312 | } |
313 | |
314 | // We reach here in two cases: |
315 | // - the very first time we paint, in which case everything except _decoration is null |
316 | // - subsequent times, if the rect has changed, in which case we only need to update |
317 | // the features that depend on the actual rect. |
318 | if (_interiorPaint == null && (_decoration.color != null || _decoration.gradient != null)) { |
319 | _interiorPaint = Paint(); |
320 | if (_decoration.color != null) { |
321 | _interiorPaint!.color = _decoration.color!; |
322 | } |
323 | } |
324 | if (_decoration.gradient != null) { |
325 | _interiorPaint!.shader = _decoration.gradient!.createShader( |
326 | rect, |
327 | textDirection: textDirection, |
328 | ); |
329 | } |
330 | if (_decoration.shadows != null) { |
331 | if (_shadowCount == null) { |
332 | _shadowCount = _decoration.shadows!.length; |
333 | _shadowPaints = <Paint>[ |
334 | ..._decoration.shadows!.map((BoxShadow shadow) => shadow.toPaint()), |
335 | ]; |
336 | } |
337 | if (_decoration.shape.preferPaintInterior) { |
338 | _shadowBounds = <Rect>[ |
339 | ..._decoration.shadows!.map((BoxShadow shadow) { |
340 | return rect.shift(shadow.offset).inflate(shadow.spreadRadius); |
341 | }), |
342 | ]; |
343 | } else { |
344 | _shadowPaths = <Path>[ |
345 | ..._decoration.shadows!.map((BoxShadow shadow) { |
346 | return _decoration.shape.getOuterPath( |
347 | rect.shift(shadow.offset).inflate(shadow.spreadRadius), |
348 | textDirection: textDirection, |
349 | ); |
350 | }), |
351 | ]; |
352 | } |
353 | } |
354 | if (!_decoration.shape.preferPaintInterior && |
355 | (_interiorPaint != null || _shadowCount != null)) { |
356 | _outerPath = _decoration.shape.getOuterPath(rect, textDirection: textDirection); |
357 | } |
358 | if (_decoration.image != null) { |
359 | _innerPath = _decoration.shape.getInnerPath(rect, textDirection: textDirection); |
360 | } |
361 | |
362 | _lastRect = rect; |
363 | _lastTextDirection = textDirection; |
364 | } |
365 | |
366 | void _paintShadows(Canvas canvas, Rect rect, TextDirection? textDirection) { |
367 | // The debugHandleDisabledShadowStart and debugHandleDisabledShadowEnd |
368 | // methods are used in debug mode only to support BlurStyle.outer when |
369 | // debugDisableShadows is set. Without these clips, the shadows would extend |
370 | // to the inside of the shape, which would likely obscure important |
371 | // portions of the rendering and would cause unit tests of widgets that use |
372 | // BlurStyle.outer to significantly diverge from the original intent. |
373 | // It is assumed that [debugDisableShadows] will not change when calling |
374 | // paintInterior or getOuterPath; if it does, the results are undefined. |
375 | bool debugHandleDisabledShadowStart(Canvas canvas, BoxShadow boxShadow, Path path) { |
376 | if (debugDisableShadows && boxShadow.blurStyle == BlurStyle.outer) { |
377 | canvas.save(); |
378 | final Path clipPath = Path(); |
379 | clipPath.fillType = PathFillType.evenOdd; |
380 | clipPath.addRect(Rect.largest); |
381 | clipPath.addPath(path, Offset.zero); |
382 | canvas.clipPath(clipPath); |
383 | } |
384 | return true; |
385 | } |
386 | |
387 | bool debugHandleDisabledShadowEnd(Canvas canvas, BoxShadow boxShadow) { |
388 | if (debugDisableShadows && boxShadow.blurStyle == BlurStyle.outer) { |
389 | canvas.restore(); |
390 | } |
391 | return true; |
392 | } |
393 | |
394 | if (_shadowCount != null) { |
395 | if (_decoration.shape.preferPaintInterior) { |
396 | for (int index = 0; index < _shadowCount!; index += 1) { |
397 | assert( |
398 | debugHandleDisabledShadowStart( |
399 | canvas, |
400 | _decoration.shadows![index], |
401 | _decoration.shape.getOuterPath(_shadowBounds[index], textDirection: textDirection), |
402 | ), |
403 | ); |
404 | _decoration.shape.paintInterior( |
405 | canvas, |
406 | _shadowBounds[index], |
407 | _shadowPaints[index], |
408 | textDirection: textDirection, |
409 | ); |
410 | assert(debugHandleDisabledShadowEnd(canvas, _decoration.shadows![index])); |
411 | } |
412 | } else { |
413 | for (int index = 0; index < _shadowCount!; index += 1) { |
414 | assert( |
415 | debugHandleDisabledShadowStart( |
416 | canvas, |
417 | _decoration.shadows![index], |
418 | _shadowPaths[index], |
419 | ), |
420 | ); |
421 | canvas.drawPath(_shadowPaths[index], _shadowPaints[index]); |
422 | assert(debugHandleDisabledShadowEnd(canvas, _decoration.shadows![index])); |
423 | } |
424 | } |
425 | } |
426 | } |
427 | |
428 | void _paintInterior(Canvas canvas, Rect rect, TextDirection? textDirection) { |
429 | if (_interiorPaint != null) { |
430 | if (_decoration.shape.preferPaintInterior) { |
431 | // When border is filled, the rect is reduced to avoid anti-aliasing |
432 | // rounding error leaking the background color around the clipped shape. |
433 | final Rect adjustedRect = _adjustedRectOnOutlinedBorder(rect); |
434 | _decoration.shape.paintInterior( |
435 | canvas, |
436 | adjustedRect, |
437 | _interiorPaint!, |
438 | textDirection: textDirection, |
439 | ); |
440 | } else { |
441 | canvas.drawPath(_outerPath, _interiorPaint!); |
442 | } |
443 | } |
444 | } |
445 | |
446 | Rect _adjustedRectOnOutlinedBorder(Rect rect) { |
447 | if (_decoration.shape is OutlinedBorder && _decoration.color != null) { |
448 | final BorderSide side = (_decoration.shape as OutlinedBorder).side; |
449 | if (side.color.alpha == 255 && side.style == BorderStyle.solid) { |
450 | return rect.deflate(side.strokeInset / 2); |
451 | } |
452 | } |
453 | return rect; |
454 | } |
455 | |
456 | DecorationImagePainter? _imagePainter; |
457 | void _paintImage(Canvas canvas, ImageConfiguration configuration) { |
458 | if (_decoration.image == null) { |
459 | return; |
460 | } |
461 | _imagePainter ??= _decoration.image!.createPainter(onChanged); |
462 | _imagePainter!.paint(canvas, _lastRect!, _innerPath, configuration); |
463 | } |
464 | |
465 | @override |
466 | void dispose() { |
467 | _imagePainter?.dispose(); |
468 | super.dispose(); |
469 | } |
470 | |
471 | @override |
472 | void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) { |
473 | assert(configuration.size != null); |
474 | final Rect rect = offset & configuration.size!; |
475 | final TextDirection? textDirection = configuration.textDirection; |
476 | _precache(rect, textDirection); |
477 | _paintShadows(canvas, rect, textDirection); |
478 | _paintInterior(canvas, rect, textDirection); |
479 | _paintImage(canvas, configuration); |
480 | _decoration.shape.paint(canvas, rect, textDirection: textDirection); |
481 | } |
482 | } |
483 |
Definitions
- ShapeDecoration
- ShapeDecoration
- fromBoxDecoration
- getClipPath
- padding
- isComplex
- lerpFrom
- lerpTo
- lerp
- ==
- hashCode
- debugFillProperties
- hitTest
- createBoxPainter
- _ShapeDecorationPainter
- _ShapeDecorationPainter
- onChanged
- _precache
- _paintShadows
- debugHandleDisabledShadowStart
- debugHandleDisabledShadowEnd
- _paintInterior
- _adjustedRectOnOutlinedBorder
- _paintImage
- dispose
Learn more about Flutter for embedded and desktop on industrialflutter.com