@@ -9,7 +9,6 @@ import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'binding.dart';
|
||||
import 'debug.dart';
|
||||
import 'image_provider.dart' as image_provider;
|
||||
import 'image_stream.dart';
|
||||
@@ -38,14 +37,14 @@ class NetworkImage extends image_provider.ImageProvider<image_provider.NetworkIm
|
||||
}
|
||||
|
||||
@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
|
||||
// method's responsibility to close the controller's stream when the image
|
||||
// has been loaded or an error is thrown.
|
||||
final StreamController<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>();
|
||||
|
||||
return MultiFrameImageStreamCompleter(
|
||||
codec: _loadAsync(key, chunkEvents),
|
||||
codec: _loadAsync(key, chunkEvents, decode),
|
||||
chunkEvents: chunkEvents.stream,
|
||||
scale: key.scale,
|
||||
informationCollector: () {
|
||||
@@ -76,6 +75,7 @@ class NetworkImage extends image_provider.ImageProvider<image_provider.NetworkIm
|
||||
Future<ui.Codec> _loadAsync(
|
||||
NetworkImage key,
|
||||
StreamController<ImageChunkEvent> chunkEvents,
|
||||
image_provider.DecoderCallback decode,
|
||||
) async {
|
||||
try {
|
||||
assert(key == this);
|
||||
@@ -101,7 +101,7 @@ class NetworkImage extends image_provider.ImageProvider<image_provider.NetworkIm
|
||||
if (bytes.lengthInBytes == 0)
|
||||
throw Exception('NetworkImage is an empty file: $resolved');
|
||||
|
||||
return PaintingBinding.instance.instantiateImageCodec(bytes);
|
||||
return decode(bytes);
|
||||
} finally {
|
||||
chunkEvents.close();
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ import 'image_provider.dart' as image_provider;
|
||||
import 'image_stream.dart';
|
||||
|
||||
/// 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 {
|
||||
/// Creates an object that fetches the image at the given URL.
|
||||
///
|
||||
@@ -34,9 +36,9 @@ class NetworkImage extends image_provider.ImageProvider<image_provider.NetworkIm
|
||||
}
|
||||
|
||||
@override
|
||||
ImageStreamCompleter load(image_provider.NetworkImage key) {
|
||||
ImageStreamCompleter load(image_provider.NetworkImage key, image_provider.DecoderCallback decode) {
|
||||
return MultiFrameImageStreamCompleter(
|
||||
codec: _loadAsync(key),
|
||||
codec: _loadAsync(key, decode),
|
||||
scale: key.scale,
|
||||
informationCollector: () {
|
||||
return <DiagnosticsNode>[
|
||||
@@ -47,7 +49,13 @@ class NetworkImage extends image_provider.ImageProvider<image_provider.NetworkIm
|
||||
);
|
||||
}
|
||||
|
||||
Future<ui.Codec> _loadAsync(NetworkImage key) async {
|
||||
// TODO(garyq): We should eventually support custom decoding of network images on Web as
|
||||
// well, see https://github.com/flutter/flutter/issues/42789.
|
||||
//
|
||||
// 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);
|
||||
|
||||
final Uri resolved = Uri.base.resolve(key.url);
|
||||
|
||||
@@ -70,8 +70,26 @@ mixin PaintingBinding on BindingBase, ServicesBinding {
|
||||
ImageCache createImageCache() => 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
|
||||
|
||||
@@ -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
|
||||
/// 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.
|
||||
@@ -312,8 +322,11 @@ abstract class ImageProvider<T> {
|
||||
}
|
||||
key.then<void>((T key) {
|
||||
obtainedKey = key;
|
||||
final ImageStreamCompleter completer = PaintingBinding.instance
|
||||
.imageCache.putIfAbsent(key, () => load(key), onError: handleError);
|
||||
final ImageStreamCompleter completer = PaintingBinding.instance.imageCache.putIfAbsent(
|
||||
key,
|
||||
() => load(key, PaintingBinding.instance.instantiateImageCodec),
|
||||
onError: handleError,
|
||||
);
|
||||
if (completer != null) {
|
||||
stream.setCompleter(completer);
|
||||
}
|
||||
@@ -379,8 +392,15 @@ abstract class ImageProvider<T> {
|
||||
|
||||
/// Converts a key into an [ImageStreamCompleter], and begins fetching the
|
||||
/// 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
|
||||
ImageStreamCompleter load(T key);
|
||||
ImageStreamCompleter load(T key, DecoderCallback decode);
|
||||
|
||||
@override
|
||||
String toString() => '$runtimeType()';
|
||||
@@ -444,9 +464,9 @@ abstract class AssetBundleImageProvider extends ImageProvider<AssetBundleImageKe
|
||||
/// Converts a key into an [ImageStreamCompleter], and begins fetching the
|
||||
/// image using [loadAsync].
|
||||
@override
|
||||
ImageStreamCompleter load(AssetBundleImageKey key) {
|
||||
ImageStreamCompleter load(AssetBundleImageKey key, DecoderCallback decode) {
|
||||
return MultiFrameImageStreamCompleter(
|
||||
codec: _loadAsync(key),
|
||||
codec: _loadAsync(key, decode),
|
||||
scale: key.scale,
|
||||
informationCollector: () sync* {
|
||||
yield DiagnosticsProperty<ImageProvider>('Image provider', this);
|
||||
@@ -460,11 +480,82 @@ abstract class AssetBundleImageProvider extends ImageProvider<AssetBundleImageKe
|
||||
///
|
||||
/// This function is used by [load].
|
||||
@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);
|
||||
if (data == null)
|
||||
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
|
||||
///
|
||||
/// 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:
|
||||
///
|
||||
/// * [Image.network] for a shorthand of an [Image] widget backed by [NetworkImage].
|
||||
@@ -496,7 +592,7 @@ abstract class NetworkImage extends ImageProvider<NetworkImage> {
|
||||
Map<String, String> get headers;
|
||||
|
||||
@override
|
||||
ImageStreamCompleter load(NetworkImage key);
|
||||
ImageStreamCompleter load(NetworkImage key, DecoderCallback decode);
|
||||
}
|
||||
|
||||
/// Decodes the given [File] object as an image, associating it with the given
|
||||
@@ -525,9 +621,9 @@ class FileImage extends ImageProvider<FileImage> {
|
||||
}
|
||||
|
||||
@override
|
||||
ImageStreamCompleter load(FileImage key) {
|
||||
ImageStreamCompleter load(FileImage key, DecoderCallback decode) {
|
||||
return MultiFrameImageStreamCompleter(
|
||||
codec: _loadAsync(key),
|
||||
codec: _loadAsync(key, decode),
|
||||
scale: key.scale,
|
||||
informationCollector: () sync* {
|
||||
yield ErrorDescription('Path: ${file?.path}');
|
||||
@@ -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);
|
||||
|
||||
final Uint8List bytes = await file.readAsBytes();
|
||||
if (bytes.lengthInBytes == 0)
|
||||
return null;
|
||||
|
||||
return await PaintingBinding.instance.instantiateImageCodec(bytes);
|
||||
return await decode(bytes);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -593,17 +689,17 @@ class MemoryImage extends ImageProvider<MemoryImage> {
|
||||
}
|
||||
|
||||
@override
|
||||
ImageStreamCompleter load(MemoryImage key) {
|
||||
ImageStreamCompleter load(MemoryImage key, DecoderCallback decode) {
|
||||
return MultiFrameImageStreamCompleter(
|
||||
codec: _loadAsync(key),
|
||||
codec: _loadAsync(key, decode),
|
||||
scale: key.scale,
|
||||
);
|
||||
}
|
||||
|
||||
Future<ui.Codec> _loadAsync(MemoryImage key) {
|
||||
Future<ui.Codec> _loadAsync(MemoryImage key, DecoderCallback decode) {
|
||||
assert(key == this);
|
||||
|
||||
return PaintingBinding.instance.instantiateImageCodec(bytes);
|
||||
return decode(bytes);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -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.
|
||||
///
|
||||
/// Returns a [Future] that will complete when the first image yielded by the
|
||||
@@ -232,6 +239,17 @@ typedef ImageLoadingBuilder = Widget Function(
|
||||
/// ```
|
||||
/// {@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:
|
||||
///
|
||||
/// * [Icon], which shows an image from a font.
|
||||
@@ -305,6 +323,19 @@ class Image extends StatefulWidget {
|
||||
/// [FilterQuality.none] which corresponds to nearest-neighbor.
|
||||
///
|
||||
/// 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.
|
||||
//
|
||||
// TODO(garyq): We should eventually support custom decoding of network images
|
||||
// on Web as well, see https://github.com/flutter/flutter/issues/42789.
|
||||
Image.network(
|
||||
String src, {
|
||||
Key key,
|
||||
@@ -325,10 +356,14 @@ class Image extends StatefulWidget {
|
||||
this.gaplessPlayback = false,
|
||||
this.filterQuality = FilterQuality.low,
|
||||
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(repeat != null),
|
||||
assert(matchTextDirection != null),
|
||||
assert(cacheWidth == null || cacheWidth > 0),
|
||||
assert(cacheHeight == null || cacheHeight > 0),
|
||||
super(key: key);
|
||||
|
||||
/// Creates a widget that displays an [ImageStream] obtained from a [File].
|
||||
@@ -349,6 +384,12 @@ class Image extends StatefulWidget {
|
||||
/// [FilterQuality.none] which corresponds to nearest-neighbor.
|
||||
///
|
||||
/// 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(
|
||||
File file, {
|
||||
Key key,
|
||||
@@ -367,12 +408,16 @@ class Image extends StatefulWidget {
|
||||
this.matchTextDirection = false,
|
||||
this.gaplessPlayback = false,
|
||||
this.filterQuality = FilterQuality.low,
|
||||
}) : image = FileImage(file, scale: scale),
|
||||
int cacheWidth,
|
||||
int cacheHeight,
|
||||
}) : image = _resizeIfNeeded(cacheWidth, cacheHeight, FileImage(file, scale: scale)),
|
||||
loadingBuilder = null,
|
||||
assert(alignment != null),
|
||||
assert(repeat != null),
|
||||
assert(filterQuality != null),
|
||||
assert(matchTextDirection != null),
|
||||
assert(cacheWidth == null || cacheWidth > 0),
|
||||
assert(cacheHeight == null || cacheHeight > 0),
|
||||
super(key: key);
|
||||
|
||||
|
||||
@@ -404,6 +449,12 @@ class Image extends StatefulWidget {
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
/// Either the [width] and [height] arguments should be specified, or the
|
||||
@@ -520,13 +571,18 @@ class Image extends StatefulWidget {
|
||||
this.gaplessPlayback = false,
|
||||
String package,
|
||||
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)
|
||||
: AssetImage(name, bundle: bundle, package: package),
|
||||
: AssetImage(name, bundle: bundle, package: package)
|
||||
),
|
||||
loadingBuilder = null,
|
||||
assert(alignment != null),
|
||||
assert(repeat != null),
|
||||
assert(matchTextDirection != null),
|
||||
assert(cacheWidth == null || cacheWidth > 0),
|
||||
assert(cacheHeight == null || cacheHeight > 0),
|
||||
super(key: key);
|
||||
|
||||
/// Creates a widget that displays an [ImageStream] obtained from a [Uint8List].
|
||||
@@ -548,6 +604,12 @@ class Image extends StatefulWidget {
|
||||
/// [FilterQuality.none] which corresponds to nearest-neighbor.
|
||||
///
|
||||
/// 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(
|
||||
Uint8List bytes, {
|
||||
Key key,
|
||||
@@ -566,11 +628,15 @@ class Image extends StatefulWidget {
|
||||
this.matchTextDirection = false,
|
||||
this.gaplessPlayback = false,
|
||||
this.filterQuality = FilterQuality.low,
|
||||
}) : image = MemoryImage(bytes, scale: scale),
|
||||
int cacheWidth,
|
||||
int cacheHeight,
|
||||
}) : image = _resizeIfNeeded(cacheWidth, cacheHeight, MemoryImage(bytes, scale: scale)),
|
||||
loadingBuilder = null,
|
||||
assert(alignment != null),
|
||||
assert(repeat != null),
|
||||
assert(matchTextDirection != null),
|
||||
assert(cacheWidth == null || cacheWidth > 0),
|
||||
assert(cacheHeight == null || cacheHeight > 0),
|
||||
super(key: key);
|
||||
|
||||
/// The image to display.
|
||||
|
||||
@@ -19,7 +19,9 @@ void main() {
|
||||
|
||||
final Uint8List bytes = Uint8List.fromList(kTransparentImage);
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ class SynchronousTestImageProvider extends ImageProvider<int> {
|
||||
}
|
||||
|
||||
@override
|
||||
ImageStreamCompleter load(int key) {
|
||||
ImageStreamCompleter load(int key, DecoderCallback decode) {
|
||||
return OneFrameImageStreamCompleter(
|
||||
SynchronousFuture<ImageInfo>(TestImageInfo(key, image: TestImage(), scale: 1.0))
|
||||
);
|
||||
@@ -47,7 +47,7 @@ class AsyncTestImageProvider extends ImageProvider<int> {
|
||||
}
|
||||
|
||||
@override
|
||||
ImageStreamCompleter load(int key) {
|
||||
ImageStreamCompleter load(int key, DecoderCallback decode) {
|
||||
return OneFrameImageStreamCompleter(
|
||||
Future<ImageInfo>.value(TestImageInfo(key))
|
||||
);
|
||||
@@ -63,7 +63,7 @@ class DelayedImageProvider extends ImageProvider<DelayedImageProvider> {
|
||||
}
|
||||
|
||||
@override
|
||||
ImageStreamCompleter load(DelayedImageProvider key) {
|
||||
ImageStreamCompleter load(DelayedImageProvider key, DecoderCallback decode) {
|
||||
return OneFrameImageStreamCompleter(_completer.future);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ class FakeImageProvider extends ImageProvider<FakeImageProvider> {
|
||||
}
|
||||
|
||||
@override
|
||||
ImageStreamCompleter load(FakeImageProvider key) {
|
||||
ImageStreamCompleter load(FakeImageProvider key, DecoderCallback decode) {
|
||||
assert(key == this);
|
||||
return MultiFrameImageStreamCompleter(
|
||||
codec: SynchronousFuture<ui.Codec>(_codec),
|
||||
|
||||
@@ -133,9 +133,9 @@ void main() {
|
||||
|
||||
test('Returns null if an error is caught resolving an image', () {
|
||||
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;
|
||||
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;
|
||||
});
|
||||
expect(result, null);
|
||||
|
||||
@@ -18,6 +18,11 @@ import 'image_data.dart';
|
||||
import 'mocks_for_image_cache.dart';
|
||||
|
||||
void main() {
|
||||
|
||||
final DecoderCallback basicDecoder = (Uint8List bytes, {int cacheWidth, int cacheHeight}) {
|
||||
return PaintingBinding.instance.instantiateImageCodec(bytes, cacheWidth: cacheWidth, cacheHeight: cacheHeight);
|
||||
};
|
||||
|
||||
group(ImageProvider, () {
|
||||
setUpAll(() {
|
||||
TestRenderingFlutterBinding(); // initializes the imageCache
|
||||
@@ -46,7 +51,7 @@ void main() {
|
||||
final Uint8List bytes = Uint8List.fromList(kTransparentImage);
|
||||
final MemoryImage imageProvider = MemoryImage(bytes);
|
||||
final ImageStreamCompleter cacheStream = otherCache.putIfAbsent(
|
||||
imageProvider, () => imageProvider.load(imageProvider),
|
||||
imageProvider, () => imageProvider.load(imageProvider, basicDecoder),
|
||||
);
|
||||
final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty);
|
||||
final Completer<void> completer = Completer<void>();
|
||||
@@ -195,7 +200,7 @@ void main() {
|
||||
|
||||
Future<void> loadNetworkImage() async {
|
||||
final NetworkImage networkImage = NetworkImage(nonconst('foo'));
|
||||
final ImageStreamCompleter completer = networkImage.load(networkImage);
|
||||
final ImageStreamCompleter completer = networkImage.load(networkImage, basicDecoder);
|
||||
completer.addListener(ImageStreamListener(
|
||||
(ImageInfo image, bool synchronousCall) { },
|
||||
onError: (dynamic error, StackTrace stackTrace) {
|
||||
@@ -293,6 +298,83 @@ void main() {
|
||||
}, 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);
|
||||
}, skip: isBrowser);
|
||||
|
||||
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));
|
||||
}, skip: isBrowser);
|
||||
|
||||
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 {}
|
||||
|
||||
@@ -31,7 +31,7 @@ class TestImageProvider extends ImageProvider<TestImageProvider> {
|
||||
}
|
||||
|
||||
@override
|
||||
ImageStreamCompleter load(TestImageProvider key) =>
|
||||
ImageStreamCompleter load(TestImageProvider key, DecoderCallback decode) =>
|
||||
OneFrameImageStreamCompleter(_completer.future);
|
||||
|
||||
ImageInfo complete() {
|
||||
|
||||
@@ -37,7 +37,7 @@ class TestImageProvider extends ImageProvider<int> {
|
||||
}
|
||||
|
||||
@override
|
||||
ImageStreamCompleter load(int key) {
|
||||
ImageStreamCompleter load(int key, DecoderCallback decode) {
|
||||
return OneFrameImageStreamCompleter(
|
||||
SynchronousFuture<ImageInfo>(TestImageInfo(imageValue, image: image))
|
||||
);
|
||||
@@ -51,7 +51,7 @@ class FailingTestImageProvider extends TestImageProvider {
|
||||
const FailingTestImageProvider(int key, int imageValue, { ui.Image image }) : super(key, imageValue, image: image);
|
||||
|
||||
@override
|
||||
ImageStreamCompleter load(int key) {
|
||||
ImageStreamCompleter load(int key, DecoderCallback decode) {
|
||||
return OneFrameImageStreamCompleter(Future<ImageInfo>.sync(() => Future<ImageInfo>.error('loading failed!')));
|
||||
}
|
||||
}
|
||||
@@ -85,7 +85,7 @@ class TestImage implements ui.Image {
|
||||
|
||||
class ErrorImageProvider extends ImageProvider<ErrorImageProvider> {
|
||||
@override
|
||||
ImageStreamCompleter load(ErrorImageProvider key) {
|
||||
ImageStreamCompleter load(ErrorImageProvider key, DecoderCallback decode) {
|
||||
throw Error();
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ class ErrorImageProvider extends ImageProvider<ErrorImageProvider> {
|
||||
|
||||
class ObtainKeyErrorImageProvider extends ImageProvider<ObtainKeyErrorImageProvider> {
|
||||
@override
|
||||
ImageStreamCompleter load(ObtainKeyErrorImageProvider key) {
|
||||
ImageStreamCompleter load(ObtainKeyErrorImageProvider key, DecoderCallback decode) {
|
||||
throw Error();
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ class ObtainKeyErrorImageProvider extends ImageProvider<ObtainKeyErrorImageProvi
|
||||
|
||||
class LoadErrorImageProvider extends ImageProvider<LoadErrorImageProvider> {
|
||||
@override
|
||||
ImageStreamCompleter load(LoadErrorImageProvider key) {
|
||||
ImageStreamCompleter load(LoadErrorImageProvider key, DecoderCallback decode) {
|
||||
throw Error();
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ class LoadErrorImageProvider extends ImageProvider<LoadErrorImageProvider> {
|
||||
|
||||
class LoadErrorCompleterImageProvider extends ImageProvider<LoadErrorCompleterImageProvider> {
|
||||
@override
|
||||
ImageStreamCompleter load(LoadErrorCompleterImageProvider key) {
|
||||
ImageStreamCompleter load(LoadErrorCompleterImageProvider key, DecoderCallback decode) {
|
||||
final Completer<void> completer = Completer<void>.sync();
|
||||
completer.completeError(Error());
|
||||
return OneFrameImageStreamCompleter(completer.future);
|
||||
|
||||
@@ -14,7 +14,7 @@ class PaintingBindingSpy extends BindingBase with ServicesBinding, PaintingBindi
|
||||
int get instantiateImageCodecCalledCount => counter;
|
||||
|
||||
@override
|
||||
Future<ui.Codec> instantiateImageCodec(Uint8List list) {
|
||||
Future<ui.Codec> instantiateImageCodec(Uint8List list, {int cacheWidth, int cacheHeight}) {
|
||||
counter++;
|
||||
return ui.instantiateImageCodec(list);
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ class TestImageProvider extends ImageProvider<TestImageProvider> {
|
||||
}
|
||||
|
||||
@override
|
||||
ImageStreamCompleter load(TestImageProvider key) {
|
||||
ImageStreamCompleter load(TestImageProvider key, DecoderCallback decode) {
|
||||
return OneFrameImageStreamCompleter(
|
||||
SynchronousFuture<ImageInfo>(ImageInfo(image: TestImage(), scale: 1.0)),
|
||||
);
|
||||
|
||||
@@ -28,7 +28,7 @@ class TestImageProvider extends ImageProvider<TestImageProvider> {
|
||||
}
|
||||
|
||||
@override
|
||||
ImageStreamCompleter load(TestImageProvider key) {
|
||||
ImageStreamCompleter load(TestImageProvider key, DecoderCallback decode) {
|
||||
return OneFrameImageStreamCompleter(
|
||||
future.then<ImageInfo>((void value) => ImageInfo(image: image))
|
||||
);
|
||||
|
||||
25
packages/flutter/test/widgets/image_data.dart
Normal file
25
packages/flutter/test/widgets/image_data.dart
Normal file
@@ -0,0 +1,25 @@
|
||||
// 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';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'image_data.dart';
|
||||
|
||||
class TestImage implements ui.Image {
|
||||
TestImage(this.scale);
|
||||
final double scale;
|
||||
@@ -104,7 +106,7 @@ class TestAssetImage extends AssetImage {
|
||||
TestAssetImage(String name) : super(name);
|
||||
|
||||
@override
|
||||
ImageStreamCompleter load(AssetBundleImageKey key) {
|
||||
ImageStreamCompleter load(AssetBundleImageKey key, DecoderCallback decode) {
|
||||
ImageInfo imageInfo;
|
||||
key.bundle.load(key.name).then<void>((ByteData data) {
|
||||
final TestByteData testData = data;
|
||||
@@ -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) {
|
||||
return tester.renderObject<RenderImage>(find.byKey(key));
|
||||
}
|
||||
@@ -303,4 +329,22 @@ void main() {
|
||||
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> {
|
||||
}
|
||||
|
||||
@override
|
||||
ImageStreamCompleter load(TestImageProvider key) {
|
||||
ImageStreamCompleter load(TestImageProvider key, DecoderCallback decode) {
|
||||
return OneFrameImageStreamCompleter(
|
||||
SynchronousFuture<ImageInfo>(ImageInfo(image: TestImage()))
|
||||
);
|
||||
|
||||
@@ -1199,7 +1199,7 @@ class TestImageProvider extends ImageProvider<TestImageProvider> {
|
||||
}
|
||||
|
||||
@override
|
||||
ImageStreamCompleter load(TestImageProvider key) => _streamCompleter;
|
||||
ImageStreamCompleter load(TestImageProvider key, DecoderCallback decode) => _streamCompleter;
|
||||
|
||||
void complete() {
|
||||
_completer.complete(ImageInfo(image: TestImage()));
|
||||
|
||||
Reference in New Issue
Block a user