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

import 'package:flutter/foundation.dart';

import 'basic_types.dart';
import 'borders.dart';
import 'box_border.dart';
import 'box_decoration.dart';
import 'box_shadow.dart';
import 'circle_border.dart';
13
import 'colors.dart';
14
import 'decoration.dart';
15
import 'decoration_image.dart';
16 17
import 'edge_insets.dart';
import 'gradient.dart';
18
import 'image_provider.dart';
19 20 21 22 23 24 25 26
import 'rounded_rectangle_border.dart';

/// An immutable description of how to paint an arbitrary shape.
///
/// The [ShapeDecoration] class provides a way to draw a [ShapeBorder],
/// optionally filling it with a color or a gradient, optionally painting an
/// image into it, and optionally casting a shadow.
///
27
/// {@tool snippet}
28 29 30 31 32 33
///
/// The following example uses the [Container] widget from the widgets layer to
/// draw a white rectangle with a 24-pixel multicolor outline, with the text
/// "RGB" inside it:
///
/// ```dart
34 35
/// Container(
///   decoration: ShapeDecoration(
36
///     color: Colors.white,
37
///     shape: Border.all(
38 39
///       color: Colors.red,
///       width: 8.0,
40
///     ) + Border.all(
41 42
///       color: Colors.green,
///       width: 8.0,
43
///     ) + Border.all(
44 45 46 47 48 49 50
///       color: Colors.blue,
///       width: 8.0,
///     ),
///   ),
///   child: const Text('RGB', textAlign: TextAlign.center),
/// )
/// ```
51
/// {@end-tool}
52 53 54 55 56
///
/// See also:
///
///  * [DecoratedBox] and [Container], widgets that can be configured with
///    [ShapeDecoration] objects.
57
///  * [BoxDecoration], a similar [Decoration] that is optimized for rectangles
58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
///    specifically.
///  * [ShapeBorder], the base class for the objects that are used in the
///    [shape] property.
class ShapeDecoration extends Decoration {
  /// Creates a shape decoration.
  ///
  /// * If [color] is null, this decoration does not paint a background color.
  /// * If [gradient] is null, this decoration does not paint gradients.
  /// * If [image] is null, this decoration does not paint a background image.
  /// * If [shadows] is null, this decoration does not paint a shadow.
  ///
  /// The [color] and [gradient] properties are mutually exclusive, one (or
  /// both) of them must be null.
  ///
  /// The [shape] must not be null.
  const ShapeDecoration({
    this.color,
    this.image,
    this.gradient,
    this.shadows,
78
    required this.shape,
79 80 81 82 83 84 85 86 87 88 89 90 91 92
  }) : assert(!(color != null && gradient != null)),
       assert(shape != null);

  /// Creates a shape decoration configured to match a [BoxDecoration].
  ///
  /// The [BoxDecoration] class is more efficient for shapes that it can
  /// describe than the [ShapeDecoration] class is for those same shapes,
  /// because [ShapeDecoration] has to be more general as it can support any
  /// shape. However, having a [ShapeDecoration] is sometimes necessary, for
  /// example when calling [ShapeDecoration.lerp] to transition between
  /// different shapes (e.g. from a [CircleBorder] to a
  /// [RoundedRectangleBorder]; the [BoxDecoration] class cannot animate the
  /// transition from a [BoxShape.circle] to [BoxShape.rectangle]).
  factory ShapeDecoration.fromBoxDecoration(BoxDecoration source) {
93
    final ShapeBorder shape;
94 95 96 97
    assert(source.shape != null);
    switch (source.shape) {
      case BoxShape.circle:
        if (source.border != null) {
98 99
          assert(source.border!.isUniform);
          shape = CircleBorder(side: source.border!.top);
100 101 102 103 104 105
        } else {
          shape = const CircleBorder();
        }
        break;
      case BoxShape.rectangle:
        if (source.borderRadius != null) {
106
          assert(source.border == null || source.border!.isUniform);
107
          shape = RoundedRectangleBorder(
108
            side: source.border?.top ?? BorderSide.none,
109
            borderRadius: source.borderRadius!,
110 111 112 113 114 115
          );
        } else {
          shape = source.border ?? const Border();
        }
        break;
    }
116
    return ShapeDecoration(
117 118 119 120 121 122 123 124
      color: source.color,
      image: source.image,
      gradient: source.gradient,
      shadows: source.boxShadow,
      shape: shape,
    );
  }

125 126 127 128 129
  @override
  Path getClipPath(Rect rect, TextDirection textDirection) {
    return shape.getOuterPath(rect, textDirection: textDirection);
  }

130 131 132 133 134
  /// The color to fill in the background of the shape.
  ///
  /// The color is under the [image].
  ///
  /// If a [gradient] is specified, [color] must be null.
135
  final Color? color;
136 137 138 139 140 141

