Flutter进阶|源码修改:DecorationImage 添加网络图片占位图

一、需求来源

开发需求过程中遇到一个痛点问题:Container 用 DecorationImage 加载网络图片图片时,会从无到有闪烁一下,特别是海报类弹窗,简直无法容忍。一直思考能否解决这个问题。终于在今天完美解决了。

蓝色背景图是占位图,上边的图片是加载后的网络图片。 【效果如下】

二、使用示例

dart 复制代码
NSectionBox(
  title: "EnDecorationImage - placeholder",
  child: Container(
    height: 200,
    decoration: BoxDecoration(
      border: Border.all(color: Colors.blue),
      image: EnDecorationImage(
        image: CachedNetworkImageProvider(//网图
          AppRes.image.urls[6],
          cacheKey: "${DateTime.now()}",//为了演示效果,让缓存失效
        ),
        placeholder: AssetImage(Assets.assetsImagesFlutter),//占位图
        fit: BoxFit.fill,
      ),
    ),
  ),
),

三、源码 EnDecorationImage

dart 复制代码
// 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';

/// 修改: 新增 destinationOffset 属性

/// 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,
    this.placeholder,
    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;

  /// image's placeholder. must be local image.
  final ImageProvider? placeholder;

  /// 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.placeholder == placeholder &&
        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,
        placeholder,
        colorFilter,
        fit,
        alignment,
        centerSlice,
        repeat,
        matchTextDirection,
        scale,
        opacity,
        filterQuality,
        invertColors,
        isAntiAlias,
        destinationOffset,
      );

  @override
  String toString() {
    final properties = <String>[
      '$image',
      '$placeholder',
      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;

  ImageStream? _placeholderStream;
  ImageInfo? _placeholderImage;

  late final _imageListener = ImageStreamListener(
    _handleImage,
    onError: _details.onError,
  );

  late final _placeholderListener = ImageStreamListener(
    _handlePlaceholder,
  );

  @override
  void paint(Canvas canvas, Rect rect, Path? clipPath, ImageConfiguration configuration,
      {double blend = 1.0, BlendMode blendMode = BlendMode.srcOver}) {
    var 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;
      }
    }

    if (_details.placeholder != null) {
      final placeholderStream = _details.placeholder!.resolve(configuration);
      if (placeholderStream.key != _placeholderStream?.key) {
        _placeholderStream?.removeListener(_placeholderListener);
        _placeholderStream = placeholderStream;
        _placeholderStream?.addListener(_placeholderListener);
      }
    }

    final newImageStream = _details.image.resolve(configuration);
    if (newImageStream.key != _imageStream?.key) {
      _imageStream?.removeListener(_imageListener);
      _imageStream = newImageStream;
      _imageStream!.addListener(_imageListener);
    }
    final imageInfo = _image ?? _placeholderImage;
    if (imageInfo == null) {
      return;
    }

    if (clipPath != null) {
      canvas.save();
      canvas.clipPath(clipPath);
    }

    _paintImage(
      canvas: canvas,
      rect: rect,
      image: imageInfo.image,
      debugImageLabel: imageInfo.debugLabel,
      scale: _details.scale * imageInfo.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();
    }
  }

  void _handlePlaceholder(ImageInfo value, bool synchronousCall) {
    if (_placeholderImage == value) {
      return;
    }
    _placeholderImage?.dispose();
    _placeholderImage = value;
    if (!synchronousCall) {
      _onChanged();
    }
  }

  @override
  void dispose() {
    if (kFlutterMemoryAllocationsEnabled) {
      FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this);
    }
    _imageStream?.removeListener(_imageListener);
    _image?.dispose();
    _image = null;

    _placeholderStream?.removeListener(_placeholderListener);
    _placeholderImage?.dispose();
    _placeholderStream = 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;
  }
  var outputSize = rect.size;
  var 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 = applyBoxFit(fit, inputSize / scale, outputSize);
  final sourceSize = fittedSizes.source * scale;
  var 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()..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 halfWidthDelta = (outputSize.width - destinationSize.width) / 2.0;
  final halfHeightDelta = (outputSize.height - destinationSize.height) / 2.0;
  final dx = halfWidthDelta + (flipHorizontally ? -alignment.x : alignment.x) * halfWidthDelta;
  final dy = halfHeightDelta + alignment.y * halfHeightDelta;
  final destinationPosition = rect.topLeft.translate(dx + destinationOffset.dx, dy + destinationOffset.dy);
  final destinationRect = destinationPosition & destinationSize;

  // Set to true if we added a saveLayer to the canvas to invert/flip the image.
  var 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 maxDevicePixelRatio = PaintingBinding.instance.platformDispatcher.views.fold(
      0.0,
      (double previousValue, ui.FlutterView view) => math.max(previousValue, view.devicePixelRatio),
    );
    final 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 overheadInKilobytes = (sizeInfo.decodedSizeInBytes - sizeInfo.displaySizeInBytes) ~/ 1024;
        final outputWidth = sizeInfo.displaySize.width.toInt();
        final 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 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 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 needSave = centerSlice != null || repeat != ImageRepeat.noRepeat || flipHorizontally;
  if (needSave) {
    canvas.save();
  }
  if (repeat != ImageRepeat.noRepeat) {
    canvas.clipRect(rect);
  }
  if (flipHorizontally) {
    final 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 sourceRect = alignment.inscribe(
      sourceSize,
      Offset.zero & inputSize,
    );
    if (repeat == ImageRepeat.noRepeat) {
      canvas.drawImageRect(image, sourceRect, destinationRect, paint);
    } else {
      for (final 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 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) {
  var startX = 0;
  var startY = 0;
  var stopX = 0;
  var stopY = 0;
  final strideX = fundamentalRect.width;
  final 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
  ImageProvider? get placeholder => b?.placeholder ?? a!.placeholder;
  @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)';
  }
}

最后、总结

1、实现原理是在占位图 placeholder 不为空时,先绘制占位图。接着等网络图片数据请求成功之后,用网络图片数据重新绘制一次。

2、效果用 Stack 也可以实现,但是我更新喜欢 Container,更少的层次,更好的自适应。

github

相关推荐
AlienZHOU7 小时前
AI Coding 时代下,我的技术面试实践分享
前端·后端·面试
Captaincc10 小时前
AI用量v0.1.11更新发布 新增 jusage doctor 诊断指令 托盘展示token 和余额 新增 AutoClaw 支持
前端·后端·vibecoding
计算机魔术师11 小时前
德国Wiki被黑后两周,OpenAI终于把模型失控的账本摊开了
前端
kyriewen12 小时前
我让 AI 当面试官面了我一轮:第 3 个追问我就卡住了(附 10 道追问清单)
前端·面试·ai编程
IT_陈寒12 小时前
Python的GIL把我坑惨了,多线程跑得比单线程还慢
前端·人工智能·后端
前端snow13 小时前
ai agent --- 多agent框架之图编排引擎-langgraph
前端
竹林81813 小时前
OmniPic Studio v3.2.1 核心技术架构与全平台发版解析文档
前端·浏览器
JamesZhang8007813 小时前
页面内存只涨不跌? 一次泄漏排查, 牵出 WeakMap 的诞生
前端
Z小明13 小时前
第 6 章 组件进阶
前端·vue.js
江华森13 小时前
HTTP请求的完整过程详解:从DNS解析到TCP挥手的微秒级实战分析
前端