box_decoration.dart 15.6 KB
Newer Older
1 2 3 4 5 6
// Copyright 2015 The Chromium Authors. All rights reserved.
// 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 'decoration.dart';
14
import 'decoration_image.dart';
15
import 'edge_insets.dart';
16
import 'gradient.dart';
17
import 'image_provider.dart';
18

Florian Loitsch's avatar
Florian Loitsch committed
19
/// An immutable description of how to paint a box.
20
///
Ian Hickson's avatar
Ian Hickson committed
21 22
/// The [BoxDecoration] class provides a variety of ways to draw a box.
///
23
/// The box has a [border], a body, and may cast a [boxShadow].
Ian Hickson's avatar
Ian Hickson committed
24 25 26 27 28 29 30 31 32
///
/// 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.
///
33
/// The [border] paints over the body; the [boxShadow], naturally, paints below it.
Ian Hickson's avatar
Ian Hickson committed
34
///
35
/// {@tool sample}
Ian Hickson's avatar
Ian Hickson committed
36
///
37
/// The following example uses the [Container] widget from the widgets layer to
38
/// draw an image with a border:
39 40
///
/// ```dart
41 42
/// Container(
///   decoration: BoxDecoration(
43
///     color: const Color(0xff7c94b6),
44 45
///     image: DecorationImage(
///       image: ExactAssetImage('images/flowers.jpeg'),
46
///       fit: BoxFit.cover,
47
///     ),
48
///     border: Border.all(
49 50 51 52 53 54
///       color: Colors.black,
///       width: 8.0,
///     ),
///   ),
/// )
/// ```
55
/// {@end-tool}
Ian Hickson's avatar
Ian Hickson committed
56
///
57 58 59 60 61 62 63
/// {@template flutter.painting.boxDecoration.clip}
/// 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
64 65 66 67 68 69
/// 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.
70
class BoxDecoration extends Decoration {
71 72
  /// Creates a box decoration.
  ///
73 74
  /// * If [color] is null, this decoration does not paint a background color.
  /// * If [image] is null, this decoration does not paint a background image.
75
  /// * If [border] is null, this decoration does not paint a border.
76
  /// * If [borderRadius] is null, this decoration uses more efficient background
77
  ///   painting commands. The [borderRadius] argument must be null if [shape] is
78 79 80
  ///   [BoxShape.circle].
  /// * If [boxShadow] is null, this decoration does not paint a shadow.
  /// * If [gradient] is null, this decoration does not paint gradients.
81
  /// * If [backgroundBlendMode] is null, this decoration paints with [BlendMode.srcOver]
82 83
  ///
  /// The [shape] argument must not be null.
84
  const BoxDecoration({
85 86
    this.color,
    this.image,
87 88 89 90
    this.border,
    this.borderRadius,
    this.boxShadow,
    this.gradient,
91
    this.backgroundBlendMode,
92
    this.shape = BoxShape.rectangle,
93 94
  }) : assert(shape != null),
       assert(
95
         backgroundBlendMode == null || color != null || gradient != null,
96
         'backgroundBlendMode applies to BoxDecoration\'s background color or '
97
         'gradient, but no color or gradient was provided.'
98
       );
99

100
  @override
101
  bool debugAssertIsValid() {
102
    assert(shape != BoxShape.circle ||
103
          borderRadius == null); // Can't have a border radius if you're a circle.
104
    return super.debugAssertIsValid();
105 106
  }

Florian Loitsch's avatar
Florian Loitsch committed
107
  /// The color to fill in the background of the box.
108
  ///
109 110 111 112 113 114
  /// 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].
115
  final Color color;
116

117 118 119 120 121
  /// 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.
122
  final DecorationImage image;
123

124 125 126
  /// A border to draw above the background [color], [gradient], or [image].
  ///
  /// Follows the [shape] and [borderRadius].
Ian Hickson's avatar
Ian Hickson committed
127 128 129 130 131 132 133 134
  ///
  /// 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.
  final BoxBorder border;
135

136
  /// If non-null, the corners of this box are rounded by this [BorderRadius].
137
  ///
138 139
  /// Applies only to boxes with rectangular shapes; ignored if [shape] is not
  /// [BoxShape.rectangle].
140 141
  ///
  /// {@macro flutter.painting.boxDecoration.clip}
142
  final BorderRadiusGeometry borderRadius;
143

144 145 146
  /// A list of shadows cast by this box behind the box.
  ///
  /// The shadow follows the [shape] of the box.
147
  final List<BoxShadow> boxShadow;
148

149
  /// A gradient to use when filling the box.
150 151 152 153
  ///
  /// If this is specified, [color] has no effect.
  ///
  /// The [gradient] is drawn under the [image].
154
  final Gradient gradient;
155

156 157
  /// The blend mode applied to the [color] or [gradient] background of the box.
  ///
158
  /// If no [backgroundBlendMode] is provided then the default painting blend
159 160
  /// mode is used.
  ///
161
  /// If no [color] or [gradient] is provided then the blend mode has no impact.
162 163
  final BlendMode backgroundBlendMode;

164 165 166
  /// The shape to fill the background [color], [gradient], and [image] into and
  /// to cast as the [boxShadow].
  ///
167 168 169 170 171 172 173 174
  /// 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].
175 176
  ///
  /// {@macro flutter.painting.boxDecoration.clip}
177
  final BoxShape shape;
178

179
  @override
Ian Hickson's avatar
Ian Hickson committed
180
  EdgeInsetsGeometry get padding => border?.dimensions;
181

Florian Loitsch's avatar
Florian Loitsch committed
182
  /// Returns a new box decoration that is scaled by the given factor.
183
  BoxDecoration scale(double factor) {
184
    return BoxDecoration(
185
      color: Color.lerp(null, color, factor),
186
      image: image, // TODO(ianh): fade the image from transparent
Ian Hickson's avatar
Ian Hickson committed
187
      border: BoxBorder.lerp(null, border, factor),
188
      borderRadius: BorderRadiusGeometry.lerp(null, borderRadius, factor),
189
      boxShadow: BoxShadow.lerpList(null, boxShadow, factor),
190
      gradient: gradient?.scale(factor),
191
      shape: shape,
192 193 194
    );
  }

195 196 197
  @override
  bool get isComplex => boxShadow != null;

198 199
  @override
  BoxDecoration lerpFrom(Decoration a, double t) {
200 201
    if (a == null)
      return scale(t);
202 203 204 205 206 207 208
    if (a is BoxDecoration)
      return BoxDecoration.lerp(a, this, t);
    return super.lerpFrom(a, t);
  }