  /// A gradient to use when filling the shape.
  ///
  /// The gradient is under the [image].
  ///
  /// If a [color] is specified, [gradient] must be null.
142
  final Gradient? gradient;
143 144 145 146

  /// An image to paint inside the shape (clipped to its outline).
  ///
  /// The image is drawn over the [color] or [gradient].
147
  final DecorationImage? image;
148

Ian Hickson's avatar
Ian Hickson committed
149 150 151 152 153 154 155
  /// A list of shadows cast by the [shape].
  ///
  /// See also:
  ///
  ///  * [kElevationToShadow], for some predefined shadows used in Material
  ///    Design.
  ///  * [PhysicalModel], a widget for showing shadows.
156
  final List<BoxShadow>? shadows;
157 158 159 160 161 162 163 164 165

  /// The shape to fill the [color], [gradient], and [image] into and to cast as
  /// the [shadows].
  ///
  /// Shapes can be stacked (using the `+` operator). The color, gradient, and
  /// image are drawn into the inner-most shape specified.
  ///
  /// The [shape] property specifies the outline (border) of the decoration. The
  /// shape must not be null.
166 167 168 169 170 171 172 173 174 175 176 177 178 179
  ///
  /// ## Directionality-dependent shapes
  ///
  /// Some [ShapeBorder] subclasses are sensitive to the [TextDirection]. The
  /// direction that is provided to the border (e.g. for its [ShapeBorder.paint]
  /// method) is the one specified in the [ImageConfiguration]
  /// ([ImageConfiguration.textDirection]) provided to the [BoxPainter] (via its
  /// [BoxPainter.paint method). The [BoxPainter] is obtained when
  /// [createBoxPainter] is called.
  ///
  /// When a [ShapeDecoration] is used with a [Container] widget or a
  /// [DecoratedBox] widget (which is what [Container] uses), the
  /// [TextDirection] specified in the [ImageConfiguration] is obtained from the
  /// ambient [Directionality], using [createLocalImageConfiguration].
180 181 182 183 184 185
  final ShapeBorder shape;

  /// The inset space occupied by the [shape]'s border.
  ///
  /// This value may be misleading. See the discussion at [ShapeBorder.dimensions].
  @override
186
  EdgeInsetsGeometry get padding => shape.dimensions;
187 188 189 190 191

  @override
  bool get isComplex => shadows != null;

  @override
192
  ShapeDecoration? lerpFrom(Decoration? a, double t) {
193
    if (a is BoxDecoration) {
194
      return ShapeDecoration.lerp(ShapeDecoration.fromBoxDecoration(a), this, t);
195
    } else if (a == null || a is ShapeDecoration) {
196
      return ShapeDecoration.lerp(a as ShapeDecoration?, this, t);
197
    }
198
    return super.lerpFrom(a, t) as ShapeDecoration?;
199 200 201
  }

  @override
202
  ShapeDecoration? lerpTo(Decoration? b, double t) {
203
    if (b is BoxDecoration) {
204
      return ShapeDecoration.lerp(this, ShapeDecoration.fromBoxDecoration(b), t);
205
    } else if (b == null || b is ShapeDecoration) {
206
      return ShapeDecoration.lerp(this, b as ShapeDecoration?, t);
207
    }
208
    return super.lerpTo(b, t) as ShapeDecoration?;
209 210 211 212 213 214 215
  }

