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';
6library;
7
8import 'package:flutter/foundation.dart';
9
10import 'basic_types.dart';
11import 'borders.dart';
12import 'box_border.dart';
13import 'box_decoration.dart';
14import 'box_shadow.dart';
15import 'circle_border.dart';
16import 'colors.dart';
17import 'debug.dart';
18import 'decoration.dart';
19import 'decoration_image.dart';
20import 'edge_insets.dart';
21import 'gradient.dart';
22import 'image_provider.dart';
23import '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.
65class 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.
291class _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

Provided by KDAB

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