// 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. // @dart = 2.8 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.onError, this.colorFilter, this.fit, this.alignment = Alignment.center, this.centerSlice, this.repeat = ImageRepeat.noRepeat, this.matchTextDirection = false, this.scale = 1.0 }) : assert(image != null), assert(alignment != null), assert(repeat != null), assert(matchTextDirection != null), assert(scale != 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; /// An optional error callback for errors emitted when loading [image]. final ImageErrorListener onError; /// 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; /// Defines image pixels to be shown per logical pixels. /// /// By default the the value of scale is 1.0. The scale for the image is /// calculated by multiplying [scale] with `scale` of the given [ImageProvider]. final double scale; /// 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 && other.scale == scale; } @override int get hashCode => hashValues(image, colorFilter, fit, alignment, centerSlice, repeat, matchTextDirection, scale); @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', 'scale: $scale' ]; 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, onError: _details.onError, ); _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: _details.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, onError: _details.onError, )); } @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, bool isAntiAlias = false, }) { assert(canvas != null); assert(image != null); assert(alignment != null); assert(repeat != null); assert(flipHorizontally != null); assert(isAntiAlias != 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 = isAntiAlias; 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)); } }