box_decoration.dart 17.8 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:math' as math;

7
import 'package:flutter/foundation.dart';
8

9
import 'basic_types.dart';
10
import 'border_radius.dart';
11
import 'box_border.dart';
12
import 'box_shadow.dart';
13
import 'colors.dart';
14
import 'decoration.dart';
15
import 'decoration_image.dart';
16
import 'edge_insets.dart';
17
import 'gradient.dart';
18
import 'image_provider.dart';
19

Florian Loitsch's avatar
Florian Loitsch committed
20
/// An immutable description of how to paint a box.
21
///
Ian Hickson's avatar
Ian Hickson committed
22 23
/// The [BoxDecoration] class provides a variety of ways to draw a box.
///
24
/// The box has a [border], a body, and may cast a [boxShadow].
Ian Hickson's avatar
Ian Hickson committed
25 26 27 28 29 30 31 32 33
///
/// The [shape] of the box can be a circle or a rectangle. If it is a rectangle,
/// then the [borderRadius] property controls the roundness of the corners.
///
/// The body of the box is painted in layers. The bottom-most layer is the
/// [color], which fills the box. Above that is the [gradient], which also fills
/// the box. Finally there is the [image], the precise alignment of which is
/// controlled by the [DecorationImage] class.
///
34
/// The [border] paints over the body; the [boxShadow], naturally, paints below it.
Ian Hickson's avatar
Ian Hickson committed
35
///
36
/// {@tool snippet}
Ian Hickson's avatar
Ian Hickson committed
37
///
38 39 40 41
/// The following applies a [BoxDecoration] to a [Container] widget to draw an
/// [image] of an owl with a thick black [border] and rounded corners.
///
/// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/box_decoration.png)
42 43
///
/// ```dart
44 45
/// Container(
///   decoration: BoxDecoration(
46
///     color: const Color(0xff7c94b6),
47
///     image: const DecorationImage(
48
///       image: NetworkImage('https://flutter.github.io/assets-for-api-docs/assets/widgets/owl-2.jpg'),
49
///       fit: BoxFit.cover,
50
///     ),
51
///     border: Border.all(
52
///       width: 8,
53
///     ),
54
///     borderRadius: BorderRadius.circular(12),
55 56 57
///   ),
/// )
/// ```
58
/// {@end-tool}
Ian Hickson's avatar
Ian Hickson committed
59
///
60
/// {@template flutter.painting.BoxDecoration.clip}
61 62 63 64 65 66
/// The [shape] or the [borderRadius] won't clip the children of the
/// decorated [Container]. If the clip is required, insert a clip widget
/// (e.g., [ClipRect], [ClipRRect], [ClipPath]) as the child of the [Container].
/// Be aware that clipping may be costly in terms of performance.
/// {@endtemplate}
///
Ian Hickson's avatar
Ian Hickson committed
67 68 69 70 71 72
/// See also:
///
///  * [DecoratedBox] and [Container], widgets that can be configured with
///    [BoxDecoration] objects.
///  * [CustomPaint], a widget that lets you draw arbitrary graphics.
///  * [Decoration], the base class which lets you define other decorations.
73
class BoxDecoration extends Decoration {
74 75
  /// Creates a box decoration.
  ///
76 77
  /// * If [color] is null, this decoration does not paint a background color.
  /// * If [image] is null, this decoration does not paint a background image.
78
  /// * If [border] is null, this decoration does not paint a border.
79
  /// * If [borderRadius] is null, this decoration uses more efficient background
80
  ///   painting commands. The [borderRadius] argument must be null if [shape] is
81 82 83
  ///   [BoxShape.circle].
  /// * If [boxShadow] is null, this decoration does not paint a shadow.
  /// * If [gradient] is null, this decoration does not paint gradients.
84
  /// * If [backgroundBlendMode] is null, this decoration paints with [BlendMode.srcOver]
85 86
  ///
  /// The [shape] argument must not be null.
87
  const BoxDecoration({
88 89
    this.color,
    this.image,
90 91 92 93
    this.border,
    this.borderRadius,
    this.boxShadow,
    this.gradient,
94
    this.backgroundBlendMode,
95
    this.shape = BoxShape.rectangle,
96 97
  }) : assert(shape != null),
       assert(
98
         backgroundBlendMode == null || color != null || gradient != null,
99
         "backgroundBlendMode applies to BoxDecoration's background color or "
100
         'gradient, but no color or gradient was provided.',
101
       );
102

103 104 105
  /// Creates a copy of this object but with the given fields replaced with the
  /// new values.
  BoxDecoration copyWith({
106 107 108 109 110 111 112 113
    Color? color,
    DecorationImage? image,
    BoxBorder? border,
    BorderRadiusGeometry? borderRadius,
    List<BoxShadow>? boxShadow,
    Gradient? gradient,
    BlendMode? backgroundBlendMode,
    BoxShape? shape,
114 115 116 117 118 119 120 121 122 123 124 125 126
  }) {
    return BoxDecoration(
      color: color ?? this.color,
      image: image ?? this.image,
      border: border ?? this.border,
      borderRadius: borderRadius ?? this.borderRadius,
      boxShadow: boxShadow ?? this.boxShadow,
      gradient: gradient ?? this.gradient,
      backgroundBlendMode: backgroundBlendMode ?? this.backgroundBlendMode,
      shape: shape ?? this.shape,
    );
  }

127
  @override
128
  bool debugAssertIsValid() {
129
    assert(shape != BoxShape.circle || borderRadius == null); // Can't have a border radius if you're a circle.
130
    return super.debugAssertIsValid();
131 132
  }

Florian Loitsch's avatar
Florian Loitsch committed
133
  /// The color to fill in the background of the box.
134
  ///
135 136 137 138 139 140
  /// The color is filled into the [shape] of the box (e.g., either a rectangle,
  /// potentially with a [borderRadius], or a circle).
  ///
  /// This is ignored if [gradient] is non-null.
  ///
  /// The [color] is drawn under the [image].
141
  final Color? color;
142

143 144 145 146 147
  /// An image to paint above the background [color] or [gradient].
  ///
  /// If [shape] is [BoxShape.circle] then the image is clipped to the circle's
  /// boundary; if [borderRadius] is non-null then the image is clipped to the
  /// given radii.
148
  final DecorationImage? image;
149

150 151 152
  /// A border to draw above the background [color], [gradient], or [image].
  ///
  /// Follows the [shape] and [borderRadius].
Ian Hickson's avatar
Ian Hickson committed
153 154 155 156 157 158 159
  ///
  /// Use [Border] objects to describe borders that do not depend on the reading
  /// direction.
  ///
  /// Use [BoxBorder] objects to describe borders that should flip their left
  /// and right edges based on whether the text is being read left-to-right or
  /// right-to-left.
160
  final BoxBorder? border;
161

162
  /// If non-null, the corners of this box are rounded by this [BorderRadius].
163
  ///
164 165
  /// Applies only to boxes with rectangular shapes; ignored if [shape] is not
  /// [BoxShape.rectangle].
166
  ///
167
  /// {@macro flutter.painting.BoxDecoration.clip}
168
  final BorderRadiusGeometry? borderRadius;
169

170 171 172
  /// A list of shadows cast by this box behind the box.
  ///
  /// The shadow follows the [shape] of the box.
Ian Hickson's avatar
Ian Hickson committed
173 174 175 176 177 178
  ///
  /// See also:
  ///
  ///  * [kElevationToShadow], for some predefined shadows used in Material
  ///    Design.
  ///  * [PhysicalModel], a widget for showing shadows.
179
  final List<BoxShadow>? boxShadow;
180

181
  /// A gradient to use when filling the box.
182 183 184 185
  ///
  /// If this is specified, [color] has no effect.
  ///
  /// The [gradient] is drawn under the [image].
186
  final Gradient? gradient;
187

188 189
  /// The blend mode applied to the [color] or [gradient] background of the box.
  ///
190
  /// If no [backgroundBlendMode] is provided then the default painting blend
191 192
  /// mode is used.
  ///
193
  /// If no [color] or [gradient] is provided then the blend mode has no impact.
194
  final BlendMode? backgroundBlendMode;
195

196 197 198
  /// The shape to fill the background [color], [gradient], and [image] into and
  /// to cast as the [boxShadow].
  ///
199 200 201 202 203 204 205 206
  /// If this is [BoxShape.circle] then [borderRadius] is ignored.
  ///
  /// The [shape] cannot be interpolated; animating between two [BoxDecoration]s
  /// with different [shape]s will result in a discontinuity in the rendering.
  /// To interpolate between two shapes, consider using [ShapeDecoration] and
  /// different [ShapeBorder]s; in particular, [CircleBorder] instead of
  /// [BoxShape.circle] and [RoundedRectangleBorder] instead of
  /// [BoxShape.rectangle].
207
  ///
208
  /// {@macro flutter.painting.BoxDecoration.clip}
209
  final BoxShape shape;
210

211
  @override
212
  EdgeInsetsGeometry? get padding => border?.dimensions;
213

214
  @override
215
  Path getClipPath(Rect rect, TextDirection textDirection) {
216 217
    switch (shape) {
      case BoxShape.circle:
218 219 220
        final Offset center = rect.center;
        final double radius = rect.shortestSide / 2.0;
        final Rect square = Rect.fromCircle(center: center, radius: radius);
221
        return Path()..addOval(square);
222
      case BoxShape.rectangle:
223
        if (borderRadius != null) {
224
          return Path()..addRRect(borderRadius!.resolve(textDirection).toRRect(rect));
225
        }
226
        return Path()..addRect(rect);
227 228 229
    }
  }

Florian Loitsch's avatar
Florian Loitsch committed
230
  /// Returns a new box decoration that is scaled by the given factor.
231
  BoxDecoration scale(double factor) {
232
    return BoxDecoration(
233
      color: Color.lerp(null, color, factor),
234
      image: image, // TODO(ianh): fade the image from transparent
Ian Hickson's avatar
Ian Hickson committed
235
      border: BoxBorder.lerp(null, border, factor),
236
      borderRadius: BorderRadiusGeometry.lerp(null, borderRadius, factor),
237
      boxShadow: BoxShadow.lerpList(null, boxShadow, factor),
238
      gradient: gradient?.scale(factor),
239
      shape: shape,
240 241 242
    );
  }

243 244 245
  @override
  bool get isComplex => boxShadow != null;

246
  @override
247
  BoxDecoration? lerpFrom(Decoration? a, double t) {
248
    if (a == null) {
249
      return scale(t);
250 251
    }
    if (a is BoxDecoration) {
252
      return BoxDecoration.lerp(a, this, t);
253
    }
254
    return super.lerpFrom(a, t) as BoxDecoration?;
255 256 257
  }

