// 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:developer' as developer; import 'dart:math' as math; import 'dart:ui' as ui show FlutterView, Image; import 'package:flutter/foundation.dart'; import 'package:flutter/scheduler.dart'; import 'alignment.dart'; import 'basic_types.dart'; import 'binding.dart'; import 'borders.dart'; import 'box_fit.dart'; import 'debug.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, this.opacity = 1.0, this.filterQuality = FilterQuality.low, this.invertColors = false, this.isAntiAlias = false, }); /// 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 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; /// If non-null, the value is multiplied with the opacity of each image /// pixel before painting onto the canvas. /// /// This is more efficient than using [Opacity] or [FadeTransition] to /// change the opacity of an image. final double opacity; /// Used to set the filterQuality of the image. /// /// Defaults to [FilterQuality.low] to scale the image, which corresponds to /// bilinear interpolation. final FilterQuality filterQuality; /// Whether the colors of the image are inverted when drawn. /// /// 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. /// /// See also: /// /// * [Paint.invertColors], for the dart:ui implementation. final bool invertColors; /// Whether to paint the image with anti-aliasing. /// /// Anti-aliasing alleviates the sawtooth artifact when the image is rotated. final bool isAntiAlias; /// 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) { 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 && other.opacity == opacity && other.filterQuality == filterQuality && other.invertColors == invertColors && other.isAntiAlias == isAntiAlias; } @override int get hashCode => Object.hash( image, colorFilter, fit, alignment, centerSlice, repeat, matchTextDirection, scale, opacity, filterQuality, invertColors, isAntiAlias, ); @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.toStringAsFixed(1)}', 'opacity ${opacity.toStringAsFixed(1)}', '$filterQuality', if (invertColors) 'invert colors', if (isAntiAlias) 'use anti-aliasing', ]; return '${objectRuntimeType(this, 'DecorationImage')}(${properties.join(", ")})'; } /// Linearly interpolates between two [DecorationImage]s. /// /// The `t` argument represents position on the timeline, with 0.0 meaning /// that the interpolation has not started, returning `a`, 1.0 meaning that /// the interpolation has finished, returning `b`, and values in between /// meaning that the interpolation is at the relevant point on the timeline /// between `a` and `this`. The interpolation can be extrapolated beyond 0.0 /// and 1.0, so negative values and values greater than 1.0 are valid (and can /// easily be generated by curves such as [Curves.elasticInOut]). /// /// Values for `t` are usually obtained from an [Animation<double>], such as /// an [AnimationController]. static DecorationImage? lerp(DecorationImage? a, DecorationImage? b, double t) { if (identical(a, b) || t == 0.0) { return a; } if (t == 1.0) { return b; } return _BlendedDecorationImage(a, b, t); } } /// 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. abstract interface class DecorationImagePainter { /// 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. /// /// The `blend` argument specifies the opacity that should be applied to the /// image due to this image being blended with another. The `blendMode` /// argument can be specified to override the [DecorationImagePainter]'s /// default [BlendMode] behavior. It is usually set to [BlendMode.srcOver] if /// this is the first or only image being blended, and [BlendMode.plus] if it /// is being blended with an image below. void paint(Canvas canvas, Rect rect, Path? clipPath, ImageConfiguration configuration, { double blend = 1.0, BlendMode blendMode = BlendMode.srcOver }); /// 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. void dispose(); } class _DecorationImagePainter implements DecorationImagePainter { _DecorationImagePainter._(this._details, this._onChanged); final DecorationImage _details; final VoidCallback _onChanged; ImageStream? _imageStream; ImageInfo? _image; @override void paint(Canvas canvas, Rect rect, Path? clipPath, ImageConfiguration configuration, { double blend = 1.0, BlendMode blendMode = BlendMode.srcOver }) { 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, debugImageLabel: _image!.debugLabel, 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, opacity: _details.opacity * blend, filterQuality: _details.filterQuality, invertColors: _details.invertColors, isAntiAlias: _details.isAntiAlias, blendMode: blendMode, ); if (clipPath != null) { canvas.restore(); } } void _handleImage(ImageInfo value, bool synchronousCall) { if (_image == value) { return; } if (_image != null && _image!.isCloneOf(value)) { value.dispose(); return; } _image?.dispose(); _image = value; if (!synchronousCall) { _onChanged(); } } @override void dispose() { _imageStream?.removeListener(ImageStreamListener( _handleImage, onError: _details.onError, )); _image?.dispose(); _image = null; } @override String toString() { return '${objectRuntimeType(this, 'DecorationImagePainter')}(stream: $_imageStream, image: $_image) for $_details'; } } /// Used by [paintImage] to report image sizes drawn at the end of the frame. Map<String, ImageSizeInfo> _pendingImageSizeInfo = <String, ImageSizeInfo>{}; /// [ImageSizeInfo]s that were reported on the last frame. /// /// Used to prevent duplicative reports from frame to frame. Set<ImageSizeInfo> _lastFrameImageSizeInfo = <ImageSizeInfo>{}; /// Flushes inter-frame tracking of image size information from [paintImage]. /// /// Has no effect if asserts are disabled. @visibleForTesting void debugFlushLastFrameImageSizeInfo() { assert(() { _lastFrameImageSizeInfo = <ImageSizeInfo>{}; return true; }()); } /// 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. /// /// * `opacity`: The opacity to paint the image onto the canvas with. /// /// * `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. See also [Canvas.drawImageNine]. /// /// * `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, String? debugImageLabel, double scale = 1.0, double opacity = 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, BlendMode blendMode = BlendMode.srcOver, }) { assert( image.debugGetOpenHandleStackTraces()?.isNotEmpty ?? true, 'Cannot paint an image that is disposed.\n' 'The caller of paintImage is expected to wait to dispose the image until ' 'after painting has completed.', ); if (rect.isEmpty) { return; } Size outputSize = rect.size; Size inputSize = Size(image.width.toDouble(), image.height.toDouble()); Offset? sliceBorder; if (centerSlice != null) { sliceBorder = inputSize / scale - centerSlice.size as Offset; outputSize = outputSize - sliceBorder as Size; inputSize = inputSize - sliceBorder * scale 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; } paint.color = Color.fromRGBO(0, 0, 0, clampDouble(opacity, 0.0, 1.0)); paint.filterQuality = filterQuality; paint.invertColors = invertColors; paint.blendMode = blendMode; 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; // Set to true if we added a saveLayer to the canvas to invert/flip the image. bool invertedCanvas = false; // Output size and destination rect are fully calculated. // Implement debug-mode and profile-mode features: // - cacheWidth/cacheHeight warning // - debugInvertOversizedImages // - debugOnPaintImage // - Flutter.ImageSizesForFrame events in timeline if (!kReleaseMode) { // We can use the devicePixelRatio of the views directly here (instead of // going through a MediaQuery) because if it changes, whatever is aware of // the MediaQuery will be repainting the image anyways. // Furthermore, for the memory check below we just assume that all images // are decoded for the view with the highest device pixel ratio and use that // as an upper bound for the display size of the image. final double maxDevicePixelRatio = PaintingBinding.instance.platformDispatcher.views.fold( 0.0, (double previousValue, ui.FlutterView view) => math.max(previousValue, view.devicePixelRatio), ); final ImageSizeInfo sizeInfo = ImageSizeInfo( // Some ImageProvider implementations may not have given this. source: debugImageLabel ?? '<Unknown Image(${image.width}×${image.height})>', imageSize: Size(image.width.toDouble(), image.height.toDouble()), displaySize: outputSize * maxDevicePixelRatio, ); assert(() { if (debugInvertOversizedImages && sizeInfo.decodedSizeInBytes > sizeInfo.displaySizeInBytes + debugImageOverheadAllowance) { final int overheadInKilobytes = (sizeInfo.decodedSizeInBytes - sizeInfo.displaySizeInBytes) ~/ 1024; final int outputWidth = sizeInfo.displaySize.width.toInt(); final int outputHeight = sizeInfo.displaySize.height.toInt(); FlutterError.reportError(FlutterErrorDetails( exception: 'Image $debugImageLabel has a display size of ' '$outputWidth×$outputHeight but a decode size of ' '${image.width}×${image.height}, which uses an additional ' '${overheadInKilobytes}KB (assuming a device pixel ratio of ' '$maxDevicePixelRatio).\n\n' 'Consider resizing the asset ahead of time, supplying a cacheWidth ' 'parameter of $outputWidth, a cacheHeight parameter of ' '$outputHeight, or using a ResizeImage.', library: 'painting library', context: ErrorDescription('while painting an image'), )); // Invert the colors of the canvas. canvas.saveLayer( destinationRect, Paint()..colorFilter = const ColorFilter.matrix(<double>[ -1, 0, 0, 0, 255, 0, -1, 0, 0, 255, 0, 0, -1, 0, 255, 0, 0, 0, 1, 0, ]), ); // Flip the canvas vertically. final double dy = -(rect.top + rect.height / 2.0); canvas.translate(0.0, -dy); canvas.scale(1.0, -1.0); canvas.translate(0.0, dy); invertedCanvas = true; } return true; }()); // Avoid emitting events that are the same as those emitted in the last frame. if (!_lastFrameImageSizeInfo.contains(sizeInfo)) { final ImageSizeInfo? existingSizeInfo = _pendingImageSizeInfo[sizeInfo.source]; if (existingSizeInfo == null || existingSizeInfo.displaySizeInBytes < sizeInfo.displaySizeInBytes) { _pendingImageSizeInfo[sizeInfo.source!] = sizeInfo; } debugOnPaintImage?.call(sizeInfo); SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) { _lastFrameImageSizeInfo = _pendingImageSizeInfo.values.toSet(); if (_pendingImageSizeInfo.isEmpty) { return; } developer.postEvent( 'Flutter.ImageSizesForFrame', <String, Object>{ for (final ImageSizeInfo imageSizeInfo in _pendingImageSizeInfo.values) imageSizeInfo.source!: imageSizeInfo.toJson(), }, ); _pendingImageSizeInfo = <String, ImageSizeInfo>{}; }); } } final bool needSave = centerSlice != null || 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 { canvas.scale(1 / scale); if (repeat == ImageRepeat.noRepeat) { canvas.drawImageNine(image, _scaleRect(centerSlice, scale), _scaleRect(destinationRect, scale), paint); } else { for (final Rect tileRect in _generateImageTileRects(rect, destinationRect, repeat)) { canvas.drawImageNine(image, _scaleRect(centerSlice, scale), _scaleRect(tileRect, scale), paint); } } } if (needSave) { canvas.restore(); } if (invertedCanvas) { canvas.restore(); } } Iterable<Rect> _generateImageTileRects(Rect outputRect, Rect fundamentalRect, ImageRepeat repeat) { 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(); } return <Rect>[ for (int i = startX; i <= stopX; ++i) for (int j = startY; j <= stopY; ++j) fundamentalRect.shift(Offset(i * strideX, j * strideY)), ]; } Rect _scaleRect(Rect rect, double scale) => Rect.fromLTRB(rect.left * scale, rect.top * scale, rect.right * scale, rect.bottom * scale); // Implements DecorationImage.lerp when the image is different. // // This class just paints both decorations on top of each other, blended together. // // The Decoration properties are faked by just forwarded to the target image. class _BlendedDecorationImage implements DecorationImage { const _BlendedDecorationImage(this.a, this.b, this.t) : assert(a != null || b != null); final DecorationImage? a; final DecorationImage? b; final double t; @override ImageProvider get image => b?.image ?? a!.image; @override ImageErrorListener? get onError => b?.onError ?? a!.onError; @override ColorFilter? get colorFilter => b?.colorFilter ?? a!.colorFilter; @override BoxFit? get fit => b?.fit ?? a!.fit; @override AlignmentGeometry get alignment => b?.alignment ?? a!.alignment; @override Rect? get centerSlice => b?.centerSlice ?? a!.centerSlice; @override ImageRepeat get repeat => b?.repeat ?? a!.repeat; @override bool get matchTextDirection => b?.matchTextDirection ?? a!.matchTextDirection; @override double get scale => b?.scale ?? a!.scale; @override double get opacity => b?.opacity ?? a!.opacity; @override FilterQuality get filterQuality => b?.filterQuality ?? a!.filterQuality; @override bool get invertColors => b?.invertColors ?? a!.invertColors; @override bool get isAntiAlias => b?.isAntiAlias ?? a!.isAntiAlias; @override DecorationImagePainter createPainter(VoidCallback onChanged) { return _BlendedDecorationImagePainter._( a?.createPainter(onChanged), b?.createPainter(onChanged), t, ); } @override bool operator ==(Object other) { if (identical(this, other)) { return true; } if (other.runtimeType != runtimeType) { return false; } return other is _BlendedDecorationImage && other.a == a && other.b == b && other.t == t; } @override int get hashCode => Object.hash(a, b, t); @override String toString() { return '${objectRuntimeType(this, '_BlendedDecorationImage')}($a, $b, $t)'; } } class _BlendedDecorationImagePainter implements DecorationImagePainter { _BlendedDecorationImagePainter._(this.a, this.b, this.t); final DecorationImagePainter? a; final DecorationImagePainter? b; final double t; @override void paint(Canvas canvas, Rect rect, Path? clipPath, ImageConfiguration configuration, { double blend = 1.0, BlendMode blendMode = BlendMode.srcOver }) { canvas.saveLayer(null, Paint()); a?.paint(canvas, rect, clipPath, configuration, blend: blend * (1.0 - t), blendMode: blendMode); b?.paint(canvas, rect, clipPath, configuration, blend: blend * t, blendMode: a != null ? BlendMode.plus : blendMode); canvas.restore(); } @override void dispose() { a?.dispose(); b?.dispose(); } @override String toString() { return '${objectRuntimeType(this, '_BlendedDecorationImagePainter')}($a, $b, $t)'; } }