| // 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 'dart:ui' as ui show Image; |
| |
| import 'package:flutter/foundation.dart'; |
| |
| import 'alignment.dart'; |
| import 'basic_types.dart'; |
| import 'borders.dart'; |
| import 'box_fit.dart'; |
| import 'image_provider.dart'; |
| import 'image_stream.dart'; |
| |
| /// 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 portions of the box transparent. |
| noRepeat, |
| } |
| |
| /// An image for a box decoration. |
| /// |
| /// The image is painted using [paintImage], which describes the meanings of the |
| /// various fields on this class in more detail. |
| @immutable |
| class DecorationImage { |
| /// Creates an image to show in a [BoxDecoration]. |
| /// |
| /// The [image], [alignment], [repeat], and [matchTextDirection] arguments |
| /// must not be null. |
| const DecorationImage({ |
| @required this.image, |
| this.colorFilter, |
| this.fit, |
| this.alignment = Alignment.center, |
| this.centerSlice, |
| this.repeat = ImageRepeat.noRepeat, |
| this.matchTextDirection = false, |
| }) : assert(image != null), |
| assert(alignment != null), |
| assert(repeat != null), |
| assert(matchTextDirection != null); |
| |
| /// The image to be painted into the decoration. |
| /// |
| /// Typically this will be an [AssetImage] (for an image shipped with the |
| /// application) or a [NetworkImage] (for an image obtained from the network). |
| final ImageProvider image; |
| |
| /// A color filter to apply to the image before painting it. |
| final ColorFilter colorFilter; |
| |
| /// How the image should be inscribed into the box. |
| /// |
| /// The default is [BoxFit.scaleDown] if [centerSlice] is null, and |
| /// [BoxFit.fill] if [centerSlice] is not null. |
| /// |
| /// See the discussion at [paintImage] for more details. |
| final BoxFit fit; |
| |
| /// How to align the image within its bounds. |
| /// |
| /// The alignment aligns the given position in the image to the given position |
| /// in the layout bounds. For example, an [Alignment] alignment of (-1.0, |
| /// -1.0) aligns the image to the top-left corner of its layout bounds, while a |
| /// [Alignment] alignment of (1.0, 1.0) aligns the bottom right of the |
| /// image with the bottom right corner of its layout bounds. Similarly, an |
| /// alignment of (0.0, 1.0) aligns the bottom middle of the image with the |
| /// middle of the bottom edge of its layout bounds. |
| /// |
| /// To display a subpart of an image, consider using a [CustomPainter] and |
| /// [Canvas.drawImageRect]. |
| /// |
| /// If the [alignment] is [TextDirection]-dependent (i.e. if it is a |
| /// [AlignmentDirectional]), then a [TextDirection] must be available |
| /// when the image is painted. |
| /// |
| /// Defaults to [Alignment.center]. |
| /// |
| /// See also: |
| /// |
| /// * [Alignment], a class with convenient constants typically used to |
| /// specify an [AlignmentGeometry]. |
| /// * [AlignmentDirectional], like [Alignment] for specifying alignments |
| /// relative to text direction. |
| final AlignmentGeometry alignment; |
| |
| /// 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. |
| /// |
| /// The stretching will be applied in order to make the image fit into the box |
| /// specified by [fit]. When [centerSlice] is not null, [fit] defaults to |
| /// [BoxFit.fill], which distorts the destination image size relative to the |
| /// image's original aspect ratio. Values of [BoxFit] which do not distort the |
| /// destination image size will result in [centerSlice] having no effect |
| /// (since the nine regions of the image will be rendered with the same |
| /// scaling, as if it wasn't specified). |
| final Rect centerSlice; |
| |
| /// How to paint any portions of the box that would not otherwise be covered |
| /// by the image. |
| final ImageRepeat repeat; |
| |
| /// Whether to paint the image in the direction of the [TextDirection]. |
| /// |
| /// If this is true, then in [TextDirection.ltr] contexts, the image will be |
| /// drawn with its origin in the top left (the "normal" painting direction for |
| /// images); and in [TextDirection.rtl] contexts, the image will be drawn with |
| /// a scaling factor of -1 in the horizontal direction so that the origin is |
| /// in the top right. |
| final bool matchTextDirection; |
| |
| /// Creates a [DecorationImagePainter] for this [DecorationImage]. |
| /// |
| /// The `onChanged` argument must not be null. It will be called whenever the |
| /// image needs to be repainted, e.g. because it is loading incrementally or |
| /// because it is animated. |
| DecorationImagePainter createPainter(VoidCallback onChanged) { |
| assert(onChanged != null); |
| return DecorationImagePainter._(this, onChanged); |
| } |
| |
| @override |
| bool operator ==(Object other) { |
| if (identical(this, other)) |
| return true; |
| if (other.runtimeType != runtimeType) |
| return false; |
| return other is DecorationImage |
| && other.image == image |
| && other.colorFilter == colorFilter |
| && other.fit == fit |
| && other.alignment == alignment |
| && other.centerSlice == centerSlice |
| && other.repeat == repeat |
| && other.matchTextDirection == matchTextDirection; |
| } |
| |
| @override |
| int get hashCode => hashValues(image, colorFilter, fit, alignment, centerSlice, repeat, matchTextDirection); |
| |
| @override |
| String toString() { |
| final List<String> properties = <String>[ |
| '$image', |
| if (colorFilter != null) |
| '$colorFilter', |
| if (fit != null && |
| !(fit == BoxFit.fill && centerSlice != null) && |
| !(fit == BoxFit.scaleDown && centerSlice == null)) |
| '$fit', |
| '$alignment', |
| if (centerSlice != null) |
| 'centerSlice: $centerSlice', |
| if (repeat != ImageRepeat.noRepeat) |
| '$repeat', |
| if (matchTextDirection) |
| 'match text direction', |
| ]; |
| return '${objectRuntimeType(this, 'DecorationImage')}(${properties.join(", ")})'; |
| } |
| } |
| |
| /// The painter for a [DecorationImage]. |
| /// |
| /// To obtain a painter, call [DecorationImage.createPainter]. |
| /// |
| /// To paint, call [paint]. The `onChanged` callback passed to |
| /// [DecorationImage.createPainter] will be called if the image needs to paint |
| /// again (e.g. because it is animated or because it had not yet loaded the |
| /// first time the [paint] method was called). |
| /// |
| /// This object should be disposed using the [dispose] method when it is no |
| /// longer needed. |
| class DecorationImagePainter { |
| DecorationImagePainter._(this._details, this._onChanged) : assert(_details != null); |
| |
| final DecorationImage _details; |
| final VoidCallback _onChanged; |
| |
| ImageStream _imageStream; |
| ImageInfo _image; |
| |
| /// Draw the image onto the given canvas. |
| /// |
| /// The image is drawn at the position and size given by the `rect` argument. |
| /// |
| /// The image is clipped to the given `clipPath`, if any. |
| /// |
| /// The `configuration` object is used to resolve the image (e.g. to pick |
| /// resolution-specific assets), and to implement the |
| /// [DecorationImage.matchTextDirection] feature. |
| /// |
| /// If the image needs to be painted again, e.g. because it is animated or |
| /// because it had not yet been loaded the first time this method was called, |
| /// then the `onChanged` callback passed to [DecorationImage.createPainter] |
| /// will be called. |
| void paint(Canvas canvas, Rect rect, Path clipPath, ImageConfiguration configuration) { |
| assert(canvas != null); |
| assert(rect != null); |
| assert(configuration != null); |
| |
| bool flipHorizontally = false; |
| if (_details.matchTextDirection) { |
| assert(() { |
| // We check this first so that the assert will fire immediately, not just |
| // when the image is ready. |
| if (configuration.textDirection == null) { |
| throw FlutterError.fromParts(<DiagnosticsNode>[ |
| ErrorSummary('DecorationImage.matchTextDirection can only be used when a TextDirection is available.'), |
| ErrorDescription( |
| 'When DecorationImagePainter.paint() was called, there was no text direction provided ' |
| 'in the ImageConfiguration object to match.' |
| ), |
| DiagnosticsProperty<DecorationImage>('The DecorationImage was', _details, style: DiagnosticsTreeStyle.errorProperty), |
| DiagnosticsProperty<ImageConfiguration>('The ImageConfiguration was', configuration, style: DiagnosticsTreeStyle.errorProperty), |
| ]); |
| } |
| return true; |
| }()); |
| if (configuration.textDirection == TextDirection.rtl) |
| flipHorizontally = true; |
| } |
| |
| final ImageStream newImageStream = _details.image.resolve(configuration); |
| if (newImageStream.key != _imageStream?.key) { |
| final ImageStreamListener listener = ImageStreamListener(_handleImage); |
| _imageStream?.removeListener(listener); |
| _imageStream = newImageStream; |
| _imageStream.addListener(listener); |
| } |
| if (_image == null) |
| return; |
| |
| if (clipPath != null) { |
| canvas.save(); |
| canvas.clipPath(clipPath); |
| } |
| |
| paintImage( |
| canvas: canvas, |
| rect: rect, |
| image: _image.image, |
| scale: _image.scale, |
| colorFilter: _details.colorFilter, |
| fit: _details.fit, |
| alignment: _details.alignment.resolve(configuration.textDirection), |
| centerSlice: _details.centerSlice, |
| repeat: _details.repeat, |
| flipHorizontally: flipHorizontally, |
| filterQuality: FilterQuality.low, |
| ); |
| |
| if (clipPath != null) |
| canvas.restore(); |
| } |
| |
| void _handleImage(ImageInfo value, bool synchronousCall) { |
| if (_image == value) |
| return; |
| _image = value; |
| assert(_onChanged != null); |
| if (!synchronousCall) |
| _onChanged(); |
| } |
| |
| /// Releases the resources used by this painter. |
| /// |
| /// This should be called whenever the painter is no longer needed. |
| /// |
| /// After this method has been called, the object is no longer usable. |
| @mustCallSuper |
| void dispose() { |
| _imageStream?.removeListener(ImageStreamListener(_handleImage)); |
| } |
| |
| @override |
| String toString() { |
| return '${objectRuntimeType(this, 'DecorationImagePainter')}(stream: $_imageStream, image: $_image) for $_details'; |
| } |
| } |
| |
| /// Paints an image into the given rectangle on the canvas. |
| /// |
| /// The arguments have the following meanings: |
| /// |
| /// * `canvas`: The canvas onto which the image will be painted. |
| /// |
| /// * `rect`: The region of the canvas into which the image will be painted. |
| /// The image might not fill the entire rectangle (e.g., depending on the |
| /// `fit`). If `rect` is empty, nothing is painted. |
| /// |
| /// * `image`: The image to paint onto the canvas. |
| /// |
| /// * `scale`: The number of image pixels for each logical pixel. |
| /// |
| /// * `colorFilter`: If non-null, the color filter to apply when painting the |
| /// image. |
| /// |
| /// * `fit`: How the image should be inscribed into `rect`. If null, the |
| /// default behavior depends on `centerSlice`. If `centerSlice` is also null, |
| /// the default behavior is [BoxFit.scaleDown]. If `centerSlice` is |
| /// non-null, the default behavior is [BoxFit.fill]. See [BoxFit] for |
| /// details. |
| /// |
| /// * `alignment`: How the destination rectangle defined by applying `fit` is |
| /// aligned within `rect`. For example, if `fit` is [BoxFit.contain] and |
| /// `alignment` is [Alignment.bottomRight], the image will be as large |
| /// as possible within `rect` and placed with its bottom right corner at the |
| /// bottom right corner of `rect`. Defaults to [Alignment.center]. |
| /// |
| /// * `centerSlice`: The image is drawn in nine portions described by splitting |
| /// the image by drawing two horizontal lines and two vertical lines, where |
| /// `centerSlice` describes the rectangle formed by the four points where |
| /// these four lines intersect each other. (This forms a 3-by-3 grid |
| /// of regions, the center region being described by `centerSlice`.) |
| /// The four regions in the corners are drawn, without scaling, in the four |
| /// corners of the destination rectangle defined by applying `fit`. The |
| /// remaining five regions are drawn by stretching them to fit such that they |
| /// exactly cover the destination rectangle while maintaining their relative |
| /// positions. |
| /// |
| /// * `repeat`: If the image does not fill `rect`, whether and how the image |
| /// should be repeated to fill `rect`. By default, the image is not repeated. |
| /// See [ImageRepeat] for details. |
| /// |
| /// * `flipHorizontally`: Whether to flip the image horizontally. This is |
| /// occasionally used with images in right-to-left environments, for images |
| /// that were designed for left-to-right locales (or vice versa). Be careful, |
| /// when using this, to not flip images with integral shadows, text, or other |
| /// effects that will look incorrect when flipped. |
| /// |
| /// * `invertColors`: Inverting the colors of an image applies a new color |
| /// filter to the paint. If there is another specified color filter, the |
| /// invert will be applied after it. This is primarily used for implementing |
| /// smart invert on iOS. |
| /// |
| /// * `filterQuality`: Use this to change the quality when scaling an image. |
| /// Use the [FilterQuality.low] quality setting to scale the image, which corresponds to |
| /// bilinear interpolation, rather than the default [FilterQuality.none] which corresponds |
| /// to nearest-neighbor. |
| /// |
| /// The `canvas`, `rect`, `image`, `scale`, `alignment`, `repeat`, `flipHorizontally` and `filterQuality` |
| /// arguments must not be null. |
| /// |
| /// See also: |
| /// |
| /// * [paintBorder], which paints a border around a rectangle on a canvas. |
| /// * [DecorationImage], which holds a configuration for calling this function. |
| /// * [BoxDecoration], which uses this function to paint a [DecorationImage]. |
| void paintImage({ |
| @required Canvas canvas, |
| @required Rect rect, |
| @required ui.Image image, |
| double scale = 1.0, |
| ColorFilter colorFilter, |
| BoxFit fit, |
| Alignment alignment = Alignment.center, |
| Rect centerSlice, |
| ImageRepeat repeat = ImageRepeat.noRepeat, |
| bool flipHorizontally = false, |
| bool invertColors = false, |
| FilterQuality filterQuality = FilterQuality.low, |
| }) { |
| assert(canvas != null); |
| assert(image != null); |
| assert(alignment != null); |
| assert(repeat != null); |
| assert(flipHorizontally != null); |
| if (rect.isEmpty) |
| return; |
| Size outputSize = rect.size; |
| Size inputSize = Size(image.width.toDouble(), image.height.toDouble()); |
| Offset sliceBorder; |
| if (centerSlice != null) { |
| sliceBorder = Offset( |
| centerSlice.left + inputSize.width - centerSlice.right, |
| centerSlice.top + inputSize.height - centerSlice.bottom, |
| ); |
| outputSize = outputSize - sliceBorder as Size; |
| inputSize = inputSize - sliceBorder as Size; |
| } |
| fit ??= centerSlice == null ? BoxFit.scaleDown : BoxFit.fill; |
| assert(centerSlice == null || (fit != BoxFit.none && fit != BoxFit.cover)); |
| final FittedSizes fittedSizes = applyBoxFit(fit, inputSize / scale, outputSize); |
| final Size sourceSize = fittedSizes.source * scale; |
| Size destinationSize = fittedSizes.destination; |
| 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, 'centerSlice was used with a BoxFit that does not guarantee that the image is fully visible.'); |
| } |
| if (repeat != ImageRepeat.noRepeat && destinationSize == outputSize) { |
| // There's no need to repeat the image because we're exactly filling the |
| // output rect with the image. |
| repeat = ImageRepeat.noRepeat; |
| } |
| final Paint paint = Paint()..isAntiAlias = false; |
| if (colorFilter != null) |
| paint.colorFilter = colorFilter; |
| if (sourceSize != destinationSize) { |
| paint.filterQuality = filterQuality; |
| } |
| paint.invertColors = invertColors; |
| final double halfWidthDelta = (outputSize.width - destinationSize.width) / 2.0; |
| final double halfHeightDelta = (outputSize.height - destinationSize.height) / 2.0; |
| final double dx = halfWidthDelta + (flipHorizontally ? -alignment.x : alignment.x) * halfWidthDelta; |
| final double dy = halfHeightDelta + alignment.y * halfHeightDelta; |
| final Offset destinationPosition = rect.topLeft.translate(dx, dy); |
| final Rect destinationRect = destinationPosition & destinationSize; |
| final bool needSave = repeat != ImageRepeat.noRepeat || flipHorizontally; |
| if (needSave) |
| canvas.save(); |
| if (repeat != ImageRepeat.noRepeat) |
| canvas.clipRect(rect); |
| if (flipHorizontally) { |
| final double dx = -(rect.left + rect.width / 2.0); |
| canvas.translate(-dx, 0.0); |
| canvas.scale(-1.0, 1.0); |
| canvas.translate(dx, 0.0); |
| } |
| if (centerSlice == null) { |
| final Rect sourceRect = alignment.inscribe( |
| sourceSize, Offset.zero & inputSize, |
| ); |
| if (repeat == ImageRepeat.noRepeat) { |
| canvas.drawImageRect(image, sourceRect, destinationRect, paint); |
| } else { |
| for (final Rect tileRect in _generateImageTileRects(rect, destinationRect, repeat)) |
| canvas.drawImageRect(image, sourceRect, tileRect, paint); |
| } |
| } else { |
| if (repeat == ImageRepeat.noRepeat) { |
| canvas.drawImageNine(image, centerSlice, destinationRect, paint); |
| } else { |
| for (final Rect tileRect in _generateImageTileRects(rect, destinationRect, repeat)) |
| canvas.drawImageNine(image, centerSlice, tileRect, paint); |
| } |
| } |
| if (needSave) |
| canvas.restore(); |
| } |
| |
| Iterable<Rect> _generateImageTileRects(Rect outputRect, Rect fundamentalRect, ImageRepeat repeat) sync* { |
| int startX = 0; |
| int startY = 0; |
| int stopX = 0; |
| int stopY = 0; |
| final double strideX = fundamentalRect.width; |
| final double strideY = fundamentalRect.height; |
| |
| if (repeat == ImageRepeat.repeat || repeat == ImageRepeat.repeatX) { |
| startX = ((outputRect.left - fundamentalRect.left) / strideX).floor(); |
| stopX = ((outputRect.right - fundamentalRect.right) / strideX).ceil(); |
| } |
| |
| if (repeat == ImageRepeat.repeat || repeat == ImageRepeat.repeatY) { |
| startY = ((outputRect.top - fundamentalRect.top) / strideY).floor(); |
| stopY = ((outputRect.bottom - fundamentalRect.bottom) / strideY).ceil(); |
| } |
| |
| for (int i = startX; i <= stopX; ++i) { |
| for (int j = startY; j <= stopY; ++j) |
| yield fundamentalRect.shift(Offset(i * strideX, j * strideY)); |
| } |
| } |