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

Provided by KDAB

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