Flutter进阶|源码修改:给 DecorationImage 源码添加偏移量
2026年3月14日 07:52
一、需求来源
开发需求过程中遇到一个痛点问题:某些卡片需要添加一个背景水印类图片(例如公司 Logo),一般在右上角,距离边界有一定间距。我一般用 Container 实现,但是 DecorationImage 仅支持对齐方式,不支持微调设定间距。研究源码之后我通过给其添加一个偏移量属性 destinationOffset 实现最终效果。
【默认效果】
![]()
【最终效果】
![]()
二、使用示例
Widget buildSectionEnDecorationImage() {
const msg = "静夜思 * 李白 • 床前明月光, 疑是地上霜, 举头望明月, 低头思故乡.";
return Container(
height: 100,
decoration: BoxDecoration(
border: Border.all(color: Colors.blue),
image: EnDecorationImage(
image: "img_update.png".toAssetImage(),
alignment: Alignment.topRight,
colorFilter: ColorFilter.mode(Colors.green, BlendMode.dstATop),
destinationOffset: Offset(-16, -12),
),
),
alignment: Alignment.centerRight, //卡片内文字居中
child: Text(
msg,
style: TextStyle(color: Colors.red),
),
);
}
三、源码 EnDecorationImage
// 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.
/// @docImport 'package:flutter/rendering.dart';
/// @docImport 'package:flutter/widgets.dart';
///
/// @docImport 'box_decoration.dart';
/// @docImport 'image_resolution.dart';
library;
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/material.dart';
import 'package:flutter/scheduler.dart';
/// 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 EnDecorationImage extends DecorationImage {
/// Creates an image to show in a [BoxDecoration].
const EnDecorationImage({
required super.image,
super.onError,
super.colorFilter,
super.fit,
super.alignment = Alignment.center,
super.centerSlice,
super.repeat = ImageRepeat.noRepeat,
super.matchTextDirection = false,
super.scale = 1.0,
super.opacity = 1.0,
super.filterQuality = FilterQuality.medium,
super.invertColors = false,
super.isAntiAlias = false,
this.destinationOffset = Offset.zero,
});
/// only translate dx, dy.
final Offset destinationOffset;
/// Creates a [DecorationImagePainter] for this [EnDecorationImage].
///
/// The `onChanged` argument will be called whenever the image needs to be
/// repainted, e.g. because it is loading incrementally or because it is
/// animated.
@override
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 EnDecorationImage &&
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 &&
other.destinationOffset == destinationOffset;
}
@override
int get hashCode => Object.hash(
image,
colorFilter,
fit,
alignment,
centerSlice,
repeat,
matchTextDirection,
scale,
opacity,
filterQuality,
invertColors,
isAntiAlias,
destinationOffset,
);
@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',
'destinationOffset $destinationOffset',
];
return '${objectRuntimeType(this, 'DecorationImage')}(${properties.join(", ")})';
}
/// Linearly interpolates between two [EnDecorationImage]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 EnDecorationImage? lerp(EnDecorationImage? a, EnDecorationImage? 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 [EnDecorationImage].
///
/// To obtain a painter, call [EnDecorationImage.createPainter].
///
/// To paint, call [paint]. The `onChanged` callback passed to
/// [EnDecorationImage.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 EnDecorationImagePainter {
// /// 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
// /// [EnDecorationImage.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 [EnDecorationImage.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) {
// TODO(polina-c): stop duplicating code across disposables
// https://github.com/flutter/flutter/issues/137435
if (kFlutterMemoryAllocationsEnabled) {
FlutterMemoryAllocations.instance.dispatchObjectCreated(
library: 'package:flutter/painting.dart',
className: '$_DecorationImagePainter',
object: this,
);
}
}
final EnDecorationImage _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<EnDecorationImage>('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,
destinationOffset: _details.destinationOffset,
);
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() {
if (kFlutterMemoryAllocationsEnabled) {
FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this);
}
_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.
/// Defaults to [FilterQuality.medium].
///
/// See also:
///
/// * [paintBorder], which paints a border around a rectangle on a canvas.
/// * [EnDecorationImage], which holds a configuration for calling this function.
/// * [BoxDecoration], which uses this function to paint a [EnDecorationImage].
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.medium,
bool isAntiAlias = false,
BlendMode blendMode = BlendMode.srcOver,
Offset destinationOffset = Offset.zero,
}) {
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 + destinationOffset.dx, dy + destinationOffset.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>{};
}, debugLabel: 'paintImage.recordImageSizes');
}
}
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 EnDecorationImage {
const _BlendedDecorationImage(this.a, this.b, this.t) : assert(a != null || b != null);
final EnDecorationImage? a;
final EnDecorationImage? 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
Offset get destinationOffset => b?.destinationOffset ?? a!.destinationOffset;
@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) {
// TODO(polina-c): stop duplicating code across disposables
// https://github.com/flutter/flutter/issues/137435
if (kFlutterMemoryAllocationsEnabled) {
FlutterMemoryAllocations.instance.dispatchObjectCreated(
library: 'package:flutter/painting.dart',
className: '$_BlendedDecorationImagePainter',
object: this,
);
}
}
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() {
if (kFlutterMemoryAllocationsEnabled) {
FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this);
}
a?.dispose();
b?.dispose();
}
@override
String toString() {
return '${objectRuntimeType(this, '_BlendedDecorationImagePainter')}($a, $b, $t)';
}
}