  @override
  BoxDecoration lerpTo(Decoration b, double t) {
209 210
    if (b == null)
      return scale(1.0 - t);
211 212 213 214 215
    if (b is BoxDecoration)
      return BoxDecoration.lerp(this, b, t);
    return super.lerpTo(b, t);
  }

216
  /// Linearly interpolate between two box decorations.
217 218
  ///
  /// Interpolates each parameter of the box decoration separately.
219
  ///
220 221 222 223 224 225 226 227 228 229
  /// 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.
  ///
230
  /// {@macro dart.ui.shadow.lerp}
231
  ///
232 233 234 235 236
  /// 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]
237
  ///    and which use [BoxDecoration.lerp] when interpolating two
238
  ///    [BoxDecoration]s or a [BoxDecoration] to or from null.
239
  static BoxDecoration lerp(BoxDecoration a, BoxDecoration b, double t) {
240
    assert(t != null);
241 242 243 244 245 246
    if (a == null && b == null)
      return null;
    if (a == null)
      return b.scale(t);
    if (b == null)
      return a.scale(1.0 - t);
247 248 249 250
    if (t == 0.0)
      return a;
    if (t == 1.0)
      return b;
251
    return BoxDecoration(
252
      color: Color.lerp(a.color, b.color, t),
253
      image: t < 0.5 ? a.image : b.image, // TODO(ianh): cross-fade the image
Ian Hickson's avatar
Ian Hickson committed
254
      border: BoxBorder.lerp(a.border, b.border, t),
255
      borderRadius: BorderRadiusGeometry.lerp(a.borderRadius, b.borderRadius, t),
256
      boxShadow: BoxShadow.lerpList(a.boxShadow, b.boxShadow, t),
257
      gradient: Gradient.lerp(a.gradient, b.gradient, t),
258
      shape: t < 0.5 ? a.shape : b.shape,
259 260 261
    );
  }

262
  @override
263 264 265
  bool operator ==(dynamic other) {
    if (identical(this, other))
      return true;
266
    if (runtimeType != other.runtimeType)
267 268
      return false;
    final BoxDecoration typedOther = other;
269 270
    return color == typedOther.color &&
           image == typedOther.image &&
271 272 273 274 275 276 277
           border == typedOther.border &&
           borderRadius == typedOther.borderRadius &&
           boxShadow == typedOther.boxShadow &&
           gradient == typedOther.gradient &&
           shape == typedOther.shape;
  }

278
  @override
279
  int get hashCode {
280
    return hashValues(
281 282
      color,
      image,
283 284 285 286
      border,
      borderRadius,
      boxShadow,
      gradient,
287
      shape,
288
    );
289 290
  }

291
  @override
292 293 294 295 296 297
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties
      ..defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.whitespace
      ..emptyBodyDescription = '<no decorations specified>';

298 299 300 301 302 303 304
    properties.add(DiagnosticsProperty<Color>('color', color, defaultValue: null));
    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));