  @override
258
  BoxDecoration? lerpTo(Decoration? b, double t) {
259
    if (b == null) {
260
      return scale(1.0 - t);
261 262
    }
    if (b is BoxDecoration) {
263
      return BoxDecoration.lerp(this, b, t);
264
    }
265
    return super.lerpTo(b, t) as BoxDecoration?;
266 267
  }

268
  /// Linearly interpolate between two box decorations.
269 270
  ///
  /// Interpolates each parameter of the box decoration separately.
271
  ///
272 273 274 275 276 277 278 279 280 281
  /// The [shape] is not interpolated. To interpolate the shape, consider using
  /// a [ShapeDecoration] with different border shapes.
  ///
  /// If both values are null, this returns null. Otherwise, it returns a
  /// non-null value. If one of the values is null, then the result is obtained
  /// by applying [scale] to the other value. If neither value is null and `t ==
  /// 0.0`, then `a` is returned unmodified; if `t == 1.0` then `b` is returned
  /// unmodified. Otherwise, the values are computed by interpolating the
  /// properties appropriately.
  ///
282
  /// {@macro dart.ui.shadow.lerp}
283
  ///
284 285 286 287 288
  /// See also:
  ///
  ///  * [Decoration.lerp], which can interpolate between any two types of
  ///    [Decoration]s, not just [BoxDecoration]s.
  ///  * [lerpFrom] and [lerpTo], which are used to implement [Decoration.lerp]
289
  ///    and which use [BoxDecoration.lerp] when interpolating two
290
  ///    [BoxDecoration]s or a [BoxDecoration] to or from null.
291
  static BoxDecoration? lerp(BoxDecoration? a, BoxDecoration? b, double t) {
292
    assert(t != null);
293
    if (a == null && b == null) {
294
      return null;
295 296
    }
    if (a == null) {
297
      return b!.scale(t);
298 299
    }
    if (b == null) {
300
      return a.scale(1.0 - t);
301 302
    }
    if (t == 0.0) {
303
      return a;
304 305
    }
    if (t == 1.0) {
306
      return b;
307
    }
308
    return BoxDecoration(
309
      color: Color.lerp(a.color, b.color, t),
310
      image: t < 0.5 ? a.image : b.image, // TODO(ianh): cross-fade the image
Ian Hickson's avatar
Ian Hickson committed
311
      border: BoxBorder.lerp(a.border, b.border, t),
312
      borderRadius: BorderRadiusGeometry.lerp(a.borderRadius, b.borderRadius, t),
313
      boxShadow: BoxShadow.lerpList(a.boxShadow, b.boxShadow, t),
314
      gradient: Gradient.lerp(a.gradient, b.gradient, t),
315
      shape: t < 0.5 ? a.shape : b.shape,
316 317 318
    );
  }

319
  @override
320
  bool operator ==(Object other) {
321
    if (identical(this, other)) {
322
      return true;
323 324
    }
    if (other.runtimeType != runtimeType) {
325
      return false;
326
    }
327 328 329 330 331
    return other is BoxDecoration
        && other.color == color
        && other.image == image
        && other.border == border
        && other.borderRadius == borderRadius
332
        && listEquals<BoxShadow>(other.boxShadow, boxShadow)
333
        && other.gradient == gradient
334
        && other.backgroundBlendMode == backgroundBlendMode
335
        && other.shape == shape;
336 337
  }

338
  @override
339 340 341 342 343 344 345
  int get hashCode => Object.hash(
    color,
    image,
    border,
    borderRadius,
    boxShadow == null ? null : Object.hashAll(boxShadow!),
    gradient,
346
    backgroundBlendMode,
347 348
    shape,
  );
349

350
  @override
351 352 353 354 355 356
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties
      ..defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.whitespace
      ..emptyBodyDescription = '<no decorations specified>';

357
    properties.add(ColorProperty('color', color, defaultValue: null));
358 359 360 361 362 363
    properties.add(DiagnosticsProperty<DecorationImage>('image', image, defaultValue: null));
    properties.add(DiagnosticsProperty<BoxBorder>('border', border, defaultValue: null));
    properties.add(DiagnosticsProperty<BorderRadiusGeometry>('borderRadius', borderRadius, defaultValue: null));
    properties.add(IterableProperty<BoxShadow>('boxShadow', boxShadow, defaultValue: null, style: DiagnosticsTreeStyle.whitespace));
    properties.add(DiagnosticsProperty<Gradient>('gradient', gradient, defaultValue: null));
    properties.add(EnumProperty<BoxShape>('shape', shape, defaultValue: BoxShape.rectangle));
364
  }
