| // Copyright 2014 The Flutter 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 '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'; |
| import 'colors.dart'; |
| import 'decoration.dart'; |
| import 'decoration_image.dart'; |
| import 'edge_insets.dart'; |
| import 'gradient.dart'; |
| import 'image_provider.dart'; |
| 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. |
| /// |
| /// {@tool snippet} |
| /// |
| /// 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 |
| /// Container( |
| /// decoration: ShapeDecoration( |
| /// color: Colors.white, |
| /// shape: Border.all( |
| /// color: Colors.red, |
| /// width: 8.0, |
| /// ) + Border.all( |
| /// color: Colors.green, |
| /// width: 8.0, |
| /// ) + Border.all( |
| /// color: Colors.blue, |
| /// width: 8.0, |
| /// ), |
| /// ), |
| /// child: const Text('RGB', textAlign: TextAlign.center), |
| /// ) |
| /// ``` |
| /// {@end-tool} |
| /// |
| /// See also: |
| /// |
| /// * [DecoratedBox] and [Container], widgets that can be configured with |
| /// [ShapeDecoration] objects. |
| /// * [BoxDecoration], a similar [Decoration] that is optimized for rectangles |
| /// 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, |
| required this.shape, |
| }) : 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) { |
| final ShapeBorder shape; |
| assert(source.shape != null); |
| switch (source.shape) { |
| case BoxShape.circle: |
| if (source.border != null) { |
| assert(source.border!.isUniform); |
| shape = CircleBorder(side: source.border!.top); |
| } else { |
| shape = const CircleBorder(); |
| } |
| break; |
| case BoxShape.rectangle: |
| if (source.borderRadius != null) { |
| assert(source.border == null || source.border!.isUniform); |
| shape = RoundedRectangleBorder( |
| side: source.border?.top ?? BorderSide.none, |
| borderRadius: source.borderRadius!, |
| ); |
| } else { |
| shape = source.border ?? const Border(); |
| } |
| break; |
| } |
| return ShapeDecoration( |
| color: source.color, |
| image: source.image, |
| gradient: source.gradient, |
| shadows: source.boxShadow, |
| shape: shape, |
| ); |
| } |
| |
| @override |
| Path getClipPath(Rect rect, TextDirection textDirection) { |
| return shape.getOuterPath(rect, textDirection: textDirection); |
| } |
| |
| /// 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. |
| final Color? color; |
| |
| /// A gradient to use when filling the shape. |
| /// |
| /// The gradient is under the [image]. |
| /// |
| /// If a [color] is specified, [gradient] must be null. |
| final Gradient? gradient; |
| |
| /// An image to paint inside the shape (clipped to its outline). |
| /// |
| /// The image is drawn over the [color] or [gradient]. |
| final DecorationImage? image; |
| |
| /// 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. |
| final List<BoxShadow>? shadows; |
| |
| /// 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. |
| /// |
| /// ## 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]. |
| final ShapeBorder shape; |
| |
| /// The inset space occupied by the [shape]'s border. |
| /// |
| /// This value may be misleading. See the discussion at [ShapeBorder.dimensions]. |
| @override |
| EdgeInsetsGeometry get padding => shape.dimensions; |
| |
| @override |
| bool get isComplex => shadows != null; |
| |
| @override |
| ShapeDecoration? lerpFrom(Decoration? a, double t) { |
| if (a is BoxDecoration) { |
| return ShapeDecoration.lerp(ShapeDecoration.fromBoxDecoration(a), this, t); |
| } else if (a == null || a is ShapeDecoration) { |
| return ShapeDecoration.lerp(a as ShapeDecoration?, this, t); |
| } |
| return super.lerpFrom(a, t) as ShapeDecoration?; |
| } |
| |
| @override |
| ShapeDecoration? lerpTo(Decoration? b, double t) { |
| if (b is BoxDecoration) { |
| return ShapeDecoration.lerp(this, ShapeDecoration.fromBoxDecoration(b), t); |
| } else if (b == null || b is ShapeDecoration) { |
| return ShapeDecoration.lerp(this, b as ShapeDecoration?, t); |
| } |
| return super.lerpTo(b, t) as ShapeDecoration?; |
| } |
| |
| /// Linearly interpolate between two shapes. |
| /// |
| /// Interpolates each parameter of the decoration separately. |
| /// |
| /// If both values are null, this returns null. Otherwise, it returns a |
| /// non-null value, with null arguments treated like a [ShapeDecoration] whose |
| /// fields are all null (including the [shape], which cannot normally be |
| /// null). |
| /// |
| /// {@macro dart.ui.shadow.lerp} |
| /// |
| /// 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. |
| static ShapeDecoration? lerp(ShapeDecoration? a, ShapeDecoration? b, double t) { |
| assert(t != null); |
| if (a == null && b == null) { |
| return null; |
| } |
| if (a != null && b != null) { |
| if (t == 0.0) { |
| return a; |
| } |
| if (t == 1.0) { |
| return b; |
| } |
| } |
| return ShapeDecoration( |
| color: Color.lerp(a?.color, b?.color, t), |
| gradient: Gradient.lerp(a?.gradient, b?.gradient, t), |
| image: t < 0.5 ? a!.image : b!.image, // TODO(ianh): cross-fade the image |
| shadows: BoxShadow.lerpList(a?.shadows, b?.shadows, t), |
| shape: ShapeBorder.lerp(a?.shape, b?.shape, t)!, |
| ); |
| } |
| |
| @override |
| bool operator ==(Object other) { |
| if (identical(this, other)) { |
| return true; |
| } |
| if (other.runtimeType != runtimeType) { |
| return false; |
| } |
| return other is ShapeDecoration |
| && other.color == color |
| && other.gradient == gradient |
| && other.image == image |
| && listEquals<BoxShadow>(other.shadows, shadows) |
| && other.shape == shape; |
| } |
| |
| @override |
| int get hashCode => Object.hash( |
| color, |
| gradient, |
| image, |
| shape, |
| shadows == null ? null : Object.hashAll(shadows!), |
| ); |
| |
| @override |
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| super.debugFillProperties(properties); |
| properties.defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.whitespace; |
| properties.add(ColorProperty('color', color, defaultValue: null)); |
| 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)); |
| } |
| |
| @override |
| bool hitTest(Size size, Offset position, { TextDirection? textDirection }) { |
| return shape.getOuterPath(Offset.zero & size, textDirection: textDirection).contains(position); |
| } |
| |
| @override |
| BoxPainter createBoxPainter([ VoidCallback? onChanged ]) { |
| assert(onChanged != null || image == null); |
| return _ShapeDecorationPainter(this, onChanged!); |
| } |
| } |
| |
| /// An object that paints a [ShapeDecoration] into a canvas. |
| class _ShapeDecorationPainter extends BoxPainter { |
| _ShapeDecorationPainter(this._decoration, VoidCallback onChanged) |
| : assert(_decoration != null), |
| super(onChanged); |
| |
| final ShapeDecoration _decoration; |
| |
| Rect? _lastRect; |
| TextDirection? _lastTextDirection; |
| late Path _outerPath; |
| Path? _innerPath; |
| Paint? _interiorPaint; |
| int? _shadowCount; |
| late List<Rect> _shadowBounds; |
| late List<Path> _shadowPaths; |
| late List<Paint> _shadowPaints; |
| |
| @override |
| VoidCallback get onChanged => super.onChanged!; |
| |
| void _precache(Rect rect, TextDirection? textDirection) { |
| assert(rect != null); |
| if (rect == _lastRect && textDirection == _lastTextDirection) { |
| return; |
| } |
| |
| // 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)) { |
| _interiorPaint = Paint(); |
| if (_decoration.color != null) { |
| _interiorPaint!.color = _decoration.color!; |
| } |
| } |
| if (_decoration.gradient != null) { |
| _interiorPaint!.shader = _decoration.gradient!.createShader(rect, textDirection: textDirection); |
| } |
| if (_decoration.shadows != null) { |
| if (_shadowCount == null) { |
| _shadowCount = _decoration.shadows!.length; |
| _shadowPaints = <Paint>[ |
| ..._decoration.shadows!.map((BoxShadow shadow) => shadow.toPaint()), |
| ]; |
| } |
| 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); |
| }), |
| ]; |
| } |
| } |
| if (!_decoration.shape.preferPaintInterior && (_interiorPaint != null || _shadowCount != null)) { |
| _outerPath = _decoration.shape.getOuterPath(rect, textDirection: textDirection); |
| } |
| if (_decoration.image != null) { |
| _innerPath = _decoration.shape.getInnerPath(rect, textDirection: textDirection); |
| } |
| |
| _lastRect = rect; |
| _lastTextDirection = textDirection; |
| } |
| |
| void _paintShadows(Canvas canvas, Rect rect, TextDirection? textDirection) { |
| if (_shadowCount != null) { |
| 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]); |
| } |
| } |
| } |
| } |
| |
| void _paintInterior(Canvas canvas, Rect rect, TextDirection? textDirection) { |
| if (_interiorPaint != null) { |
| if (_decoration.shape.preferPaintInterior) { |
| _decoration.shape.paintInterior(canvas, rect, _interiorPaint!, textDirection: textDirection); |
| } else { |
| canvas.drawPath(_outerPath, _interiorPaint!); |
| } |
| } |
| } |
| |
| DecorationImagePainter? _imagePainter; |
| void _paintImage(Canvas canvas, ImageConfiguration configuration) { |
| if (_decoration.image == null) { |
| return; |
| } |
| _imagePainter ??= _decoration.image!.createPainter(onChanged); |
| _imagePainter!.paint(canvas, _lastRect!, _innerPath, configuration); |
| } |
| |
| @override |
| void dispose() { |
| _imagePainter?.dispose(); |
| super.dispose(); |
| } |
| |
| @override |
| void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) { |
| assert(configuration != null); |
| assert(configuration.size != null); |
| final Rect rect = offset & configuration.size!; |
| final TextDirection? textDirection = configuration.textDirection; |
| _precache(rect, textDirection); |
| _paintShadows(canvas, rect, textDirection); |
| _paintInterior(canvas, rect, textDirection); |
| _paintImage(canvas, configuration); |
| _decoration.shape.paint(canvas, rect, textDirection: textDirection); |
| } |
| } |