305
  }
306

307
  @override
308
  bool hitTest(Size size, Offset position, { TextDirection textDirection }) {
309
    assert(shape != null);
310
    assert((Offset.zero & size).contains(position));
311 312 313
    switch (shape) {
      case BoxShape.rectangle:
        if (borderRadius != null) {
314
          final RRect bounds = borderRadius.resolve(textDirection).toRRect(Offset.zero & size);
315 316 317 318 319
          return bounds.contains(position);
        }
        return true;
      case BoxShape.circle:
        // Circles are inscribed into our smallest dimension.
320
        final Offset center = size.center(Offset.zero);
321
        final double distance = (position - center).distance;
322 323
        return distance <= math.min(size.width, size.height) / 2.0;
    }
pq's avatar
pq committed
324
    assert(shape != null);
pq's avatar
pq committed
325
    return null;
326 327
  }

328
  @override
329
  _BoxDecorationPainter createBoxPainter([ VoidCallback onChanged ]) {
330
    assert(onChanged != null || image == null);
331
    return _BoxDecorationPainter(this, onChanged);
332
  }
333 334
}

Florian Loitsch's avatar
Florian Loitsch committed
335
/// An object that paints a [BoxDecoration] into a canvas.
336
class _BoxDecorationPainter extends BoxPainter {
337
  _BoxDecorationPainter(this._decoration, VoidCallback onChanged)
338
    : assert(_decoration != null),
339
      super(onChanged);
340

341
  final BoxDecoration _decoration;
342 343

