| // 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; |
| import 'dart:ui' as ui; |
| |
| import 'package:flutter/services.dart'; |
| |
| import 'basic_types.dart'; |
| import 'shadows.dart'; |
| |
| /// An immutable set of offsets in each of the four cardinal directions. |
| /// |
| /// Typically used for an offset from each of the four sides of a box. For |
| /// example, the padding inside a box can be represented using this class. |
| class EdgeDims { |
| /// Constructs an EdgeDims from offsets from the top, right, bottom and left. |
| const EdgeDims.TRBL(this.top, this.right, this.bottom, this.left); |
| |
| /// Constructs an EdgeDims where all the offsets are value. |
| const EdgeDims.all(double value) |
| : top = value, right = value, bottom = value, left = value; |
| |
| /// Constructs an EdgeDims with only the given values non-zero. |
| const EdgeDims.only({ this.top: 0.0, |
| this.right: 0.0, |
| this.bottom: 0.0, |
| this.left: 0.0 }); |
| |
| /// Constructs an EdgeDims with symmetrical vertical and horizontal offsets. |
| const EdgeDims.symmetric({ double vertical: 0.0, |
| double horizontal: 0.0 }) |
| : top = vertical, left = horizontal, bottom = vertical, right = horizontal; |
| |
| /// The offset from the top. |
| final double top; |
| |
| /// The offset from the right. |
| final double right; |
| |
| /// The offset from the bottom. |
| final double bottom; |
| |
| /// The offset from the left. |
| final double left; |
| |
| /// Whether every dimension is non-negative. |
| bool get isNonNegative => top >= 0.0 && right >= 0.0 && bottom >= 0.0 && left >= 0.0; |
| |
| /// The size that this edge dims would occupy with an empty interior. |
| Size get collapsedSize => new Size(left + right, top + bottom); |
| |
| EdgeDims operator -(EdgeDims other) { |
| return new EdgeDims.TRBL( |
| top - other.top, |
| right - other.right, |
| bottom - other.bottom, |
| left - other.left |
| ); |
| } |
| |
| EdgeDims operator +(EdgeDims other) { |
| return new EdgeDims.TRBL( |
| top + other.top, |
| right + other.right, |
| bottom + other.bottom, |
| left + other.left |
| ); |
| } |
| |
| EdgeDims operator *(double other) { |
| return new EdgeDims.TRBL( |
| top * other, |
| right * other, |
| bottom * other, |
| left * other |
| ); |
| } |
| |
| EdgeDims operator /(double other) { |
| return new EdgeDims.TRBL( |
| top / other, |
| right / other, |
| bottom / other, |
| left / other |
| ); |
| } |
| |
| EdgeDims operator ~/(double other) { |
| return new EdgeDims.TRBL( |
| (top ~/ other).toDouble(), |
| (right ~/ other).toDouble(), |
| (bottom ~/ other).toDouble(), |
| (left ~/ other).toDouble() |
| ); |
| } |
| |
| EdgeDims operator %(double other) { |
| return new EdgeDims.TRBL( |
| top % other, |
| right % other, |
| bottom % other, |
| left % other |
| ); |
| } |
| |
| /// Linearly interpolate between two EdgeDims. |
| /// |
| /// If either is null, this function interpolates from [EdgeDims.zero]. |
| static EdgeDims lerp(EdgeDims a, EdgeDims b, double t) { |
| if (a == null && b == null) |
| return null; |
| if (a == null) |
| return b * t; |
| if (b == null) |
| return a * (1.0 - t); |
| return new EdgeDims.TRBL( |
| ui.lerpDouble(a.top, b.top, t), |
| ui.lerpDouble(a.right, b.right, t), |
| ui.lerpDouble(a.bottom, b.bottom, t), |
| ui.lerpDouble(a.left, b.left, t) |
| ); |
| } |
| |
| /// An EdgeDims with zero offsets in each direction. |
| static const EdgeDims zero = const EdgeDims.TRBL(0.0, 0.0, 0.0, 0.0); |
| |
| bool operator ==(dynamic other) { |
| if (identical(this, other)) |
| return true; |
| if (other is! EdgeDims) |
| return false; |
| final EdgeDims typedOther = other; |
| return top == typedOther.top && |
| right == typedOther.right && |
| bottom == typedOther.bottom && |
| left == typedOther.left; |
| } |
| |
| int get hashCode { |
| int value = 373; |
| value = 37 * value + top.hashCode; |
| value = 37 * value + left.hashCode; |
| value = 37 * value + bottom.hashCode; |
| value = 37 * value + right.hashCode; |
| return value; |
| } |
| |
| String toString() => "EdgeDims($top, $right, $bottom, $left)"; |
| } |
| |
| /// A side of a border of a box |
| class BorderSide { |
| const BorderSide({ |
| this.color: const Color(0xFF000000), |
| this.width: 1.0 |
| }); |
| |
| /// The color of this side of the border |
| final Color color; |
| |
| /// The width of this side of the border |
| final double width; |
| |
| /// A black border side of zero width |
| static const none = const BorderSide(width: 0.0); |
| |
| bool operator ==(dynamic other) { |
| if (identical(this, other)) |
| return true; |
| if (other is! BorderSide) |
| return false; |
| final BorderSide typedOther = other; |
| return color == typedOther.color && |
| width == typedOther.width; |
| } |
| |
| int get hashCode { |
| int value = 373; |
| value = 37 * value + color.hashCode; |
| value = 37 * value + width.hashCode; |
| return value; |
| } |
| |
| String toString() => 'BorderSide($color, $width)'; |
| } |
| |
| /// A border of a box, comprised of four sides |
| class Border { |
| const Border({ |
| this.top: BorderSide.none, |
| this.right: BorderSide.none, |
| this.bottom: BorderSide.none, |
| this.left: BorderSide.none |
| }); |
| |
| /// A uniform border with all sides the same color and width |
| factory Border.all({ |
| Color color: const Color(0xFF000000), |
| double width: 1.0 |
| }) { |
| BorderSide side = new BorderSide(color: color, width: width); |
| return new Border(top: side, right: side, bottom: side, left: side); |
| } |
| |
| /// The top side of this border |
| final BorderSide top; |
| |
| /// The right side of this border |
| final BorderSide right; |
| |
| /// The bottom side of this border |
| final BorderSide bottom; |
| |
| /// The left side of this border |
| final BorderSide left; |
| |
| /// The widths of the sides of this border represented as an EdgeDims |
| EdgeDims get dimensions { |
| return new EdgeDims.TRBL(top.width, right.width, bottom.width, left.width); |
| } |
| |
| bool operator ==(dynamic other) { |
| if (identical(this, other)) |
| return true; |
| if (other is! Border) |
| return false; |
| final Border typedOther = other; |
| return top == typedOther.top && |
| right == typedOther.right && |
| bottom == typedOther.bottom && |
| left == typedOther.left; |
| } |
| |
| int get hashCode { |
| int value = 373; |
| value = 37 * value + top.hashCode; |
| value = 37 * value + right.hashCode; |
| value = 37 * value + bottom.hashCode; |
| value = 37 * value + left.hashCode; |
| return value; |
| } |
| |
| String toString() => 'Border($top, $right, $bottom, $left)'; |
| } |
| |
| /// A shadow cast by a box |
| /// |
| /// Note: BoxShadow can cast non-rectangular shadows if the box is |
| /// non-rectangular (e.g., has a border radius or a circular shape). |
| class BoxShadow { |
| const BoxShadow({ |
| this.color, |
| this.offset, |
| this.blur |
| }); |
| |
| /// The color of the shadow |
| final Color color; |
| |
| /// The displacement of the shadow from the box |
| final Offset offset; |
| |
| /// The standard deviation of the Gaussian to convolve with the box's shape |
| final double blur; |
| |
| /// Returns a new box shadow with its offset and blur scaled by the given factor |
| BoxShadow scale(double factor) { |
| return new BoxShadow( |
| color: color, |
| offset: offset * factor, |
| blur: blur * factor |
| ); |
| } |
| |
| /// Linearly interpolate between two box shadows |
| /// |
| /// If either box shadow is null, this function linearly interpolates from a |
| /// a box shadow that matches the other box shadow in color but has a zero |
| /// offset and a zero blur. |
| static BoxShadow lerp(BoxShadow a, BoxShadow b, double t) { |
| if (a == null && b == null) |
| return null; |
| if (a == null) |
| return b.scale(t); |
| if (b == null) |
| return a.scale(1.0 - t); |
| return new BoxShadow( |
| color: Color.lerp(a.color, b.color, t), |
| offset: Offset.lerp(a.offset, b.offset, t), |
| blur: ui.lerpDouble(a.blur, b.blur, t) |
| ); |
| } |
| |
| /// Linearly interpolate between two lists of box shadows |
| /// |
| /// If the lists differ in length, excess items are lerped with null. |
| static List<BoxShadow> lerpList(List<BoxShadow> a, List<BoxShadow> b, double t) { |
| if (a == null && b == null) |
| return null; |
| if (a == null) |
| a = new List<BoxShadow>(); |
| if (b == null) |
| b = new List<BoxShadow>(); |
| List<BoxShadow> result = new List<BoxShadow>(); |
| int commonLength = math.min(a.length, b.length); |
| for (int i = 0; i < commonLength; ++i) |
| result.add(BoxShadow.lerp(a[i], b[i], t)); |
| for (int i = commonLength; i < a.length; ++i) |
| result.add(a[i].scale(1.0 - t)); |
| for (int i = commonLength; i < b.length; ++i) |
| result.add(b[i].scale(t)); |
| return result; |
| } |
| |
| bool operator ==(dynamic other) { |
| if (identical(this, other)) |
| return true; |
| if (other is! BoxShadow) |
| return false; |
| final BoxShadow typedOther = other; |
| return color == typedOther.color && |
| offset == typedOther.offset && |
| blur == typedOther.blur; |
| } |
| |
| int get hashCode { |
| int value = 373; |
| value = 37 * value + color.hashCode; |
| value = 37 * value + offset.hashCode; |
| value = 37 * value + blur.hashCode; |
| return value; |
| } |
| |
| String toString() => 'BoxShadow($color, $offset, $blur)'; |
| } |
| |
| /// A 2D gradient |
| abstract class Gradient { |
| const Gradient(); |
| ui.Shader createShader(); |
| } |
| |
| /// A 2D linear gradient |
| class LinearGradient extends Gradient { |
| const LinearGradient({ |
| this.begin, |
| this.end, |
| this.colors, |
| this.stops, |
| this.tileMode: ui.TileMode.clamp |
| }); |
| |
| /// The point at which stop 0.0 of the gradient is placed |
| final Point begin; |
| |
| /// The point at which stop 1.0 of the gradient is placed |
| final Point end; |
| |
| /// The colors the gradient should obtain at each of the stops |
| /// |
| /// Note: This list must have the same length as [stops]. |
| final List<Color> colors; |
| |
| /// A list of values from 0.0 to 1.0 that denote fractions of the vector from start to end |
| /// |
| /// Note: If specified, this list must have the same length as [colors]. Otherwise the colors |
| /// are distributed evenly between [begin] and [end]. |
| final List<double> stops; |
| |
| /// How this gradient should tile the plane |
| final ui.TileMode tileMode; |
| |
| ui.Shader createShader() { |
| return new ui.Gradient.linear(<Point>[begin, end], this.colors, this.stops, this.tileMode); |
| } |
| |
| bool operator ==(dynamic other) { |
| if (identical(this, other)) |
| return true; |
| if (other is! LinearGradient) |
| return false; |
| final LinearGradient typedOther = other; |
| if (begin != typedOther.begin || |
| end != typedOther.end || |
| tileMode != typedOther.tileMode || |
| colors?.length != typedOther.colors?.length || |
| stops?.length != typedOther.stops?.length) |
| return false; |
| if (colors != null) { |
| assert(typedOther.colors != null); |
| assert(colors.length == typedOther.colors.length); |
| for (int i = 0; i < colors.length; i += 1) { |
| if (colors[i] != typedOther.colors[i]) |
| return false; |
| } |
| } |
| if (stops != null) { |
| assert(typedOther.stops != null); |
| assert(stops.length == typedOther.stops.length); |
| for (int i = 0; i < stops.length; i += 1) { |
| if (stops[i] != typedOther.stops[i]) |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| int get hashCode { |
| int value = 373; |
| value = 37 * value + begin.hashCode; |
| value = 37 * value + end.hashCode; |
| value = 37 * value + tileMode.hashCode; |
| if (colors != null) { |
| for (int i = 0; i < colors.length; i += 1) |
| value = 37 * value + colors[i].hashCode; |
| } else { |
| value = 37 * value + null.hashCode; |
| } |
| if (stops != null) { |
| for (int i = 0; i < stops.length; i += 1) |
| value = 37 * value + stops[i].hashCode; |
| } else { |
| value = 37 * value + null.hashCode; |
| } |
| return value; |
| } |
| |
| String toString() { |
| return 'LinearGradient($begin, $end, $colors, $stops, $tileMode)'; |
| } |
| } |
| |
| /// A 2D radial gradient |
| class RadialGradient extends Gradient { |
| const RadialGradient({ |
| this.center, |
| this.radius, |
| this.colors, |
| this.stops, |
| this.tileMode: ui.TileMode.clamp |
| }); |
| |
| /// The center of the gradient |
| final Point center; |
| |
| /// The radius at which stop 1.0 is placed |
| final double radius; |
| |
| /// The colors the gradient should obtain at each of the stops |
| /// |
| /// Note: This list must have the same length as [stops]. |
| final List<Color> colors; |
| |
| /// A list of values from 0.0 to 1.0 that denote concentric rings |
| /// |
| /// The rings are centered at [center] and have a radius equal to the value of |
| /// the stop times [radius]. |
| /// |
| /// Note: This list must have the same length as [colors]. |
| final List<double> stops; |
| |
| /// How this gradient should tile the plane |
| final ui.TileMode tileMode; |
| |
| ui.Shader createShader() { |
| return new ui.Gradient.radial(center, radius, colors, stops, tileMode); |
| } |
| |
| bool operator ==(dynamic other) { |
| if (identical(this, other)) |
| return true; |
| if (other is! RadialGradient) |
| return false; |
| final RadialGradient typedOther = other; |
| if (center != typedOther.center || |
| radius != typedOther.radius || |
| tileMode != typedOther.tileMode || |
| colors?.length != typedOther.colors?.length || |
| stops?.length != typedOther.stops?.length) |
| return false; |
| if (colors != null) { |
| assert(typedOther.colors != null); |
| assert(colors.length == typedOther.colors.length); |
| for (int i = 0; i < colors.length; i += 1) { |
| if (colors[i] != typedOther.colors[i]) |
| return false; |
| } |
| } |
| if (stops != null) { |
| assert(typedOther.stops != null); |
| assert(stops.length == typedOther.stops.length); |
| for (int i = 0; i < stops.length; i += 1) { |
| if (stops[i] != typedOther.stops[i]) |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| int get hashCode { |
| int value = 373; |
| value = 37 * value + center.hashCode; |
| value = 37 * value + radius.hashCode; |
| value = 37 * value + tileMode.hashCode; |
| if (colors != null) { |
| for (int i = 0; i < colors.length; i += 1) |
| value = 37 * value + colors[i].hashCode; |
| } else { |
| value = 37 * value + null.hashCode; |
| } |
| if (stops != null) { |
| for (int i = 0; i < stops.length; i += 1) |
| value = 37 * value + stops[i].hashCode; |
| } else { |
| value = 37 * value + null.hashCode; |
| } |
| return value; |
| } |
| |
| String toString() { |
| return 'RadialGradient($center, $radius, $colors, $stops, $tileMode)'; |
| } |
| } |
| |
| /// How an image should be inscribed into a box |
| enum ImageFit { |
| /// Fill the box by distorting the image's aspect ratio |
| fill, |
| |
| /// As large as possible while still containing the image entirely within the box |
| contain, |
| |
| /// As small as possible while still covering the entire box |
| cover, |
| |
| /// Center the image within the box and discard any portions of the image that |
| /// lie outside the box |
| none, |
| |
| /// Center the image within the box and, if necessary, scale the image down to |
| /// ensure that the image fits within the box |
| scaleDown |
| } |
| |
| /// How to paint any portions of a box not covered by an image |
| enum ImageRepeat { |
| /// Repeat the image in both the x and y directions until the box is filled |
| repeat, |
| |
| /// Repeat the image in the x direction until the box is filled horizontally |
| repeatX, |
| |
| /// Repeat the image in the y direction until the box is filled vertically |
| repeatY, |
| |
| /// Leave uncovered poritions of the box transparent |
| noRepeat |
| } |
| |
| /// Paint an image into the given rectangle in the canvas |
| void paintImage({ |
| Canvas canvas, |
| Rect rect, |
| ui.Image image, |
| ColorFilter colorFilter, |
| ImageFit fit, |
| repeat: ImageRepeat.noRepeat, |
| Rect centerSlice, |
| double positionX: 0.5, |
| double positionY: 0.5 |
| }) { |
| Size outputSize = rect.size; |
| Size inputSize = new Size(image.width.toDouble(), image.height.toDouble()); |
| Offset sliceBorder; |
| if (centerSlice != null) { |
| sliceBorder = new Offset( |
| centerSlice.left + inputSize.width - centerSlice.right, |
| centerSlice.top + inputSize.height - centerSlice.bottom |
| ); |
| outputSize -= sliceBorder; |
| inputSize -= sliceBorder; |
| } |
| Size sourceSize; |
| Size destinationSize; |
| fit ??= centerSlice == null ? ImageFit.scaleDown : ImageFit.fill; |
| assert(centerSlice == null || (fit != ImageFit.none && fit != ImageFit.cover)); |
| switch (fit) { |
| case ImageFit.fill: |
| sourceSize = inputSize; |
| destinationSize = outputSize; |
| break; |
| case ImageFit.contain: |
| sourceSize = inputSize; |
| if (outputSize.width / outputSize.height > sourceSize.width / sourceSize.height) |
| destinationSize = new Size(sourceSize.width * outputSize.height / sourceSize.height, outputSize.height); |
| else |
| destinationSize = new Size(outputSize.width, sourceSize.height * outputSize.width / sourceSize.width); |
| break; |
| case ImageFit.cover: |
| if (outputSize.width / outputSize.height > inputSize.width / inputSize.height) |
| sourceSize = new Size(inputSize.width, inputSize.width * outputSize.height / outputSize.width); |
| else |
| sourceSize = new Size(inputSize.height * outputSize.width / outputSize.height, inputSize.height); |
| destinationSize = outputSize; |
| break; |
| case ImageFit.none: |
| sourceSize = new Size(math.min(inputSize.width, outputSize.width), |
| math.min(inputSize.height, outputSize.height)); |
| destinationSize = sourceSize; |
| break; |
| case ImageFit.scaleDown: |
| sourceSize = inputSize; |
| destinationSize = outputSize; |
| if (sourceSize.height > destinationSize.height) |
| destinationSize = new Size(sourceSize.width * destinationSize.height / sourceSize.height, sourceSize.height); |
| if (sourceSize.width > destinationSize.width) |
| destinationSize = new Size(destinationSize.width, sourceSize.height * destinationSize.width / sourceSize.width); |
| break; |
| } |
| if (centerSlice != null) { |
| outputSize += sliceBorder; |
| destinationSize += sliceBorder; |
| // We don't have the ability to draw a subset of the image at the same time |
| // as we apply a nine-patch stretch. |
| assert(sourceSize == inputSize); |
| } |
| // TODO(abarth): Implement |repeat|. |
| Paint paint = new Paint()..isAntiAlias = false; |
| if (colorFilter != null) |
| paint.colorFilter = colorFilter; |
| double dx = (outputSize.width - destinationSize.width) * positionX; |
| double dy = (outputSize.height - destinationSize.height) * positionY; |
| Point destinationPosition = rect.topLeft + new Offset(dx, dy); |
| Rect destinationRect = destinationPosition & destinationSize; |
| if (centerSlice == null) |
| canvas.drawImageRect(image, Point.origin & sourceSize, destinationRect, paint); |
| else |
| canvas.drawImageNine(image, centerSlice, destinationRect, paint); |
| } |
| |
| /// A background image for a box. |
| class BackgroundImage { |
| BackgroundImage({ |
| ImageResource image, |
| this.fit, |
| this.repeat: ImageRepeat.noRepeat, |
| this.centerSlice, |
| this.colorFilter |
| }) : _imageResource = image; |
| |
| /// How the background image should be inscribed into the box. |
| final ImageFit fit; |
| |
| /// How to paint any portions of the box not covered by the background image. |
| final ImageRepeat repeat; |
| |
| /// The center slice for a nine-patch image. |
| /// |
| /// The region of the image inside the center slice will be stretched both |
| /// horizontally and vertically to fit the image into its destination. The |
| /// region of the image above and below the center slice will be stretched |
| /// only horizontally and the region of the image to the left and right of |
| /// the center slice will be stretched only vertically. |
| final Rect centerSlice; |
| |
| /// A color filter to apply to the background image before painting it. |
| final ColorFilter colorFilter; |
| |
| /// The image to be painted into the background. |
| ui.Image get image => _image; |
| ui.Image _image; |
| |
| final ImageResource _imageResource; |
| |
| final List<VoidCallback> _listeners = |
| new List<VoidCallback>(); |
| |
| /// Call listener when the background images changes (e.g., arrives from the network). |
| void addChangeListener(VoidCallback listener) { |
| // We add the listener to the _imageResource first so that the first change |
| // listener doesn't get callback synchronously if the image resource is |
| // already resolved. |
| if (_listeners.isEmpty) |
| _imageResource.addListener(_handleImageChanged); |
| _listeners.add(listener); |
| } |
| |
| /// No longer call listener when the background image changes. |
| void removeChangeListener(VoidCallback listener) { |
| _listeners.remove(listener); |
| // We need to remove ourselves as listeners from the _imageResource so that |
| // we're not kept alive by the image_cache. |
| if (_listeners.isEmpty) |
| _imageResource.removeListener(_handleImageChanged); |
| } |
| |
| void _handleImageChanged(ui.Image resolvedImage) { |
| if (resolvedImage == null) |
| return; |
| _image = resolvedImage; |
| final List<VoidCallback> localListeners = |
| new List<VoidCallback>.from(_listeners); |
| for (VoidCallback listener in localListeners) |
| listener(); |
| } |
| |
| bool operator ==(dynamic other) { |
| if (identical(this, other)) |
| return true; |
| if (other is! BackgroundImage) |
| return false; |
| final BackgroundImage typedOther = other; |
| return fit == typedOther.fit && |
| repeat == typedOther.repeat && |
| centerSlice == typedOther.centerSlice && |
| colorFilter == typedOther.colorFilter && |
| _imageResource == typedOther._imageResource; |
| } |
| |
| int get hashCode { |
| int value = 373; |
| value = 37 * value + fit.hashCode; |
| value = 37 * value + repeat.hashCode; |
| value = 37 * value + centerSlice.hashCode; |
| value = 37 * value + colorFilter.hashCode; |
| value = 37 * value + _imageResource.hashCode; |
| return value; |
| } |
| |
| String toString() => 'BackgroundImage($fit, $repeat)'; |
| } |
| |
| // TODO(abarth): Rename to BoxShape? |
| /// A 2D geometrical shape |
| enum Shape { |
| /// An axis-aligned, 2D rectangle |
| rectangle, |
| |
| /// A 2D locus of points equidistant from a single point |
| circle |
| } |
| |
| /// An immutable description of how to paint a box |
| class BoxDecoration { |
| const BoxDecoration({ |
| this.backgroundColor, // null = don't draw background color |
| this.backgroundImage, // null = don't draw background image |
| this.border, // null = don't draw border |
| this.borderRadius, // null = use more efficient background drawing; note that this must be null for circles |
| this.boxShadow, // null = don't draw shadows |
| this.gradient, // null = don't allocate gradient objects |
| this.shape: Shape.rectangle |
| }); |
| |
| /// The color to fill in the background of the box |
| /// |
| /// The color is filled into the shape of the box (e.g., either a rectangle, |
| /// potentially with a border radius, or a circle). |
| final Color backgroundColor; |
| |
| /// An image to paint above the background color |
| final BackgroundImage backgroundImage; |
| |
| /// A border to draw above the background |
| final Border border; |
| |
| /// If non-null, the corners of this box are rounded by this radius |
| /// |
| /// Applies only to boxes with rectangular shapes. |
| final double borderRadius; |
| |
| /// A list of shadows cast by this box behind the background |
| final List<BoxShadow> boxShadow; |
| |
| /// A graident to use when filling the background |
| final Gradient gradient; |
| |
| /// The shape to fill the background color into and to cast as a shadow |
| final Shape shape; |
| |
| /// Returns a new box decoration that is scalled by the given factor |
| BoxDecoration scale(double factor) { |
| // TODO(abarth): Scale ALL the things. |
| return new BoxDecoration( |
| backgroundColor: Color.lerp(null, backgroundColor, factor), |
| backgroundImage: backgroundImage, |
| border: border, |
| borderRadius: ui.lerpDouble(null, borderRadius, factor), |
| boxShadow: BoxShadow.lerpList(null, boxShadow, factor), |
| gradient: gradient, |
| shape: shape |
| ); |
| } |
| |
| /// Linearly interpolate between two box decorations |
| /// |
| /// Interpolates each parameter of the box decoration separately. |
| static BoxDecoration lerp(BoxDecoration a, BoxDecoration b, double t) { |
| if (a == null && b == null) |
| return null; |
| if (a == null) |
| return b.scale(t); |
| if (b == null) |
| return a.scale(1.0 - t); |
| // TODO(abarth): lerp ALL the fields. |
| return new BoxDecoration( |
| backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), |
| backgroundImage: b.backgroundImage, |
| border: b.border, |
| borderRadius: ui.lerpDouble(a.borderRadius, b.borderRadius, t), |
| boxShadow: BoxShadow.lerpList(a.boxShadow, b.boxShadow, t), |
| gradient: b.gradient, |
| shape: b.shape |
| ); |
| } |
| |
| bool operator ==(dynamic other) { |
| if (identical(this, other)) |
| return true; |
| if (other is! BoxDecoration) |
| return false; |
| final BoxDecoration typedOther = other; |
| return backgroundColor == typedOther.backgroundColor && |
| backgroundImage == typedOther.backgroundImage && |
| border == typedOther.border && |
| borderRadius == typedOther.borderRadius && |
| boxShadow == typedOther.boxShadow && |
| gradient == typedOther.gradient && |
| shape == typedOther.shape; |
| } |
| |
| int get hashCode { |
| int value = 373; |
| value = 37 * value + backgroundColor.hashCode; |
| value = 37 * value + backgroundImage.hashCode; |
| value = 37 * value + border.hashCode; |
| value = 37 * value + borderRadius.hashCode; |
| value = 37 * value + boxShadow.hashCode; |
| value = 37 * value + gradient.hashCode; |
| value = 37 * value + shape.hashCode; |
| return value; |
| } |
| |
| String toString([String prefix = '']) { |
| List<String> result = <String>[]; |
| if (backgroundColor != null) |
| result.add('${prefix}backgroundColor: $backgroundColor'); |
| if (backgroundImage != null) |
| result.add('${prefix}backgroundImage: $backgroundImage'); |
| if (border != null) |
| result.add('${prefix}border: $border'); |
| if (borderRadius != null) |
| result.add('${prefix}borderRadius: $borderRadius'); |
| if (boxShadow != null) |
| result.add('${prefix}boxShadow: ${boxShadow.map((BoxShadow shadow) => shadow.toString())}'); |
| if (gradient != null) |
| result.add('${prefix}gradient: $gradient'); |
| if (shape != Shape.rectangle) |
| result.add('${prefix}shape: $shape'); |
| if (result.isEmpty) |
| return '$prefix<no decorations specified>'; |
| return result.join('\n'); |
| } |
| } |
| |
| /// An object that paints a [BoxDecoration] into a canvas |
| class BoxPainter { |
| BoxPainter(BoxDecoration decoration) : _decoration = decoration { |
| assert(decoration != null); |
| } |
| |
| BoxDecoration _decoration; |
| /// The box decoration to paint |
| BoxDecoration get decoration => _decoration; |
| void set decoration (BoxDecoration value) { |
| assert(value != null); |
| if (value == _decoration) |
| return; |
| _decoration = value; |
| _cachedBackgroundPaint = null; |
| } |
| |
| Paint _cachedBackgroundPaint; |
| Paint get _backgroundPaint { |
| if (_cachedBackgroundPaint == null) { |
| Paint paint = new Paint(); |
| |
| if (_decoration.backgroundColor != null) |
| paint.color = _decoration.backgroundColor; |
| |
| if (_decoration.boxShadow != null) { |
| var builder = new ShadowDrawLooperBuilder(); |
| for (BoxShadow boxShadow in _decoration.boxShadow) |
| builder.addShadow(boxShadow.offset, boxShadow.color, boxShadow.blur); |
| paint.drawLooper = builder.build(); |
| } |
| |
| if (_decoration.gradient != null) |
| paint.shader = _decoration.gradient.createShader(); |
| |
| _cachedBackgroundPaint = paint; |
| } |
| |
| return _cachedBackgroundPaint; |
| } |
| |
| bool get _hasUniformBorder { |
| Color color = _decoration.border.top.color; |
| bool hasUniformColor = |
| _decoration.border.right.color == color && |
| _decoration.border.bottom.color == color && |
| _decoration.border.left.color == color; |
| |
| if (!hasUniformColor) |
| return false; |
| |
| double width = _decoration.border.top.width; |
| bool hasUniformWidth = |
| _decoration.border.right.width == width && |
| _decoration.border.bottom.width == width && |
| _decoration.border.left.width == width; |
| |
| return hasUniformWidth; |
| } |
| |
| double _getEffectiveBorderRadius(Rect rect) { |
| double shortestSide = rect.shortestSide; |
| // In principle, we should use shortestSide / 2.0, but we don't want to |
| // run into floating point rounding errors. Instead, we just use |
| // shortestSide and let ui.Canvas do any remaining clamping. |
| return _decoration.borderRadius > shortestSide ? shortestSide : _decoration.borderRadius; |
| } |
| |
| void _paintBackgroundColor(ui.Canvas canvas, Rect rect) { |
| if (_decoration.backgroundColor != null || |
| _decoration.boxShadow != null || |
| _decoration.gradient != null) { |
| switch (_decoration.shape) { |
| case Shape.circle: |
| assert(_decoration.borderRadius == null); |
| Point center = rect.center; |
| double radius = rect.shortestSide / 2.0; |
| canvas.drawCircle(center, radius, _backgroundPaint); |
| break; |
| case Shape.rectangle: |
| if (_decoration.borderRadius == null) { |
| canvas.drawRect(rect, _backgroundPaint); |
| } else { |
| double radius = _getEffectiveBorderRadius(rect); |
| canvas.drawRRect(new ui.RRect.fromRectXY(rect, radius, radius), _backgroundPaint); |
| } |
| break; |
| } |
| } |
| } |
| |
| void _paintBackgroundImage(ui.Canvas canvas, Rect rect) { |
| final BackgroundImage backgroundImage = _decoration.backgroundImage; |
| if (backgroundImage == null) |
| return; |
| ui.Image image = backgroundImage.image; |
| if (image == null) |
| return; |
| paintImage( |
| canvas: canvas, |
| rect: rect, |
| image: image, |
| colorFilter: backgroundImage.colorFilter, |
| fit: backgroundImage.fit, |
| repeat: backgroundImage.repeat |
| ); |
| } |
| |
| void _paintBorder(ui.Canvas canvas, Rect rect) { |
| if (_decoration.border == null) |
| return; |
| |
| if (_hasUniformBorder) { |
| if (_decoration.borderRadius != null) { |
| _paintBorderWithRadius(canvas, rect); |
| return; |
| } |
| if (_decoration.shape == Shape.circle) { |
| _paintBorderWithCircle(canvas, rect); |
| return; |
| } |
| } |
| |
| assert(_decoration.borderRadius == null); // TODO(abarth): Support non-uniform rounded borders. |
| assert(_decoration.shape == Shape.rectangle); // TODO(ianh): Support non-uniform borders on circles. |
| |
| assert(_decoration.border.top != null); |
| assert(_decoration.border.right != null); |
| assert(_decoration.border.bottom != null); |
| assert(_decoration.border.left != null); |
| |
| Paint paint = new Paint(); |
| Path path; |
| |
| paint.color = _decoration.border.top.color; |
| path = new Path(); |
| path.moveTo(rect.left, rect.top); |
| path.lineTo(rect.left + _decoration.border.left.width, rect.top + _decoration.border.top.width); |
| path.lineTo(rect.right - _decoration.border.right.width, rect.top + _decoration.border.top.width); |
| path.lineTo(rect.right, rect.top); |
| path.close(); |
| canvas.drawPath(path, paint); |
| |
| paint.color = _decoration.border.right.color; |
| path = new Path(); |
| path.moveTo(rect.right, rect.top); |
| path.lineTo(rect.right - _decoration.border.right.width, rect.top + _decoration.border.top.width); |
| path.lineTo(rect.right - _decoration.border.right.width, rect.bottom - _decoration.border.bottom.width); |
| path.lineTo(rect.right, rect.bottom); |
| path.close(); |
| canvas.drawPath(path, paint); |
| |
| paint.color = _decoration.border.bottom.color; |
| path = new Path(); |
| path.moveTo(rect.right, rect.bottom); |
| path.lineTo(rect.right - _decoration.border.right.width, rect.bottom - _decoration.border.bottom.width); |
| path.lineTo(rect.left + _decoration.border.left.width, rect.bottom - _decoration.border.bottom.width); |
| path.lineTo(rect.left, rect.bottom); |
| path.close(); |
| canvas.drawPath(path, paint); |
| |
| paint.color = _decoration.border.left.color; |
| path = new Path(); |
| path.moveTo(rect.left, rect.bottom); |
| path.lineTo(rect.left + _decoration.border.left.width, rect.bottom - _decoration.border.bottom.width); |
| path.lineTo(rect.left + _decoration.border.left.width, rect.top + _decoration.border.top.width); |
| path.lineTo(rect.left, rect.top); |
| path.close(); |
| canvas.drawPath(path, paint); |
| } |
| |
| void _paintBorderWithRadius(ui.Canvas canvas, Rect rect) { |
| assert(_hasUniformBorder); |
| assert(_decoration.shape == Shape.rectangle); |
| Color color = _decoration.border.top.color; |
| double width = _decoration.border.top.width; |
| double radius = _getEffectiveBorderRadius(rect); |
| |
| ui.RRect outer = new ui.RRect.fromRectXY(rect, radius, radius); |
| ui.RRect inner = new ui.RRect.fromRectXY(rect.deflate(width), radius - width, radius - width); |
| canvas.drawDRRect(outer, inner, new Paint()..color = color); |
| } |
| |
| void _paintBorderWithCircle(ui.Canvas canvas, Rect rect) { |
| assert(_hasUniformBorder); |
| assert(_decoration.shape == Shape.circle); |
| assert(_decoration.borderRadius == null); |
| double width = _decoration.border.top.width; |
| if (width <= 0.0) { |
| return; |
| } |
| Paint paint = new Paint() |
| ..color = _decoration.border.top.color |
| ..strokeWidth = width |
| ..style = ui.PaintingStyle.stroke; |
| Point center = rect.center; |
| double radius = (rect.shortestSide - width) / 2.0; |
| canvas.drawCircle(center, radius, paint); |
| } |
| |
| /// Paint the box decoration into the given location on the given canvas |
| void paint(ui.Canvas canvas, Rect rect) { |
| _paintBackgroundColor(canvas, rect); |
| _paintBackgroundImage(canvas, rect); |
| _paintBorder(canvas, rect); |
| } |
| } |