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 'dart:math' as math;
9
10import 'package:flutter/foundation.dart';
11
12import 'basic_types.dart';
13import 'border_radius.dart';
14import 'borders.dart';
15import 'box_border.dart';
16import 'box_shadow.dart';
17import 'colors.dart';
18import 'debug.dart';
19import 'decoration.dart';
20import 'decoration_image.dart';
21import 'edge_insets.dart';
22import 'gradient.dart';
23import 'image_provider.dart';
24
25/// An immutable description of how to paint a box.
26///
27/// The [BoxDecoration] class provides a variety of ways to draw a box.
28///
29/// The box has a [border], a body, and may cast a [boxShadow].
30///
31/// The [shape] of the box can be [BoxShape.circle] or [BoxShape.rectangle]. If
32/// it is [BoxShape.rectangle], then the [borderRadius] property can be used to
33/// make it a rounded rectangle ([RRect]).
34///
35/// The body of the box is painted in layers. The bottom-most layer is the
36/// [color], which fills the box. Above that is the [gradient], which also fills
37/// the box. Finally there is the [image], the precise alignment of which is
38/// controlled by the [DecorationImage] class.
39///
40/// The [border] paints over the body; the [boxShadow], naturally, paints below it.
41///
42/// {@tool snippet}
43///
44/// The following applies a [BoxDecoration] to a [Container] widget to draw an
45/// [image] of an owl with a thick black [border] and rounded corners.
46///
47/// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/box_decoration.png)
48///
49/// ```dart
50/// Container(
51/// decoration: BoxDecoration(
52/// color: const Color(0xff7c94b6),
53/// image: const DecorationImage(
54/// image: NetworkImage('https://flutter.github.io/assets-for-api-docs/assets/widgets/owl-2.jpg'),
55/// fit: BoxFit.cover,
56/// ),
57/// border: Border.all(
58/// width: 8,
59/// ),
60/// borderRadius: BorderRadius.circular(12),
61/// ),
62/// )
63/// ```
64/// {@end-tool}
65///
66/// {@template flutter.painting.BoxDecoration.clip}
67/// The [shape] or the [borderRadius] won't clip the children of the
68/// decorated [Container]. If the clip is required, insert a clip widget
69/// (e.g., [ClipRect], [ClipRRect], [ClipPath]) as the child of the [Container].
70/// Be aware that clipping may be costly in terms of performance.
71/// {@endtemplate}
72///
73/// See also:
74///
75/// * [DecoratedBox] and [Container], widgets that can be configured with
76/// [BoxDecoration] objects.
77/// * [DecoratedSliver], a widget that can be configured with a [BoxDecoration]
78/// that is converted to render with slivers.
79/// * [CustomPaint], a widget that lets you draw arbitrary graphics.
80/// * [Decoration], the base class which lets you define other decorations.
81class BoxDecoration extends Decoration {
82 /// Creates a box decoration.
83 ///
84 /// * If [color] is null, this decoration does not paint a background color.
85 /// * If [image] is null, this decoration does not paint a background image.
86 /// * If [border] is null, this decoration does not paint a border.
87 /// * If [borderRadius] is null, this decoration uses more efficient background
88 /// painting commands. The [borderRadius] argument must be null if [shape] is
89 /// [BoxShape.circle].
90 /// * If [boxShadow] is null, this decoration does not paint a shadow.
91 /// * If [gradient] is null, this decoration does not paint gradients.
92 /// * If [backgroundBlendMode] is null, this decoration paints with [BlendMode.srcOver]
93 const BoxDecoration({
94 this.color,
95 this.image,
96 this.border,
97 this.borderRadius,
98 this.boxShadow,
99 this.gradient,
100 this.backgroundBlendMode,
101 this.shape = BoxShape.rectangle,
102 }) : assert(
103 backgroundBlendMode == null || color != null || gradient != null,
104 "backgroundBlendMode applies to BoxDecoration's background color or "
105 'gradient, but no color or gradient was provided.',
106 );
107
108 /// Creates a copy of this object but with the given fields replaced with the
109 /// new values.
110 BoxDecoration copyWith({
111 Color? color,
112 DecorationImage? image,
113 BoxBorder? border,
114 BorderRadiusGeometry? borderRadius,
115 List<BoxShadow>? boxShadow,
116 Gradient? gradient,
117 BlendMode? backgroundBlendMode,
118 BoxShape? shape,
119 }) {
120 return BoxDecoration(
121 color: color ?? this.color,
122 image: image ?? this.image,
123 border: border ?? this.border,
124 borderRadius: borderRadius ?? this.borderRadius,
125 boxShadow: boxShadow ?? this.boxShadow,
126 gradient: gradient ?? this.gradient,
127 backgroundBlendMode: backgroundBlendMode ?? this.backgroundBlendMode,
128 shape: shape ?? this.shape,
129 );
130 }
131
132 @override
133 bool debugAssertIsValid() {
134 assert(
135 shape != BoxShape.circle || borderRadius == null,
136 ); // Can't have a border radius if you're a circle.
137 return super.debugAssertIsValid();
138 }
139
140 /// The color to fill in the background of the box.
141 ///
142 /// The color is filled into the [shape] of the box (e.g., either a rectangle,
143 /// potentially with a [borderRadius], or a circle).
144 ///
145 /// This is ignored if [gradient] is non-null.
146 ///
147 /// The [color] is drawn under the [image].
148 final Color? color;
149
150 /// An image to paint above the background [color] or [gradient].
151 ///
152 /// If [shape] is [BoxShape.circle] then the image is clipped to the circle's
153 /// boundary; if [borderRadius] is non-null then the image is clipped to the
154 /// given radii.
155 final DecorationImage? image;
156
157 /// A border to draw above the background [color], [gradient], or [image].
158 ///
159 /// Follows the [shape] and [borderRadius].
160 ///
161 /// Use [Border] objects to describe borders that do not depend on the reading
162 /// direction.
163 ///
164 /// Use [BoxBorder] objects to describe borders that should flip their left
165 /// and right edges based on whether the text is being read left-to-right or
166 /// right-to-left.
167 final BoxBorder? border;
168
169 /// If non-null, the corners of this box are rounded by this [BorderRadius].
170 ///
171 /// Applies only to boxes with rectangular shapes; ignored if [shape] is not
172 /// [BoxShape.rectangle].
173 ///
174 /// {@macro flutter.painting.BoxDecoration.clip}
175 final BorderRadiusGeometry? borderRadius;
176
177 /// A list of shadows cast by this box behind the box.
178 ///
179 /// The shadow follows the [shape] of the box.
180 ///
181 /// See also:
182 ///
183 /// * [kElevationToShadow], for some predefined shadows used in Material
184 /// Design.
185 /// * [PhysicalModel], a widget for showing shadows.
186 final List<BoxShadow>? boxShadow;
187
188 /// A gradient to use when filling the box.
189 ///
190 /// If this is specified, [color] has no effect.
191 ///
192 /// The [gradient] is drawn under the [image].
193 final Gradient? gradient;
194
195 /// The blend mode applied to the [color] or [gradient] background of the box.
196 ///
197 /// If no [backgroundBlendMode] is provided then the default painting blend
198 /// mode is used.
199 ///
200 /// If no [color] or [gradient] is provided then the blend mode has no impact.
201 final BlendMode? backgroundBlendMode;
202
203 /// The shape to fill the background [color], [gradient], and [image] into and
204 /// to cast as the [boxShadow].
205 ///
206 /// If this is [BoxShape.circle] then [borderRadius] is ignored.
207 ///
208 /// The [shape] cannot be interpolated; animating between two [BoxDecoration]s
209 /// with different [shape]s will result in a discontinuity in the rendering.
210 /// To interpolate between two shapes, consider using [ShapeDecoration] and
211 /// different [ShapeBorder]s; in particular, [CircleBorder] instead of
212 /// [BoxShape.circle] and [RoundedRectangleBorder] instead of
213 /// [BoxShape.rectangle].
214 ///
215 /// {@macro flutter.painting.BoxDecoration.clip}
216 final BoxShape shape;
217
218 @override
219 EdgeInsetsGeometry get padding => border?.dimensions ?? EdgeInsets.zero;
220
221 @override
222 Path getClipPath(Rect rect, TextDirection textDirection) {
223 switch (shape) {
224 case BoxShape.circle:
225 final Offset center = rect.center;
226 final double radius = rect.shortestSide / 2.0;
227 final Rect square = Rect.fromCircle(center: center, radius: radius);
228 return Path()..addOval(square);
229 case BoxShape.rectangle:
230 if (borderRadius != null) {
231 return Path()..addRRect(borderRadius!.resolve(textDirection).toRRect(rect));
232 }
233 return Path()..addRect(rect);
234 }
235 }
236
237 /// Returns a new box decoration that is scaled by the given factor.
238 BoxDecoration scale(double factor) {
239 return BoxDecoration(
240 color: Color.lerp(null, color, factor),
241 image: DecorationImage.lerp(null, image, factor),
242 border: BoxBorder.lerp(null, border, factor),
243 borderRadius: BorderRadiusGeometry.lerp(null, borderRadius, factor),
244 boxShadow: BoxShadow.lerpList(null, boxShadow, factor),
245 gradient: gradient?.scale(factor),
246 shape: shape,
247 );
248 }
249
250 @override
251 bool get isComplex => boxShadow != null;
252
253 @override
254 BoxDecoration? lerpFrom(Decoration? a, double t) => switch (a) {
255 null => scale(t),
256 BoxDecoration() => BoxDecoration.lerp(a, this, t),
257 _ => super.lerpFrom(a, t) as BoxDecoration?,
258 };
259
260 @override
261 BoxDecoration? lerpTo(Decoration? b, double t) => switch (b) {
262 null => scale(1.0 - t),
263 BoxDecoration() => BoxDecoration.lerp(this, b, t),
264 _ => super.lerpTo(b, t) as BoxDecoration?,
265 };
266
267 /// Linearly interpolate between two box decorations.
268 ///
269 /// Interpolates each parameter of the box decoration separately.
270 ///
271 /// The [shape] is not interpolated. To interpolate the shape, consider using
272 /// a [ShapeDecoration] with different border shapes.
273 ///
274 /// If both values are null, this returns null. Otherwise, it returns a
275 /// non-null value. If one of the values is null, then the result is obtained
276 /// by applying [scale] to the other value. If neither value is null and `t ==
277 /// 0.0`, then `a` is returned unmodified; if `t == 1.0` then `b` is returned
278 /// unmodified. Otherwise, the values are computed by interpolating the
279 /// properties appropriately.
280 ///
281 /// {@macro dart.ui.shadow.lerp}
282 ///
283 /// See also:
284 ///
285 /// * [Decoration.lerp], which can interpolate between any two types of
286 /// [Decoration]s, not just [BoxDecoration]s.
287 /// * [lerpFrom] and [lerpTo], which are used to implement [Decoration.lerp]
288 /// and which use [BoxDecoration.lerp] when interpolating two
289 /// [BoxDecoration]s or a [BoxDecoration] to or from null.
290 static BoxDecoration? lerp(BoxDecoration? a, BoxDecoration? b, double t) {
291 if (identical(a, b)) {
292 return a;
293 }
294 if (a == null) {
295 return b!.scale(t);
296 }
297 if (b == null) {
298 return a.scale(1.0 - t);
299 }
300 if (t == 0.0) {
301 return a;
302 }
303 if (t == 1.0) {
304 return b;
305 }
306 return BoxDecoration(
307 color: Color.lerp(a.color, b.color, t),
308 image: DecorationImage.lerp(a.image, b.image, t),
309 border: BoxBorder.lerp(a.border, b.border, t),
310 borderRadius: BorderRadiusGeometry.lerp(a.borderRadius, b.borderRadius, t),
311 boxShadow: BoxShadow.lerpList(a.boxShadow, b.boxShadow, t),
312 gradient: Gradient.lerp(a.gradient, b.gradient, t),
313 shape: t < 0.5 ? a.shape : b.shape,
314 );
315 }
316
317 @override
318 bool operator ==(Object other) {
319 if (identical(this, other)) {
320 return true;
321 }
322 if (other.runtimeType != runtimeType) {
323 return false;
324 }
325 return other is BoxDecoration &&
326 other.color == color &&
327 other.image == image &&
328 other.border == border &&
329 other.borderRadius == borderRadius &&
330 listEquals<BoxShadow>(other.boxShadow, boxShadow) &&
331 other.gradient == gradient &&
332 other.backgroundBlendMode == backgroundBlendMode &&
333 other.shape == shape;
334 }
335
336 @override
337 int get hashCode => Object.hash(
338 color,
339 image,
340 border,
341 borderRadius,
342 boxShadow == null ? null : Object.hashAll(boxShadow!),
343 gradient,
344 backgroundBlendMode,
345 shape,
346 );
347
348 @override
349 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
350 super.debugFillProperties(properties);
351 properties
352 ..defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.whitespace
353 ..emptyBodyDescription = '<no decorations specified>';
354
355 properties.add(ColorProperty('color', color, defaultValue: null));
356 properties.add(DiagnosticsProperty<DecorationImage>('image', image, defaultValue: null));
357 properties.add(DiagnosticsProperty<BoxBorder>('border', border, defaultValue: null));
358 properties.add(
359 DiagnosticsProperty<BorderRadiusGeometry>('borderRadius', borderRadius, defaultValue: null),
360 );
361 properties.add(
362 IterableProperty<BoxShadow>(
363 'boxShadow',
364 boxShadow,
365 defaultValue: null,
366 style: DiagnosticsTreeStyle.whitespace,
367 ),
368 );
369 properties.add(DiagnosticsProperty<Gradient>('gradient', gradient, defaultValue: null));
370 properties.add(EnumProperty<BoxShape>('shape', shape, defaultValue: BoxShape.rectangle));
371 }
372
373 @override
374 bool hitTest(Size size, Offset position, {TextDirection? textDirection}) {
375 assert((Offset.zero & size).contains(position));
376 switch (shape) {
377 case BoxShape.rectangle:
378 if (borderRadius != null) {
379 final RRect bounds = borderRadius!.resolve(textDirection).toRRect(Offset.zero & size);
380 return bounds.contains(position);
381 }
382 return true;
383 case BoxShape.circle:
384 // Circles are inscribed into our smallest dimension.
385 final Offset center = size.center(Offset.zero);
386 final double distance = (position - center).distance;
387 return distance <= math.min(size.width, size.height) / 2.0;
388 }
389 }
390
391 @override
392 BoxPainter createBoxPainter([VoidCallback? onChanged]) {
393 assert(onChanged != null || image == null);
394 return _BoxDecorationPainter(this, onChanged);
395 }
396}
397
398/// An object that paints a [BoxDecoration] into a canvas.
399class _BoxDecorationPainter extends BoxPainter {
400 _BoxDecorationPainter(this._decoration, super.onChanged);
401
402 final BoxDecoration _decoration;
403
404 Paint? _cachedBackgroundPaint;
405 Rect? _rectForCachedBackgroundPaint;
406 Paint _getBackgroundPaint(Rect rect, TextDirection? textDirection) {
407 assert(_decoration.gradient != null || _rectForCachedBackgroundPaint == null);
408
409 if (_cachedBackgroundPaint == null ||
410 (_decoration.gradient != null && _rectForCachedBackgroundPaint != rect)) {
411 final Paint paint = Paint();
412 if (_decoration.backgroundBlendMode != null) {
413 paint.blendMode = _decoration.backgroundBlendMode!;
414 }
415 if (_decoration.color != null) {
416 paint.color = _decoration.color!;
417 }
418 if (_decoration.gradient != null) {
419 paint.shader = _decoration.gradient!.createShader(rect, textDirection: textDirection);
420 _rectForCachedBackgroundPaint = rect;
421 }
422 _cachedBackgroundPaint = paint;
423 }
424
425 return _cachedBackgroundPaint!;
426 }
427
428 void _paintBox(Canvas canvas, Rect rect, Paint paint, TextDirection? textDirection) {
429 switch (_decoration.shape) {
430 case BoxShape.circle:
431 assert(_decoration.borderRadius == null);
432 final Offset center = rect.center;
433 final double radius = rect.shortestSide / 2.0;
434 canvas.drawCircle(center, radius, paint);
435 case BoxShape.rectangle:
436 if (_decoration.borderRadius == null || _decoration.borderRadius == BorderRadius.zero) {
437 canvas.drawRect(rect, paint);
438 } else {
439 canvas.drawRRect(_decoration.borderRadius!.resolve(textDirection).toRRect(rect), paint);
440 }
441 }
442 }
443
444 void _paintShadows(Canvas canvas, Rect rect, TextDirection? textDirection) {
445 if (_decoration.boxShadow == null) {
446 return;
447 }
448 for (final BoxShadow boxShadow in _decoration.boxShadow!) {
449 final Paint paint = boxShadow.toPaint();
450 final Rect bounds = rect.shift(boxShadow.offset).inflate(boxShadow.spreadRadius);
451 assert(() {
452 if (debugDisableShadows && boxShadow.blurStyle == BlurStyle.outer) {
453 canvas.save();
454 canvas.clipRect(bounds);
455 }
456 return true;
457 }());
458 _paintBox(canvas, bounds, paint, textDirection);
459 assert(() {
460 if (debugDisableShadows && boxShadow.blurStyle == BlurStyle.outer) {
461 canvas.restore();
462 }
463 return true;
464 }());
465 }
466 }
467
468 void _paintBackgroundColor(Canvas canvas, Rect rect, TextDirection? textDirection) {
469 if (_decoration.color != null || _decoration.gradient != null) {
470 // When border is filled, the rect is reduced to avoid anti-aliasing
471 // rounding error leaking the background color around the clipped shape.
472 final Rect adjustedRect = _adjustedRectOnOutlinedBorder(rect, textDirection);
473 _paintBox(canvas, adjustedRect, _getBackgroundPaint(rect, textDirection), textDirection);
474 }
475 }
476
477 double _calculateAdjustedSide(BorderSide side) {
478 if (side.color.alpha == 255 && side.style == BorderStyle.solid) {
479 return side.strokeInset;
480 }
481 return 0;
482 }
483
484 Rect _adjustedRectOnOutlinedBorder(Rect rect, TextDirection? textDirection) {
485 if (_decoration.border == null) {
486 return rect;
487 }
488
489 if (_decoration.border is Border) {
490 final Border border = _decoration.border! as Border;
491
492 final EdgeInsets insets =
493 EdgeInsets.fromLTRB(
494 _calculateAdjustedSide(border.left),
495 _calculateAdjustedSide(border.top),
496 _calculateAdjustedSide(border.right),
497 _calculateAdjustedSide(border.bottom),
498 ) /
499 2;
500
501 return Rect.fromLTRB(
502 rect.left + insets.left,
503 rect.top + insets.top,
504 rect.right - insets.right,
505 rect.bottom - insets.bottom,
506 );
507 } else if (_decoration.border is BorderDirectional && textDirection != null) {
508 final BorderDirectional border = _decoration.border! as BorderDirectional;
509 final BorderSide leftSide = textDirection == TextDirection.rtl ? border.end : border.start;
510 final BorderSide rightSide = textDirection == TextDirection.rtl ? border.start : border.end;
511
512 final EdgeInsets insets =
513 EdgeInsets.fromLTRB(
514 _calculateAdjustedSide(leftSide),
515 _calculateAdjustedSide(border.top),
516 _calculateAdjustedSide(rightSide),
517 _calculateAdjustedSide(border.bottom),
518 ) /
519 2;
520
521 return Rect.fromLTRB(
522 rect.left + insets.left,
523 rect.top + insets.top,
524 rect.right - insets.right,
525 rect.bottom - insets.bottom,
526 );
527 }
528 return rect;
529 }
530
531 DecorationImagePainter? _imagePainter;
532 void _paintBackgroundImage(Canvas canvas, Rect rect, ImageConfiguration configuration) {
533 if (_decoration.image == null) {
534 return;
535 }
536 _imagePainter ??= _decoration.image!.createPainter(onChanged!);
537 Path? clipPath;
538 switch (_decoration.shape) {
539 case BoxShape.circle:
540 assert(_decoration.borderRadius == null);
541 final Offset center = rect.center;
542 final double radius = rect.shortestSide / 2.0;
543 final Rect square = Rect.fromCircle(center: center, radius: radius);
544 clipPath = Path()..addOval(square);
545 case BoxShape.rectangle:
546 if (_decoration.borderRadius != null) {
547 clipPath =
548 Path()..addRRect(
549 _decoration.borderRadius!.resolve(configuration.textDirection).toRRect(rect),
550 );
551 }
552 }
553 _imagePainter!.paint(canvas, rect, clipPath, configuration);
554 }
555
556 @override
557 void dispose() {
558 _imagePainter?.dispose();
559 super.dispose();
560 }
561
562 /// Paint the box decoration into the given location on the given canvas.
563 @override
564 void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
565 assert(configuration.size != null);
566 final Rect rect = offset & configuration.size!;
567 final TextDirection? textDirection = configuration.textDirection;
568 _paintShadows(canvas, rect, textDirection);
569 _paintBackgroundColor(canvas, rect, textDirection);
570 _paintBackgroundImage(canvas, rect, configuration);
571 _decoration.border?.paint(
572 canvas,
573 rect,
574 shape: _decoration.shape,
575 borderRadius: _decoration.borderRadius?.resolve(textDirection),
576 textDirection: configuration.textDirection,
577 );
578 }
579
580 @override
581 String toString() {
582 return 'BoxPainter for $_decoration';
583 }
584}
585

Provided by KDAB

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