365

366
  @override
367
  bool hitTest(Size size, Offset position, { TextDirection? textDirection }) {
368
    assert(shape != null);
369
    assert((Offset.zero & size).contains(position));
370 371 372
    switch (shape) {
      case BoxShape.rectangle:
        if (borderRadius != null) {
373
          final RRect bounds = borderRadius!.resolve(textDirection).toRRect(Offset.zero & size);
374 375 376 377 378
          return bounds.contains(position);
        }
        return true;
      case BoxShape.circle:
        // Circles are inscribed into our smallest dimension.
379
        final Offset center = size.center(Offset.zero);
380
        final double distance = (position - center).distance;
381 382 383 384
        return distance <= math.min(size.width, size.height) / 2.0;
    }
  }

385
  @override
386
  BoxPainter createBoxPainter([ VoidCallback? onChanged ]) {
387
    assert(onChanged != null || image == null);
388
    return _BoxDecorationPainter(this, onChanged);
389
  }
390 391
}

Florian Loitsch's avatar
Florian Loitsch committed
392
/// An object that paints a [BoxDecoration] into a canvas.
393
class _BoxDecorationPainter extends BoxPainter {
394 395
  _BoxDecorationPainter(this._decoration, super.onChanged)
    : assert(_decoration != null);
396

397
  final BoxDecoration _decoration;
398

399 400 401
  Paint? _cachedBackgroundPaint;
  Rect? _rectForCachedBackgroundPaint;
  Paint _getBackgroundPaint(Rect rect, TextDirection? textDirection) {
402
    assert(rect != null);
403 404
    assert(_decoration.gradient != null || _rectForCachedBackgroundPaint == null);

405 406
    if (_cachedBackgroundPaint == null ||
        (_decoration.gradient != null && _rectForCachedBackgroundPaint != rect)) {
407
      final Paint paint = Paint();
408
      if (_decoration.backgroundBlendMode != null) {
409
        paint.blendMode = _decoration.backgroundBlendMode!;
410 411
      }
      if (_decoration.color != null) {
412
        paint.color = _decoration.color!;
413
      }
414
      if (_decoration.gradient != null) {
415
        paint.shader = _decoration.gradient!.createShader(rect, textDirection: textDirection);
416 417
        _rectForCachedBackgroundPaint = rect;
      }
418 419 420
      _cachedBackgroundPaint = paint;
    }

421
    return _cachedBackgroundPaint!;
422 423
  }

424
  void _paintBox(Canvas canvas, Rect rect, Paint paint, TextDirection? textDirection) {
Hans Muller's avatar
Hans Muller committed
425
    switch (_decoration.shape) {
426
      case BoxShape.circle:
Hans Muller's avatar
Hans Muller committed
427
        assert(_decoration.borderRadius == null);
428
        final Offset center = rect.center;
429
        final double radius = rect.shortestSide / 2.0;
Hans Muller's avatar
Hans Muller committed
430 431
        canvas.drawCircle(center, radius, paint);
        break;
432
      case BoxShape.rectangle:
433
        if (_decoration.borderRadius == null || _decoration.borderRadius == BorderRadius.zero) {
Hans Muller's avatar
Hans Muller committed
434 435
          canvas.drawRect(rect, paint);
        } else {
436
          canvas.drawRRect(_decoration.borderRadius!.resolve(textDirection).toRRect(rect), paint);
Hans Muller's avatar
Hans Muller committed
437 438
        }
        break;
439 440 441
    }
  }

442
  void _paintShadows(Canvas canvas, Rect rect, TextDirection? textDirection) {
443
    if (_decoration.boxShadow == null) {
Hans Muller's avatar
Hans Muller committed
444
      return;
445
    }
446
    for (final BoxShadow boxShadow in _decoration.boxShadow!) {
447
      final Paint paint = boxShadow.toPaint();
Hans Muller's avatar
Hans Muller committed
448
      final Rect bounds = rect.shift(boxShadow.offset).inflate(boxShadow.spreadRadius);
449
      _paintBox(canvas, bounds, paint, textDirection);
Hans Muller's avatar
Hans Muller committed
450 451 452
    }
  }

453
  void _paintBackgroundColor(Canvas canvas, Rect rect, TextDirection? textDirection) {
454
    if (_decoration.color != null || _decoration.gradient != null) {
455
      _paintBox(canvas, rect, _getBackgroundPaint(rect, textDirection), textDirection);
456
    }
Hans Muller's avatar
Hans Muller committed
457 458
  }

459
  DecorationImagePainter? _imagePainter;
460
  void _paintBackgroundImage(Canvas canvas, Rect rect, ImageConfiguration configuration) {
461
    if (_decoration.image == null) {
462
      return;
463
    }
464 465
    _imagePainter ??= _decoration.image!.createPainter(onChanged!);
    Path? clipPath;
466 467
    switch (_decoration.shape) {
      case BoxShape.circle:
468 469 470 471 472
        assert(_decoration.borderRadius == null);
        final Offset center = rect.center;
        final double radius = rect.shortestSide / 2.0;
        final Rect square = Rect.fromCircle(center: center, radius: radius);
        clipPath = Path()..addOval(square);
473 474
        break;
      case BoxShape.rectangle:
475
        if (_decoration.borderRadius != null) {
476
          clipPath = Path()..addRRect(_decoration.borderRadius!.resolve(configuration.textDirection).toRRect(rect));
477
        }
478
        break;
479
    }
480
    _imagePainter!.paint(canvas, rect, clipPath, configuration);
481 482 483 484
  }

  @override
  void dispose() {
485
    _imagePainter?.dispose();
486 487 488
    super.dispose();
  }

489
  /// Paint the box decoration into the given location on the given canvas.
490
  @override
491 492 493
  void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
    assert(configuration != null);
    assert(configuration.size != null);
494 495
    final Rect rect = offset & configuration.size!;
    final TextDirection? textDirection = configuration.textDirection;
496 497
    _paintShadows(canvas, rect, textDirection);
    _paintBackgroundColor(canvas, rect, textDirection);
498
    _paintBackgroundImage(canvas, rect, configuration);
499 500 501 502
    _decoration.border?.paint(
      canvas,
      rect,
      shape: _decoration.shape,
nt4f04uNd's avatar
nt4f04uNd committed
503
      borderRadius: _decoration.borderRadius?.resolve(textDirection),
Ian Hickson's avatar
Ian Hickson committed
504
      textDirection: configuration.textDirection,
505
    );
506
  }
507 508 509 510 511

  @override
  String toString() {
    return 'BoxPainter for $_decoration';
  }
512
}