  /// Linearly interpolate between two shapes.
  ///
  /// Interpolates each parameter of the decoration separately.
  ///
  /// If both values are null, this returns null. Otherwise, it returns a
216
  /// non-null value, with null arguments treated like a [ShapeDecoration] whose
217 218 219
  /// fields are all null (including the [shape], which cannot normally be
  /// null).
  ///
220
  /// {@macro dart.ui.shadow.lerp}
221
  ///
222 223 224 225 226 227 228
  /// See also:
  ///
  ///  * [Decoration.lerp], which can interpolate between any two types of
  ///    [Decoration]s, not just [ShapeDecoration]s.
  ///  * [lerpFrom] and [lerpTo], which are used to implement [Decoration.lerp]
  ///    and which use [ShapeDecoration.lerp] when interpolating two
  ///    [ShapeDecoration]s or a [ShapeDecoration] to or from null.
229
  static ShapeDecoration? lerp(ShapeDecoration? a, ShapeDecoration? b, double t) {
230
    assert(t != null);
231
    if (a == null && b == null) {
232
      return null;
233
    }
234
    if (a != null && b != null) {
235
      if (t == 0.0) {
236
        return a;
237 238
      }
      if (t == 1.0) {
239
        return b;
240
      }
241
    }
242
    return ShapeDecoration(
243 244
      color: Color.lerp(a?.color, b?.color, t),
      gradient: Gradient.lerp(a?.gradient, b?.gradient, t),
245
      image: t < 0.5 ? a!.image : b!.image, // TODO(ianh): cross-fade the image
246
      shadows: BoxShadow.lerpList(a?.shadows, b?.shadows, t),
247
      shape: ShapeBorder.lerp(a?.shape, b?.shape, t)!,
248 249 250 251
    );
  }

  @override
252
  bool operator ==(Object other) {
253
    if (identical(this, other)) {
254
      return true;
255 256
    }
    if (other.runtimeType != runtimeType) {
257
      return false;
258
    }
259 260 261 262
    return other is ShapeDecoration
        && other.color == color
        && other.gradient == gradient
        && other.image == image
263
        && listEquals<BoxShadow>(other.shadows, shadows)
264
        && other.shape == shape;
265 266 267
  }

  @override
268 269 270 271 272 273 274
  int get hashCode => Object.hash(
    color,
    gradient,
    image,
    shape,
    shadows == null ? null : Object.hashAll(shadows!),
  );
275 276 277 278 279

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.whitespace;
280
    properties.add(ColorProperty('color', color, defaultValue: null));
281 282 283 284
    properties.add(DiagnosticsProperty<Gradient>('gradient', gradient, defaultValue: null));
    properties.add(DiagnosticsProperty<DecorationImage>('image', image, defaultValue: null));
    properties.add(IterableProperty<BoxShadow>('shadows', shadows, defaultValue: null, style: DiagnosticsTreeStyle.whitespace));
    properties.add(DiagnosticsProperty<ShapeBorder>('shape', shape));
285 286 287
  }

  @override
288
  bool hitTest(Size size, Offset position, { TextDirection? textDirection }) {
289 290 291 292
    return shape.getOuterPath(Offset.zero & size, textDirection: textDirection).contains(position);
  }

  @override
293
  BoxPainter createBoxPainter([ VoidCallback? onChanged ]) {
294
    assert(onChanged != null || image == null);
295
    return _ShapeDecorationPainter(this, onChanged!);
296 297 298 299 300
  }
}

/// An object that paints a [ShapeDecoration] into a canvas.
class _ShapeDecorationPainter extends BoxPainter {
301
  _ShapeDecorationPainter(this._decoration, VoidCallback onChanged)
302
    : assert(_decoration != null),
303
      super(onChanged);
304 305 306

  final ShapeDecoration _decoration;

307 308 309 310 311 312
  Rect? _lastRect;
  TextDirection? _lastTextDirection;
  late Path _outerPath;
  Path? _innerPath;
  Paint? _interiorPaint;
  int? _shadowCount;
313
  late List<Rect> _shadowBounds;
314 315
  late List<Path> _shadowPaths;
  late List<Paint> _shadowPaints;
316

317 318 319 320
  @override
  VoidCallback get onChanged => super.onChanged!;

