Unverified Commit b8576323 authored by Gary Qian's avatar Gary Qian Committed by GitHub

Expose API for custom image decode and cache sizes (#41415)

parent 1ea6a38f
...@@ -9,7 +9,6 @@ import 'dart:ui' as ui; ...@@ -9,7 +9,6 @@ import 'dart:ui' as ui;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'binding.dart';
import 'debug.dart'; import 'debug.dart';
import 'image_provider.dart' as image_provider; import 'image_provider.dart' as image_provider;
import 'image_stream.dart'; import 'image_stream.dart';
...@@ -38,14 +37,14 @@ class NetworkImage extends image_provider.ImageProvider<image_provider.NetworkIm ...@@ -38,14 +37,14 @@ class NetworkImage extends image_provider.ImageProvider<image_provider.NetworkIm
} }
@override @override
ImageStreamCompleter load(image_provider.NetworkImage key) { ImageStreamCompleter load(image_provider.NetworkImage key, image_provider.DecoderCallback decode) {
// Ownership of this controller is handed off to [_loadAsync]; it is that // Ownership of this controller is handed off to [_loadAsync]; it is that
// method's responsibility to close the controller's stream when the image // method's responsibility to close the controller's stream when the image
// has been loaded or an error is thrown. // has been loaded or an error is thrown.
final StreamController<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>(); final StreamController<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>();
return MultiFrameImageStreamCompleter( return MultiFrameImageStreamCompleter(
codec: _loadAsync(key, chunkEvents), codec: _loadAsync(key, chunkEvents, decode),
chunkEvents: chunkEvents.stream, chunkEvents: chunkEvents.stream,
scale: key.scale, scale: key.scale,
informationCollector: () { informationCollector: () {
...@@ -76,6 +75,7 @@ class NetworkImage extends image_provider.ImageProvider<image_provider.NetworkIm ...@@ -76,6 +75,7 @@ class NetworkImage extends image_provider.ImageProvider<image_provider.NetworkIm
Future<ui.Codec> _loadAsync( Future<ui.Codec> _loadAsync(
NetworkImage key, NetworkImage key,
StreamController<ImageChunkEvent> chunkEvents, StreamController<ImageChunkEvent> chunkEvents,
image_provider.DecoderCallback decode,
) async { ) async {
try { try {
assert(key == this); assert(key == this);
...@@ -101,7 +101,7 @@ class NetworkImage extends image_provider.ImageProvider<image_provider.NetworkIm ...@@ -101,7 +101,7 @@ class NetworkImage extends image_provider.ImageProvider<image_provider.NetworkIm
if (bytes.lengthInBytes == 0) if (bytes.lengthInBytes == 0)
throw Exception('NetworkImage is an empty file: $resolved'); throw Exception('NetworkImage is an empty file: $resolved');
return PaintingBinding.instance.instantiateImageCodec(bytes); return decode(bytes);
} finally { } finally {
chunkEvents.close(); chunkEvents.close();
} }
......
...@@ -11,6 +11,8 @@ import 'image_provider.dart' as image_provider; ...@@ -11,6 +11,8 @@ import 'image_provider.dart' as image_provider;
import 'image_stream.dart'; import 'image_stream.dart';
/// The dart:html implemenation of [image_provider.NetworkImage]. /// The dart:html implemenation of [image_provider.NetworkImage].
///
/// NetworkImage on the web does not support decoding to a specified size.
class NetworkImage extends image_provider.ImageProvider<image_provider.NetworkImage> implements image_provider.NetworkImage { class NetworkImage extends image_provider.ImageProvider<image_provider.NetworkImage> implements image_provider.NetworkImage {
/// Creates an object that fetches the image at the given URL. /// Creates an object that fetches the image at the given URL.
/// ///
...@@ -34,9 +36,9 @@ class NetworkImage extends image_provider.ImageProvider<image_provider.NetworkIm ...@@ -34,9 +36,9 @@ class NetworkImage extends image_provider.ImageProvider<image_provider.NetworkIm
} }
@override @override
ImageStreamCompleter load(image_provider.NetworkImage key) { ImageStreamCompleter load(image_provider.NetworkImage key, image_provider.DecoderCallback decode) {
return MultiFrameImageStreamCompleter( return MultiFrameImageStreamCompleter(
codec: _loadAsync(key), codec: _loadAsync(key, decode),
scale: key.scale, scale: key.scale,
informationCollector: () { informationCollector: () {
return <DiagnosticsNode>[ return <DiagnosticsNode>[
...@@ -47,7 +49,10 @@ class NetworkImage extends image_provider.ImageProvider<image_provider.NetworkIm ...@@ -47,7 +49,10 @@ class NetworkImage extends image_provider.ImageProvider<image_provider.NetworkIm
); );
} }
Future<ui.Codec> _loadAsync(NetworkImage key) async { // Web does not support decoding network images to a specified size. The decode parameter
// here is ignored and the web-only `ui.webOnlyInstantiateImageCodecFromUrl` will be used
// directly in place of the typical `instantiateImageCodec` method.
Future<ui.Codec> _loadAsync(NetworkImage key, image_provider.DecoderCallback decode) async {
assert(key == this); assert(key == this);
final Uri resolved = Uri.base.resolve(key.url); final Uri resolved = Uri.base.resolve(key.url);
......
...@@ -70,8 +70,26 @@ mixin PaintingBinding on BindingBase, ServicesBinding { ...@@ -70,8 +70,26 @@ mixin PaintingBinding on BindingBase, ServicesBinding {
ImageCache createImageCache() => ImageCache(); ImageCache createImageCache() => ImageCache();
/// Calls through to [dart:ui] with [decodedCacheRatioCap] from [ImageCache]. /// Calls through to [dart:ui] with [decodedCacheRatioCap] from [ImageCache].
Future<ui.Codec> instantiateImageCodec(Uint8List list) { ///
return ui.instantiateImageCodec(list); /// The [cacheWidth] and [cacheHeight] parameters, when specified, indicate the
/// size to decode the image to.
///
/// Both [cacheWidth] and [cacheHeight] must be positive values greater than or
/// equal to 1 or null. It is valid to specify only one of [cacheWidth] and
/// [cacheHeight] with the other remaining null, in which case the omitted
/// dimension will decode to its original size. When both are null or omitted,
/// the image will be decoded at its native resolution.
Future<ui.Codec> instantiateImageCodec(Uint8List bytes, {
int cacheWidth,
int cacheHeight,
}) {
assert(cacheWidth == null || cacheWidth > 0);
assert(cacheHeight == null || cacheHeight > 0);
return ui.instantiateImageCodec(
bytes,
targetWidth: cacheWidth,
targetHeight: cacheHeight,
);
} }
@override @override
......
...@@ -152,6 +152,16 @@ class ImageConfiguration { ...@@ -152,6 +152,16 @@ class ImageConfiguration {
} }
} }
/// Performs the decode process for use in [ImageProvider.load].
///
/// This callback allows decoupling of the `cacheWidth` and `cacheHeight`
/// parameters from implementations of [ImageProvider] that do not use them.
///
/// See also:
///
/// * [ResizeImage], which uses this to override the `cacheWidth` and `cacheHeight` parameters.
typedef DecoderCallback = Future<ui.Codec> Function(Uint8List bytes, {int cacheWidth, int cacheHeight});
/// Identifies an image without committing to the precise final asset. This /// Identifies an image without committing to the precise final asset. This
/// allows a set of images to be identified and for the precise image to later /// allows a set of images to be identified and for the precise image to later
/// be resolved based on the environment, e.g. the device pixel ratio. /// be resolved based on the environment, e.g. the device pixel ratio.
...@@ -312,8 +322,11 @@ abstract class ImageProvider<T> { ...@@ -312,8 +322,11 @@ abstract class ImageProvider<T> {
} }
key.then<void>((T key) { key.then<void>((T key) {
obtainedKey = key; obtainedKey = key;
final ImageStreamCompleter completer = PaintingBinding.instance final ImageStreamCompleter completer = PaintingBinding.instance.imageCache.putIfAbsent(
.imageCache.putIfAbsent(key, () => load(key), onError: handleError); key,
() => load(key, PaintingBinding.instance.instantiateImageCodec),
onError: handleError,
);
if (completer != null) { if (completer != null) {
stream.setCompleter(completer); stream.setCompleter(completer);
} }
...@@ -379,8 +392,15 @@ abstract class ImageProvider<T> { ...@@ -379,8 +392,15 @@ abstract class ImageProvider<T> {
/// Converts a key into an [ImageStreamCompleter], and begins fetching the /// Converts a key into an [ImageStreamCompleter], and begins fetching the
/// image. /// image.
///
/// The [decode] callback provides the logic to obtain the codec for the
/// image.
///
/// See also:
///
/// * [ResizeImage], for modifying the key to account for cache dimensions.
@protected @protected
ImageStreamCompleter load(T key); ImageStreamCompleter load(T key, DecoderCallback decode);
@override @override
String toString() => '$runtimeType()'; String toString() => '$runtimeType()';
...@@ -444,9 +464,9 @@ abstract class AssetBundleImageProvider extends ImageProvider<AssetBundleImageKe ...@@ -444,9 +464,9 @@ abstract class AssetBundleImageProvider extends ImageProvider<AssetBundleImageKe
/// Converts a key into an [ImageStreamCompleter], and begins fetching the /// Converts a key into an [ImageStreamCompleter], and begins fetching the
/// image using [loadAsync]. /// image using [loadAsync].
@override @override
ImageStreamCompleter load(AssetBundleImageKey key) { ImageStreamCompleter load(AssetBundleImageKey key, DecoderCallback decode) {
return MultiFrameImageStreamCompleter( return MultiFrameImageStreamCompleter(
codec: _loadAsync(key), codec: _loadAsync(key, decode),
scale: key.scale, scale: key.scale,
informationCollector: () sync* { informationCollector: () sync* {
yield DiagnosticsProperty<ImageProvider>('Image provider', this); yield DiagnosticsProperty<ImageProvider>('Image provider', this);
...@@ -460,11 +480,82 @@ abstract class AssetBundleImageProvider extends ImageProvider<AssetBundleImageKe ...@@ -460,11 +480,82 @@ abstract class AssetBundleImageProvider extends ImageProvider<AssetBundleImageKe
/// ///
/// This function is used by [load]. /// This function is used by [load].
@protected @protected
Future<ui.Codec> _loadAsync(AssetBundleImageKey key) async { Future<ui.Codec> _loadAsync(AssetBundleImageKey key, DecoderCallback decode) async {
final ByteData data = await key.bundle.load(key.name); final ByteData data = await key.bundle.load(key.name);
if (data == null) if (data == null)
throw 'Unable to read data'; throw 'Unable to read data';
return await PaintingBinding.instance.instantiateImageCodec(data.buffer.asUint8List()); return await decode(data.buffer.asUint8List());
}
}
class _SizeAwareCacheKey {
const _SizeAwareCacheKey(this.providerCacheKey, this.width, this.height);
final Object providerCacheKey;
final int width;
final int height;
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType)
return false;
final _SizeAwareCacheKey typedOther = other;
return providerCacheKey == typedOther.providerCacheKey
&& width == typedOther.width
&& height == typedOther.height;
}
@override
int get hashCode => hashValues(providerCacheKey, width, height);
}
/// Instructs Flutter to decode the image at the specified dimensions
/// instead of at its native size.
///
/// This allows finer control of the size of the image in [ImageCache] and is
/// generally used to reduce the memory footprint of [ImageCache].
///
/// The decoded image may still be displayed at sizes other than the
/// cached size provided here.
class ResizeImage extends ImageProvider<_SizeAwareCacheKey> {
/// Creates an ImageProvider that decodes the image to the specified size.
///
/// The cached image will be directly decoded and stored at the resolution
/// defined by `width` and `height`. The image will lose detail and
/// use less memory if resized to a size smaller than the native size.
const ResizeImage(
this.imageProvider, {
this.width,
this.height,
}) : assert(width != null || height != null);
/// The [ImageProvider] that this class wraps.
final ImageProvider imageProvider;
/// The width the image should decode to and cache.
final int width;
/// The height the image should decode to and cache.
final int height;
@override
ImageStreamCompleter load(_SizeAwareCacheKey key, DecoderCallback decode) {
final DecoderCallback decodeResize = (Uint8List bytes, {int cacheWidth, int cacheHeight}) {
assert(
cacheWidth == null && cacheHeight == null,
'ResizeImage cannot be composed with another ImageProvider that applies cacheWidth or cacheHeight.'
);
return decode(bytes, cacheWidth: width, cacheHeight: height);
};
return imageProvider.load(key.providerCacheKey, decodeResize);
}
@override
Future<_SizeAwareCacheKey> obtainKey(ImageConfiguration configuration) async {
final Object providerCacheKey = await imageProvider.obtainKey(configuration);
return _SizeAwareCacheKey(providerCacheKey, width, height);
} }
} }
...@@ -472,6 +563,11 @@ abstract class AssetBundleImageProvider extends ImageProvider<AssetBundleImageKe ...@@ -472,6 +563,11 @@ abstract class AssetBundleImageProvider extends ImageProvider<AssetBundleImageKe
/// ///
/// The image will be cached regardless of cache headers from the server. /// The image will be cached regardless of cache headers from the server.
/// ///
/// When a network image is used on the Web platform, the [cacheWidth] and
/// [cacheHeight] parameters of the [DecoderCallback] are ignored as the Web
/// engine delegates image decoding of network images to the Web, which does
/// not support custom decode sizes.
///
/// See also: /// See also:
/// ///
/// * [Image.network] for a shorthand of an [Image] widget backed by [NetworkImage]. /// * [Image.network] for a shorthand of an [Image] widget backed by [NetworkImage].
...@@ -496,7 +592,7 @@ abstract class NetworkImage extends ImageProvider<NetworkImage> { ...@@ -496,7 +592,7 @@ abstract class NetworkImage extends ImageProvider<NetworkImage> {
Map<String, String> get headers; Map<String, String> get headers;
@override @override
ImageStreamCompleter load(NetworkImage key); ImageStreamCompleter load(NetworkImage key, DecoderCallback decode);
} }
/// Decodes the given [File] object as an image, associating it with the given /// Decodes the given [File] object as an image, associating it with the given
...@@ -525,9 +621,9 @@ class FileImage extends ImageProvider<FileImage> { ...@@ -525,9 +621,9 @@ class FileImage extends ImageProvider<FileImage> {
} }
@override @override
ImageStreamCompleter load(FileImage key) { ImageStreamCompleter load(FileImage key, DecoderCallback decode) {
return MultiFrameImageStreamCompleter( return MultiFrameImageStreamCompleter(
codec: _loadAsync(key), codec: _loadAsync(key, decode),
scale: key.scale, scale: key.scale,
informationCollector: () sync* { informationCollector: () sync* {
yield ErrorDescription('Path: ${file?.path}'); yield ErrorDescription('Path: ${file?.path}');
...@@ -535,14 +631,14 @@ class FileImage extends ImageProvider<FileImage> { ...@@ -535,14 +631,14 @@ class FileImage extends ImageProvider<FileImage> {
); );
} }
Future<ui.Codec> _loadAsync(FileImage key) async { Future<ui.Codec> _loadAsync(FileImage key, DecoderCallback decode) async {
assert(key == this); assert(key == this);
final Uint8List bytes = await file.readAsBytes(); final Uint8List bytes = await file.readAsBytes();
if (bytes.lengthInBytes == 0) if (bytes.lengthInBytes == 0)
return null; return null;
return await PaintingBinding.instance.instantiateImageCodec(bytes); return await decode(bytes);
} }
@override @override
...@@ -593,17 +689,17 @@ class MemoryImage extends ImageProvider<MemoryImage> { ...@@ -593,17 +689,17 @@ class MemoryImage extends ImageProvider<MemoryImage> {
} }
@override @override
ImageStreamCompleter load(MemoryImage key) { ImageStreamCompleter load(MemoryImage key, DecoderCallback decode) {
return MultiFrameImageStreamCompleter( return MultiFrameImageStreamCompleter(
codec: _loadAsync(key), codec: _loadAsync(key, decode),
scale: key.scale, scale: key.scale,
); );
} }
Future<ui.Codec> _loadAsync(MemoryImage key) { Future<ui.Codec> _loadAsync(MemoryImage key, DecoderCallback decode) {
assert(key == this); assert(key == this);
return PaintingBinding.instance.instantiateImageCodec(bytes); return decode(bytes);
} }
@override @override
......
...@@ -55,6 +55,13 @@ ImageConfiguration createLocalImageConfiguration(BuildContext context, { Size si ...@@ -55,6 +55,13 @@ ImageConfiguration createLocalImageConfiguration(BuildContext context, { Size si
); );
} }
ImageProvider<dynamic> _resizeIfNeeded(int cacheWidth, int cacheHeight, ImageProvider<dynamic> provider) {
if (cacheWidth != null || cacheHeight != null) {
return ResizeImage(provider, width: cacheWidth, height: cacheHeight);
}
return provider;
}
/// Prefetches an image into the image cache. /// Prefetches an image into the image cache.
/// ///
/// Returns a [Future] that will complete when the first image yielded by the /// Returns a [Future] that will complete when the first image yielded by the
...@@ -232,6 +239,17 @@ typedef ImageLoadingBuilder = Widget Function( ...@@ -232,6 +239,17 @@ typedef ImageLoadingBuilder = Widget Function(
/// ``` /// ```
/// {@end-tool} /// {@end-tool}
/// ///
/// The [Image.asset], [Image.network], [Image.file], and [Image.memory]
/// constructors allow a custom decode size to be specified through
/// [cacheWidth] and [cacheHeight] parameters. The engine will decode the
/// image to the specified size, which is primarily intended to reduce the
/// memory usage of [ImageCache].
///
/// In the case where a network image is used on the Web platform, the
/// [cacheWidth] and [cacheHeight] parameters are ignored as the Web engine
/// delegates image decoding of network images to the Web, which does not support
/// custom decode sizes.
///
/// See also: /// See also:
/// ///
/// * [Icon], which shows an image from a font. /// * [Icon], which shows an image from a font.
...@@ -305,6 +323,16 @@ class Image extends StatefulWidget { ...@@ -305,6 +323,16 @@ class Image extends StatefulWidget {
/// [FilterQuality.none] which corresponds to nearest-neighbor. /// [FilterQuality.none] which corresponds to nearest-neighbor.
/// ///
/// If [excludeFromSemantics] is true, then [semanticLabel] will be ignored. /// If [excludeFromSemantics] is true, then [semanticLabel] will be ignored.
///
/// If [cacheWidth] or [cacheHeight] are provided, it indicates to the
/// engine that the image should be decoded at the specified size. The image
/// will be rendered to the constraints of the layout or [width] and [height]
/// regardless of these parameters. These parameters are primarily intended
/// to reduce the memory usage of [ImageCache].
///
/// In the case where the network image is on the Web platform, the [cacheWidth]
/// and [cacheHeight] parameters are ignored as the web engine delegates
/// image decoding to the web which does not support custom decode sizes.
Image.network( Image.network(
String src, { String src, {
Key key, Key key,
...@@ -325,10 +353,14 @@ class Image extends StatefulWidget { ...@@ -325,10 +353,14 @@ class Image extends StatefulWidget {
this.gaplessPlayback = false, this.gaplessPlayback = false,
this.filterQuality = FilterQuality.low, this.filterQuality = FilterQuality.low,
Map<String, String> headers, Map<String, String> headers,
}) : image = NetworkImage(src, scale: scale, headers: headers), int cacheWidth,
int cacheHeight,
}) : image = _resizeIfNeeded(cacheWidth, cacheHeight, NetworkImage(src, scale: scale, headers: headers)),
assert(alignment != null), assert(alignment != null),
assert(repeat != null), assert(repeat != null),
assert(matchTextDirection != null), assert(matchTextDirection != null),
assert(cacheWidth == null || cacheWidth > 0),
assert(cacheHeight == null || cacheHeight > 0),
super(key: key); super(key: key);
/// Creates a widget that displays an [ImageStream] obtained from a [File]. /// Creates a widget that displays an [ImageStream] obtained from a [File].
...@@ -349,6 +381,12 @@ class Image extends StatefulWidget { ...@@ -349,6 +381,12 @@ class Image extends StatefulWidget {
/// [FilterQuality.none] which corresponds to nearest-neighbor. /// [FilterQuality.none] which corresponds to nearest-neighbor.
/// ///
/// If [excludeFromSemantics] is true, then [semanticLabel] will be ignored. /// If [excludeFromSemantics] is true, then [semanticLabel] will be ignored.
///
/// If [cacheWidth] or [cacheHeight] are provided, it indicates to the
/// engine that the image must be decoded at the specified size. The image
/// will be rendered to the constraints of the layout or [width] and [height]
/// regardless of these parameters. These parameters are primarily intended
/// to reduce the memory usage of [ImageCache].
Image.file( Image.file(
File file, { File file, {
Key key, Key key,
...@@ -367,12 +405,16 @@ class Image extends StatefulWidget { ...@@ -367,12 +405,16 @@ class Image extends StatefulWidget {
this.matchTextDirection = false, this.matchTextDirection = false,
this.gaplessPlayback = false, this.gaplessPlayback = false,
this.filterQuality = FilterQuality.low, this.filterQuality = FilterQuality.low,
}) : image = FileImage(file, scale: scale), int cacheWidth,
int cacheHeight,
}) : image = _resizeIfNeeded(cacheWidth, cacheHeight, FileImage(file, scale: scale)),
loadingBuilder = null, loadingBuilder = null,
assert(alignment != null), assert(alignment != null),
assert(repeat != null), assert(repeat != null),
assert(filterQuality != null), assert(filterQuality != null),
assert(matchTextDirection != null), assert(matchTextDirection != null),
assert(cacheWidth == null || cacheWidth > 0),
assert(cacheHeight == null || cacheHeight > 0),
super(key: key); super(key: key);
...@@ -404,6 +446,12 @@ class Image extends StatefulWidget { ...@@ -404,6 +446,12 @@ class Image extends StatefulWidget {
/// ///
/// If [excludeFromSemantics] is true, then [semanticLabel] will be ignored. /// If [excludeFromSemantics] is true, then [semanticLabel] will be ignored.
/// ///
/// If [cacheWidth] or [cacheHeight] are provided, it indicates to the
/// engine that the image must be decoded at the specified size. The image
/// will be rendered to the constraints of the layout or [width] and [height]
/// regardless of these parameters. These parameters are primarily intended
/// to reduce the memory usage of [ImageCache].
///
/// The [name] and [repeat] arguments must not be null. /// The [name] and [repeat] arguments must not be null.
/// ///
/// Either the [width] and [height] arguments should be specified, or the /// Either the [width] and [height] arguments should be specified, or the
...@@ -520,13 +568,18 @@ class Image extends StatefulWidget { ...@@ -520,13 +568,18 @@ class Image extends StatefulWidget {
this.gaplessPlayback = false, this.gaplessPlayback = false,
String package, String package,
this.filterQuality = FilterQuality.low, this.filterQuality = FilterQuality.low,
}) : image = scale != null int cacheWidth,
int cacheHeight,
}) : image = _resizeIfNeeded(cacheWidth, cacheHeight, scale != null
? ExactAssetImage(name, bundle: bundle, scale: scale, package: package) ? ExactAssetImage(name, bundle: bundle, scale: scale, package: package)
: AssetImage(name, bundle: bundle, package: package), : AssetImage(name, bundle: bundle, package: package)
),
loadingBuilder = null, loadingBuilder = null,
assert(alignment != null), assert(alignment != null),
assert(repeat != null), assert(repeat != null),
assert(matchTextDirection != null), assert(matchTextDirection != null),
assert(cacheWidth == null || cacheWidth > 0),
assert(cacheHeight == null || cacheHeight > 0),
super(key: key); super(key: key);
/// Creates a widget that displays an [ImageStream] obtained from a [Uint8List]. /// Creates a widget that displays an [ImageStream] obtained from a [Uint8List].
...@@ -548,6 +601,12 @@ class Image extends StatefulWidget { ...@@ -548,6 +601,12 @@ class Image extends StatefulWidget {
/// [FilterQuality.none] which corresponds to nearest-neighbor. /// [FilterQuality.none] which corresponds to nearest-neighbor.
/// ///
/// If [excludeFromSemantics] is true, then [semanticLabel] will be ignored. /// If [excludeFromSemantics] is true, then [semanticLabel] will be ignored.
///
/// If [cacheWidth] or [cacheHeight] are provided, it indicates to the
/// engine that the image must be decoded at the specified size. The image
/// will be rendered to the constraints of the layout or [width] and [height]
/// regardless of these parameters. These parameters are primarily intended
/// to reduce the memory usage of [ImageCache].
Image.memory( Image.memory(
Uint8List bytes, { Uint8List bytes, {
Key key, Key key,
...@@ -566,11 +625,15 @@ class Image extends StatefulWidget { ...@@ -566,11 +625,15 @@ class Image extends StatefulWidget {
this.matchTextDirection = false, this.matchTextDirection = false,
this.gaplessPlayback = false, this.gaplessPlayback = false,
this.filterQuality = FilterQuality.low, this.filterQuality = FilterQuality.low,
}) : image = MemoryImage(bytes, scale: scale), int cacheWidth,
int cacheHeight,
}) : image = _resizeIfNeeded(cacheWidth, cacheHeight, MemoryImage(bytes, scale: scale)),
loadingBuilder = null, loadingBuilder = null,
assert(alignment != null), assert(alignment != null),
assert(repeat != null), assert(repeat != null),
assert(matchTextDirection != null), assert(matchTextDirection != null),
assert(cacheWidth == null || cacheWidth > 0),
assert(cacheHeight == null || cacheHeight > 0),
super(key: key); super(key: key);
/// The image to display. /// The image to display.
......
...@@ -19,7 +19,9 @@ void main() { ...@@ -19,7 +19,9 @@ void main() {
final Uint8List bytes = Uint8List.fromList(kTransparentImage); final Uint8List bytes = Uint8List.fromList(kTransparentImage);
final MemoryImage memoryImage = MemoryImage(bytes); final MemoryImage memoryImage = MemoryImage(bytes);
memoryImage.load(memoryImage); memoryImage.load(memoryImage, (Uint8List bytes, {int cacheWidth, int cacheHeight}) {
return PaintingBinding.instance.instantiateImageCodec(bytes, cacheWidth: cacheWidth, cacheHeight: cacheHeight);
});
expect(binding.instantiateImageCodecCalledCount, 1); expect(binding.instantiateImageCodecCalledCount, 1);
}); });
} }
...@@ -33,7 +33,7 @@ class SynchronousTestImageProvider extends ImageProvider<int> { ...@@ -33,7 +33,7 @@ class SynchronousTestImageProvider extends ImageProvider<int> {
} }
@override @override
ImageStreamCompleter load(int key) { ImageStreamCompleter load(int key, DecoderCallback decode) {
return OneFrameImageStreamCompleter( return OneFrameImageStreamCompleter(
SynchronousFuture<ImageInfo>(TestImageInfo(key, image: TestImage(), scale: 1.0)) SynchronousFuture<ImageInfo>(TestImageInfo(key, image: TestImage(), scale: 1.0))
); );
...@@ -47,7 +47,7 @@ class AsyncTestImageProvider extends ImageProvider<int> { ...@@ -47,7 +47,7 @@ class AsyncTestImageProvider extends ImageProvider<int> {
} }
@override @override
ImageStreamCompleter load(int key) { ImageStreamCompleter load(int key, DecoderCallback decode) {
return OneFrameImageStreamCompleter( return OneFrameImageStreamCompleter(
Future<ImageInfo>.value(TestImageInfo(key)) Future<ImageInfo>.value(TestImageInfo(key))
); );
...@@ -63,7 +63,7 @@ class DelayedImageProvider extends ImageProvider<DelayedImageProvider> { ...@@ -63,7 +63,7 @@ class DelayedImageProvider extends ImageProvider<DelayedImageProvider> {
} }
@override @override
ImageStreamCompleter load(DelayedImageProvider key) { ImageStreamCompleter load(DelayedImageProvider key, DecoderCallback decode) {
return OneFrameImageStreamCompleter(_completer.future); return OneFrameImageStreamCompleter(_completer.future);
} }
......
...@@ -26,7 +26,7 @@ class FakeImageProvider extends ImageProvider<FakeImageProvider> { ...@@ -26,7 +26,7 @@ class FakeImageProvider extends ImageProvider<FakeImageProvider> {
} }
@override @override
ImageStreamCompleter load(FakeImageProvider key) { ImageStreamCompleter load(FakeImageProvider key, DecoderCallback decode) {
assert(key == this); assert(key == this);
return MultiFrameImageStreamCompleter( return MultiFrameImageStreamCompleter(
codec: SynchronousFuture<ui.Codec>(_codec), codec: SynchronousFuture<ui.Codec>(_codec),
......
...@@ -133,9 +133,9 @@ void main() { ...@@ -133,9 +133,9 @@ void main() {
test('Returns null if an error is caught resolving an image', () { test('Returns null if an error is caught resolving an image', () {
final ErrorImageProvider errorImage = ErrorImageProvider(); final ErrorImageProvider errorImage = ErrorImageProvider();
expect(() => imageCache.putIfAbsent(errorImage, () => errorImage.load(errorImage)), throwsA(isInstanceOf<Error>())); expect(() => imageCache.putIfAbsent(errorImage, () => errorImage.load(errorImage, null)), throwsA(isInstanceOf<Error>()));
bool caughtError = false; bool caughtError = false;
final ImageStreamCompleter result = imageCache.putIfAbsent(errorImage, () => errorImage.load(errorImage), onError: (dynamic error, StackTrace stackTrace) { final ImageStreamCompleter result = imageCache.putIfAbsent(errorImage, () => errorImage.load(errorImage, null), onError: (dynamic error, StackTrace stackTrace) {
caughtError = true; caughtError = true;
}); });
expect(result, null); expect(result, null);
......
...@@ -18,6 +18,11 @@ import 'image_data.dart'; ...@@ -18,6 +18,11 @@ import 'image_data.dart';
import 'mocks_for_image_cache.dart'; import 'mocks_for_image_cache.dart';
void main() { void main() {
final DecoderCallback basicDecoder = (Uint8List bytes, {int cacheWidth, int cacheHeight}) {
return PaintingBinding.instance.instantiateImageCodec(bytes, cacheWidth: cacheWidth, cacheHeight: cacheHeight);
};
group(ImageProvider, () { group(ImageProvider, () {
setUpAll(() { setUpAll(() {
TestRenderingFlutterBinding(); // initializes the imageCache TestRenderingFlutterBinding(); // initializes the imageCache
...@@ -46,7 +51,7 @@ void main() { ...@@ -46,7 +51,7 @@ void main() {
final Uint8List bytes = Uint8List.fromList(kTransparentImage); final Uint8List bytes = Uint8List.fromList(kTransparentImage);
final MemoryImage imageProvider = MemoryImage(bytes); final MemoryImage imageProvider = MemoryImage(bytes);
final ImageStreamCompleter cacheStream = otherCache.putIfAbsent( final ImageStreamCompleter cacheStream = otherCache.putIfAbsent(
imageProvider, () => imageProvider.load(imageProvider), imageProvider, () => imageProvider.load(imageProvider, basicDecoder),
); );
final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty); final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty);
final Completer<void> completer = Completer<void>(); final Completer<void> completer = Completer<void>();
...@@ -195,7 +200,7 @@ void main() { ...@@ -195,7 +200,7 @@ void main() {
Future<void> loadNetworkImage() async { Future<void> loadNetworkImage() async {
final NetworkImage networkImage = NetworkImage(nonconst('foo')); final NetworkImage networkImage = NetworkImage(nonconst('foo'));
final ImageStreamCompleter completer = networkImage.load(networkImage); final ImageStreamCompleter completer = networkImage.load(networkImage, basicDecoder);
completer.addListener(ImageStreamListener( completer.addListener(ImageStreamListener(
(ImageInfo image, bool synchronousCall) { }, (ImageInfo image, bool synchronousCall) { },
onError: (dynamic error, StackTrace stackTrace) { onError: (dynamic error, StackTrace stackTrace) {
...@@ -293,6 +298,83 @@ void main() { ...@@ -293,6 +298,83 @@ void main() {
}, skip: isBrowser); }, skip: isBrowser);
}); });
}); });
test('ResizeImage resizes to the correct dimensions', () async {
final Uint8List bytes = Uint8List.fromList(kTransparentImage);
final MemoryImage imageProvider = MemoryImage(bytes);
final Size rawImageSize = await _resolveAndGetSize(imageProvider);
expect(rawImageSize, const Size(1, 1));
const Size resizeDims = Size(14, 7);
final ResizeImage resizedImage = ResizeImage(MemoryImage(bytes), width: resizeDims.width.round(), height: resizeDims.height.round());
const ImageConfiguration resizeConfig = ImageConfiguration(size: resizeDims);
final Size resizedImageSize = await _resolveAndGetSize(resizedImage, configuration: resizeConfig);
expect(resizedImageSize, resizeDims);
});
test('ResizeImage does not resize when no size is passed', () async {
final Uint8List bytes = Uint8List.fromList(kTransparentImage);
final MemoryImage imageProvider = MemoryImage(bytes);
final Size rawImageSize = await _resolveAndGetSize(imageProvider);
expect(rawImageSize, const Size(1, 1));
// Cannot pass in two null arguments for cache dimensions, so will use the regular
// MemoryImage
final MemoryImage resizedImage = MemoryImage(bytes);
final Size resizedImageSize = await _resolveAndGetSize(resizedImage);
expect(resizedImageSize, const Size(1, 1));
});
test('ResizeImage stores values', () async {
final Uint8List bytes = Uint8List.fromList(kTransparentImage);
final MemoryImage memoryImage = MemoryImage(bytes);
final ResizeImage resizeImage = ResizeImage(memoryImage, width: 10, height: 20);
expect(resizeImage.width, 10);
expect(resizeImage.height, 20);
expect(resizeImage.imageProvider, memoryImage);
expect(memoryImage.resolve(ImageConfiguration.empty) != resizeImage.resolve(ImageConfiguration.empty), true);
});
test('ResizeImage takes one dim', () async {
final Uint8List bytes = Uint8List.fromList(kTransparentImage);
final MemoryImage memoryImage = MemoryImage(bytes);
final ResizeImage resizeImage = ResizeImage(memoryImage, width: 10, height: null);
expect(resizeImage.width, 10);
expect(resizeImage.height, null);
expect(resizeImage.imageProvider, memoryImage);
expect(memoryImage.resolve(ImageConfiguration.empty) != resizeImage.resolve(ImageConfiguration.empty), true);
});
test('ResizeImage forms closure', () async {
final Uint8List bytes = Uint8List.fromList(kTransparentImage);
final MemoryImage memoryImage = MemoryImage(bytes);
final ResizeImage resizeImage = ResizeImage(memoryImage, width: 123, height: 321);
final DecoderCallback decode = (Uint8List bytes, {int cacheWidth, int cacheHeight}) {
expect(cacheWidth, 123);
expect(cacheHeight, 321);
return PaintingBinding.instance.instantiateImageCodec(bytes, cacheWidth: cacheWidth, cacheHeight: cacheHeight);
};
resizeImage.load(await resizeImage.obtainKey(ImageConfiguration.empty), decode);
});
}
Future<Size> _resolveAndGetSize(ImageProvider imageProvider,
{ImageConfiguration configuration = ImageConfiguration.empty}) async {
final ImageStream stream = imageProvider.resolve(configuration);
final Completer<Size> completer = Completer<Size>();
final ImageStreamListener listener =
ImageStreamListener((ImageInfo image, bool synchronousCall) {
final int height = image.image.height;
final int width = image.image.width;
completer.complete(Size(width.toDouble(), height.toDouble()));
}
);
stream.addListener(listener);
return await completer.future;
} }
class MockHttpClient extends Mock implements HttpClient {} class MockHttpClient extends Mock implements HttpClient {}
......
...@@ -31,7 +31,7 @@ class TestImageProvider extends ImageProvider<TestImageProvider> { ...@@ -31,7 +31,7 @@ class TestImageProvider extends ImageProvider<TestImageProvider> {
} }
@override @override
ImageStreamCompleter load(TestImageProvider key) => ImageStreamCompleter load(TestImageProvider key, DecoderCallback decode) =>
OneFrameImageStreamCompleter(_completer.future); OneFrameImageStreamCompleter(_completer.future);
ImageInfo complete() { ImageInfo complete() {
......
...@@ -37,7 +37,7 @@ class TestImageProvider extends ImageProvider<int> { ...@@ -37,7 +37,7 @@ class TestImageProvider extends ImageProvider<int> {
} }
@override @override
ImageStreamCompleter load(int key) { ImageStreamCompleter load(int key, DecoderCallback decode) {
return OneFrameImageStreamCompleter( return OneFrameImageStreamCompleter(
SynchronousFuture<ImageInfo>(TestImageInfo(imageValue, image: image)) SynchronousFuture<ImageInfo>(TestImageInfo(imageValue, image: image))
); );
...@@ -51,7 +51,7 @@ class FailingTestImageProvider extends TestImageProvider { ...@@ -51,7 +51,7 @@ class FailingTestImageProvider extends TestImageProvider {
const FailingTestImageProvider(int key, int imageValue, { ui.Image image }) : super(key, imageValue, image: image); const FailingTestImageProvider(int key, int imageValue, { ui.Image image }) : super(key, imageValue, image: image);
@override @override
ImageStreamCompleter load(int key) { ImageStreamCompleter load(int key, DecoderCallback decode) {
return OneFrameImageStreamCompleter(Future<ImageInfo>.sync(() => Future<ImageInfo>.error('loading failed!'))); return OneFrameImageStreamCompleter(Future<ImageInfo>.sync(() => Future<ImageInfo>.error('loading failed!')));
} }
} }
...@@ -85,7 +85,7 @@ class TestImage implements ui.Image { ...@@ -85,7 +85,7 @@ class TestImage implements ui.Image {
class ErrorImageProvider extends ImageProvider<ErrorImageProvider> { class ErrorImageProvider extends ImageProvider<ErrorImageProvider> {
@override @override
ImageStreamCompleter load(ErrorImageProvider key) { ImageStreamCompleter load(ErrorImageProvider key, DecoderCallback decode) {
throw Error(); throw Error();
} }
...@@ -97,7 +97,7 @@ class ErrorImageProvider extends ImageProvider<ErrorImageProvider> { ...@@ -97,7 +97,7 @@ class ErrorImageProvider extends ImageProvider<ErrorImageProvider> {
class ObtainKeyErrorImageProvider extends ImageProvider<ObtainKeyErrorImageProvider> { class ObtainKeyErrorImageProvider extends ImageProvider<ObtainKeyErrorImageProvider> {
@override @override
ImageStreamCompleter load(ObtainKeyErrorImageProvider key) { ImageStreamCompleter load(ObtainKeyErrorImageProvider key, DecoderCallback decode) {
throw Error(); throw Error();
} }
...@@ -109,7 +109,7 @@ class ObtainKeyErrorImageProvider extends ImageProvider<ObtainKeyErrorImageProvi ...@@ -109,7 +109,7 @@ class ObtainKeyErrorImageProvider extends ImageProvider<ObtainKeyErrorImageProvi
class LoadErrorImageProvider extends ImageProvider<LoadErrorImageProvider> { class LoadErrorImageProvider extends ImageProvider<LoadErrorImageProvider> {
@override @override
ImageStreamCompleter load(LoadErrorImageProvider key) { ImageStreamCompleter load(LoadErrorImageProvider key, DecoderCallback decode) {
throw Error(); throw Error();
} }
...@@ -121,7 +121,7 @@ class LoadErrorImageProvider extends ImageProvider<LoadErrorImageProvider> { ...@@ -121,7 +121,7 @@ class LoadErrorImageProvider extends ImageProvider<LoadErrorImageProvider> {
class LoadErrorCompleterImageProvider extends ImageProvider<LoadErrorCompleterImageProvider> { class LoadErrorCompleterImageProvider extends ImageProvider<LoadErrorCompleterImageProvider> {
@override @override
ImageStreamCompleter load(LoadErrorCompleterImageProvider key) { ImageStreamCompleter load(LoadErrorCompleterImageProvider key, DecoderCallback decode) {
final Completer<void> completer = Completer<void>.sync(); final Completer<void> completer = Completer<void>.sync();
completer.completeError(Error()); completer.completeError(Error());
return OneFrameImageStreamCompleter(completer.future); return OneFrameImageStreamCompleter(completer.future);
......
...@@ -14,7 +14,7 @@ class PaintingBindingSpy extends BindingBase with ServicesBinding, PaintingBindi ...@@ -14,7 +14,7 @@ class PaintingBindingSpy extends BindingBase with ServicesBinding, PaintingBindi
int get instantiateImageCodecCalledCount => counter; int get instantiateImageCodecCalledCount => counter;
@override @override
Future<ui.Codec> instantiateImageCodec(Uint8List list) { Future<ui.Codec> instantiateImageCodec(Uint8List list, {int cacheWidth, int cacheHeight}) {
counter++; counter++;
return ui.instantiateImageCodec(list); return ui.instantiateImageCodec(list);
} }
......
...@@ -107,7 +107,7 @@ class TestImageProvider extends ImageProvider<TestImageProvider> { ...@@ -107,7 +107,7 @@ class TestImageProvider extends ImageProvider<TestImageProvider> {
} }
@override @override
ImageStreamCompleter load(TestImageProvider key) { ImageStreamCompleter load(TestImageProvider key, DecoderCallback decode) {
return OneFrameImageStreamCompleter( return OneFrameImageStreamCompleter(
SynchronousFuture<ImageInfo>(ImageInfo(image: TestImage(), scale: 1.0)), SynchronousFuture<ImageInfo>(ImageInfo(image: TestImage(), scale: 1.0)),
); );
......
...@@ -28,7 +28,7 @@ class TestImageProvider extends ImageProvider<TestImageProvider> { ...@@ -28,7 +28,7 @@ class TestImageProvider extends ImageProvider<TestImageProvider> {
} }
@override @override
ImageStreamCompleter load(TestImageProvider key) { ImageStreamCompleter load(TestImageProvider key, DecoderCallback decode) {
return OneFrameImageStreamCompleter( return OneFrameImageStreamCompleter(
future.then<ImageInfo>((void value) => ImageInfo(image: image)) future.then<ImageInfo>((void value) => ImageInfo(image: image))
); );
......
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
const List<int> kTransparentImage = <int>[
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49,
0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06,
0x00, 0x00, 0x00, 0x1F, 0x15, 0xC4, 0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44,
0x41, 0x54, 0x78, 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00, 0x01, 0x0D,
0x0A, 0x2D, 0xB4, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE,
];
/// An animated GIF image with 3 1x1 pixel frames (a red, green, and blue
/// frames). The gif animates forever, and each frame has a 100ms delay.
const List<int> kAnimatedGif = <int> [
0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x01, 0x00, 0x01, 0x00, 0xa1, 0x03, 0x00,
0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0xff, 0xff, 0xff, 0x21,
0xff, 0x0b, 0x4e, 0x45, 0x54, 0x53, 0x43, 0x41, 0x50, 0x45, 0x32, 0x2e, 0x30,
0x03, 0x01, 0x00, 0x00, 0x00, 0x21, 0xf9, 0x04, 0x00, 0x0a, 0x00, 0xff, 0x00,
0x2c, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x02, 0x02, 0x4c,
0x01, 0x00, 0x21, 0xf9, 0x04, 0x00, 0x0a, 0x00, 0xff, 0x00, 0x2c, 0x00, 0x00,
0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x02, 0x02, 0x54, 0x01, 0x00, 0x21,
0xf9, 0x04, 0x00, 0x0a, 0x00, 0xff, 0x00, 0x2c, 0x00, 0x00, 0x00, 0x00, 0x01,
0x00, 0x01, 0x00, 0x00, 0x02, 0x02, 0x44, 0x01, 0x00, 0x3b,
];
...@@ -13,6 +13,8 @@ import 'package:flutter/services.dart'; ...@@ -13,6 +13,8 @@ import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'image_data.dart';
class TestImage implements ui.Image { class TestImage implements ui.Image {
TestImage(this.scale); TestImage(this.scale);
final double scale; final double scale;
...@@ -104,7 +106,7 @@ class TestAssetImage extends AssetImage { ...@@ -104,7 +106,7 @@ class TestAssetImage extends AssetImage {
TestAssetImage(String name) : super(name); TestAssetImage(String name) : super(name);
@override @override
ImageStreamCompleter load(AssetBundleImageKey key) { ImageStreamCompleter load(AssetBundleImageKey key, DecoderCallback decode) {
ImageInfo imageInfo; ImageInfo imageInfo;
key.bundle.load(key.name).then<void>((ByteData data) { key.bundle.load(key.name).then<void>((ByteData data) {
final TestByteData testData = data; final TestByteData testData = data;
...@@ -150,6 +152,30 @@ Widget buildImageAtRatio(String image, Key key, double ratio, bool inferSize, [ ...@@ -150,6 +152,30 @@ Widget buildImageAtRatio(String image, Key key, double ratio, bool inferSize, [
); );
} }
Widget buildImageCacheResized(String name, Key key, int width, int height, int cacheWidth, int cacheHeight) {
return Center(
child: RepaintBoundary(
child: Container(
width: 250,
height: 250,
child: Center(
child: Image.memory(
Uint8List.fromList(kTransparentImage),
key: key,
excludeFromSemantics: true,
color: const Color(0xFF00FFFF),
colorBlendMode: BlendMode.plus,
width: width.toDouble(),
height: height.toDouble(),
cacheWidth: cacheWidth,
cacheHeight: cacheHeight,
),
),
),
),
);
}
RenderImage getRenderImage(WidgetTester tester, Key key) { RenderImage getRenderImage(WidgetTester tester, Key key) {
return tester.renderObject<RenderImage>(find.byKey(key)); return tester.renderObject<RenderImage>(find.byKey(key));
} }
...@@ -303,4 +329,22 @@ void main() { ...@@ -303,4 +329,22 @@ void main() {
expect(getTestImage(tester, key).scale, 10.0); expect(getTestImage(tester, key).scale, 10.0);
}); });
testWidgets('Image cache resize upscale display 5', (WidgetTester tester) async {
final Key key = GlobalKey();
await pumpTreeToLayout(tester, buildImageCacheResized(image, key, 5, 5, 20, 20));
expect(getRenderImage(tester, key).size, const Size(5.0, 5.0));
});
testWidgets('Image cache resize upscale display 50', (WidgetTester tester) async {
final Key key = GlobalKey();
await pumpTreeToLayout(tester, buildImageCacheResized(image, key, 50, 50, 20, 20));
expect(getRenderImage(tester, key).size, const Size(50.0, 50.0));
});
testWidgets('Image cache resize downscale display 5', (WidgetTester tester) async {
final Key key = GlobalKey();
await pumpTreeToLayout(tester, buildImageCacheResized(image, key, 5, 5, 1, 1));
expect(getRenderImage(tester, key).size, const Size(5.0, 5.0));
});
} }
...@@ -19,7 +19,7 @@ class TestImageProvider extends ImageProvider<TestImageProvider> { ...@@ -19,7 +19,7 @@ class TestImageProvider extends ImageProvider<TestImageProvider> {
} }
@override @override
ImageStreamCompleter load(TestImageProvider key) { ImageStreamCompleter load(TestImageProvider key, DecoderCallback decode) {
return OneFrameImageStreamCompleter( return OneFrameImageStreamCompleter(
SynchronousFuture<ImageInfo>(ImageInfo(image: TestImage())) SynchronousFuture<ImageInfo>(ImageInfo(image: TestImage()))
); );
......
...@@ -1199,7 +1199,7 @@ class TestImageProvider extends ImageProvider<TestImageProvider> { ...@@ -1199,7 +1199,7 @@ class TestImageProvider extends ImageProvider<TestImageProvider> {
} }
@override @override
ImageStreamCompleter load(TestImageProvider key) => _streamCompleter; ImageStreamCompleter load(TestImageProvider key, DecoderCallback decode) => _streamCompleter;
void complete() { void complete() {
_completer.complete(ImageInfo(image: TestImage())); _completer.complete(ImageInfo(image: TestImage()));
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment