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

相关推荐
小新1101 小时前
vue 实战项目 天气查询
前端·javascript·vue.js
7yue1 小时前
用 TypScript 学习 Claude Code
前端·typescript·claude
Rain5091 小时前
实战:搭建 AI Code Review 自动化流水线
前端·人工智能·git·ci/cd·自动化·ai编程·代码复审
Nian_Baikal1 小时前
从零搭建离线地图服务:Nginx + Cesium/Leaflet 实战指南
前端
问心无愧05131 小时前
ctf show web入门99
android·前端·笔记
用户600071819101 小时前
【翻译】CSS 与 JavaScript:动画性能该怎么选
前端
用户059540174461 小时前
GitHub Actions 自动化测试流水线踩坑实录:一个 `&&` 符号,折腾了 4 小时,但前端事故率降为 0
前端·css
还有多久拿退休金1 小时前
一行命令切换 Claude Code 的 AI 大脑:告别繁琐的 provider 切换流程
前端·ai编程