  Paint _cachedBackgroundPaint;
344
  Rect _rectForCachedBackgroundPaint;
345
  Paint _getBackgroundPaint(Rect rect, TextDirection textDirection) {
346
    assert(rect != null);
347 348
    assert(_decoration.gradient != null || _rectForCachedBackgroundPaint == null);

349 350
    if (_cachedBackgroundPaint == null ||
        (_decoration.gradient != null && _rectForCachedBackgroundPaint != rect)) {
351
      final Paint paint = Paint();
352 353
      if (_decoration.backgroundBlendMode != null)
        paint.blendMode = _decoration.backgroundBlendMode;
354 355
      if (_decoration.color != null)
        paint.color = _decoration.color;
356
      if (_decoration.gradient != null) {
357
        paint.shader = _decoration.gradient.createShader(rect, textDirection: textDirection);
358 359
        _rectForCachedBackgroundPaint = rect;
      }
360 361 362 363 364 365
      _cachedBackgroundPaint = paint;
    }

    return _cachedBackgroundPaint;
  }

366
  void _paintBox(Canvas canvas, Rect rect, Paint paint, TextDirection textDirection) {
Hans Muller's avatar
Hans Muller committed
367
    switch (_decoration.shape) {
368
      case BoxShape.circle:
Hans Muller's avatar
Hans Muller committed
369
        assert(_decoration.borderRadius == null);
370
        final Offset center = rect.center;
371
        final double radius = rect.shortestSide / 2.0;
Hans Muller's avatar
Hans Muller committed
372 373
        canvas.drawCircle(center, radius, paint);
        break;
374
      case BoxShape.rectangle:
Hans Muller's avatar
Hans Muller committed
375 376 377
        if (_decoration.borderRadius == null) {
          canvas.drawRect(rect, paint);
        } else {
378
          canvas.drawRRect(_decoration.borderRadius.resolve(textDirection).toRRect(rect), paint);
Hans Muller's avatar
Hans Muller committed
379 380
        }
        break;
381 382 383
    }
  }

384
  void _paintShadows(Canvas canvas, Rect rect, TextDirection textDirection) {
Hans Muller's avatar
Hans Muller committed
385 386 387
    if (_decoration.boxShadow == null)
      return;
    for (BoxShadow boxShadow in _decoration.boxShadow) {
388
      final Paint paint = boxShadow.toPaint();
Hans Muller's avatar
Hans Muller committed
389
      final Rect bounds = rect.shift(boxShadow.offset).inflate(boxShadow.spreadRadius);
390
      _paintBox(canvas, bounds, paint, textDirection);
Hans Muller's avatar
Hans Muller committed
391 392 393
    }
  }

394
  void _paintBackgroundColor(Canvas canvas, Rect rect, TextDirection textDirection) {
395
    if (_decoration.color != null || _decoration.gradient != null)
396
      _paintBox(canvas, rect, _getBackgroundPaint(rect, textDirection), textDirection);
Hans Muller's avatar
Hans Muller committed
397 398
  }

399
  DecorationImagePainter _imagePainter;
400
  void _paintBackgroundImage(Canvas canvas, Rect rect, ImageConfiguration configuration) {
401
    if (_decoration.image == null)
402
      return;
403
    _imagePainter ??= _decoration.image.createPainter(onChanged);
404
    Path clipPath;
405 406
    switch (_decoration.shape) {
      case BoxShape.circle:
407
        clipPath = Path()..addOval(rect);
408 409 410
        break;
      case BoxShape.rectangle:
        if (_decoration.borderRadius != null)
411
          clipPath = Path()..addRRect(_decoration.borderRadius.resolve(configuration.textDirection).toRRect(rect));
412
        break;
413
    }
414
    _imagePainter.paint(canvas, rect, clipPath, configuration);
415 416 417 418
  }

  @override
  void dispose() {
419
    _imagePainter?.dispose();
420 421 422
    super.dispose();
  }

423
  /// Paint the box decoration into the given location on the given canvas
424
  @override
425 426 427 428
  void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
    assert(configuration != null);
    assert(configuration.size != null);
    final Rect rect = offset & configuration.size;
429 430 431
    final TextDirection textDirection = configuration.textDirection;
    _paintShadows(canvas, rect, textDirection);
    _paintBackgroundColor(canvas, rect, textDirection);
432
    _paintBackgroundImage(canvas, rect, configuration);
433 434 435 436
    _decoration.border?.paint(
      canvas,
      rect,
      shape: _decoration.shape,
Ian Hickson's avatar
Ian Hickson committed
437 438
      borderRadius: _decoration.borderRadius,
      textDirection: configuration.textDirection,
439
    );
440
  }
441 442 443 444 445

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