  void _precache(Rect rect, TextDirection? textDirection) {
321
    assert(rect != null);
322
    if (rect == _lastRect && textDirection == _lastTextDirection) {
323
      return;
324
    }
325

326 327 328 329 330
    // We reach here in two cases:
    //  - the very first time we paint, in which case everything except _decoration is null
    //  - subsequent times, if the rect has changed, in which case we only need to update
    //    the features that depend on the actual rect.
    if (_interiorPaint == null && (_decoration.color != null || _decoration.gradient != null)) {
331
      _interiorPaint = Paint();
332
      if (_decoration.color != null) {
333
        _interiorPaint!.color = _decoration.color!;
334
      }
335
    }
336
    if (_decoration.gradient != null) {
337
      _interiorPaint!.shader = _decoration.gradient!.createShader(rect, textDirection: textDirection);
338
    }
339 340
    if (_decoration.shadows != null) {
      if (_shadowCount == null) {
341 342 343 344
        _shadowCount = _decoration.shadows!.length;
        _shadowPaints = <Paint>[
          ..._decoration.shadows!.map((BoxShadow shadow) => shadow.toPaint()),
        ];
345
      }
346 347 348 349 350 351 352 353 354 355 356 357 358
      if (_decoration.shape.preferPaintInterior) {
        _shadowBounds = <Rect>[
          ..._decoration.shadows!.map((BoxShadow shadow) {
            return rect.shift(shadow.offset).inflate(shadow.spreadRadius);
          }),
        ];
      } else {
        _shadowPaths = <Path>[
          ..._decoration.shadows!.map((BoxShadow shadow) {
            return _decoration.shape.getOuterPath(rect.shift(shadow.offset).inflate(shadow.spreadRadius), textDirection: textDirection);
          }),
        ];
      }
359
    }
360
    if (!_decoration.shape.preferPaintInterior && (_interiorPaint != null || _shadowCount != null)) {
361
      _outerPath = _decoration.shape.getOuterPath(rect, textDirection: textDirection);
362 363
    }
    if (_decoration.image != null) {
364
      _innerPath = _decoration.shape.getInnerPath(rect, textDirection: textDirection);
365
    }
366

367
    _lastRect = rect;
368
    _lastTextDirection = textDirection;
369 370
  }

371
  void _paintShadows(Canvas canvas, Rect rect, TextDirection? textDirection) {
372
    if (_shadowCount != null) {
373 374 375 376 377 378 379 380
      if (_decoration.shape.preferPaintInterior) {
        for (int index = 0; index < _shadowCount!; index += 1) {
          _decoration.shape.paintInterior(canvas, _shadowBounds[index], _shadowPaints[index], textDirection: textDirection);
        }
      } else {
        for (int index = 0; index < _shadowCount!; index += 1) {
          canvas.drawPath(_shadowPaths[index], _shadowPaints[index]);
        }
381
      }
382 383 384
    }
  }

385
  void _paintInterior(Canvas canvas, Rect rect, TextDirection? textDirection) {
386
    if (_interiorPaint != null) {
387 388 389 390 391
      if (_decoration.shape.preferPaintInterior) {
        _decoration.shape.paintInterior(canvas, rect, _interiorPaint!, textDirection: textDirection);
      } else {
        canvas.drawPath(_outerPath, _interiorPaint!);
      }
392
    }
393 394
  }

395
  DecorationImagePainter? _imagePainter;
396
  void _paintImage(Canvas canvas, ImageConfiguration configuration) {
397
    if (_decoration.image == null) {
398
      return;
399
    }
400 401
    _imagePainter ??= _decoration.image!.createPainter(onChanged);
    _imagePainter!.paint(canvas, _lastRect!, _innerPath, configuration);
402 403 404 405
  }

  @override
  void dispose() {
406
    _imagePainter?.dispose();
407 408 409 410 411 412 413
    super.dispose();
  }

  @override
  void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
    assert(configuration != null);
    assert(configuration.size != null);
414 415
    final Rect rect = offset & configuration.size!;
    final TextDirection? textDirection = configuration.textDirection;
416
    _precache(rect, textDirection);
417 418
    _paintShadows(canvas, rect, textDirection);
    _paintInterior(canvas, rect, textDirection);
419
    _paintImage(canvas, configuration);
420
    _decoration.shape.paint(canvas, rect, textDirection: textDirection);
421 422
  }
}