diff --git a/packages/flutter/lib/src/painting/_network_image_io.dart b/packages/flutter/lib/src/painting/_network_image_io.dart index 2b2c4a72cf..d976b9335b 100644 --- a/packages/flutter/lib/src/painting/_network_image_io.dart +++ b/packages/flutter/lib/src/painting/_network_image_io.dart @@ -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 chunkEvents = StreamController(); 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 _loadAsync( NetworkImage key, StreamController chunkEvents, + image_provider.DecoderCallback decode, ) async { try { assert(key == this); @@ -101,7 +101,7 @@ class NetworkImage extends image_provider.ImageProvider 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[ @@ -47,7 +49,13 @@ class NetworkImage extends image_provider.ImageProvider _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 _loadAsync(NetworkImage key, image_provider.DecoderCallback decode) async { assert(key == this); final Uri resolved = Uri.base.resolve(key.url); diff --git a/packages/flutter/lib/src/painting/binding.dart b/packages/flutter/lib/src/painting/binding.dart index 4eb915d65f..c0223734fb 100644 --- a/packages/flutter/lib/src/painting/binding.dart +++ b/packages/flutter/lib/src/painting/binding.dart @@ -70,8 +70,26 @@ mixin PaintingBinding on BindingBase, ServicesBinding { ImageCache createImageCache() => ImageCache(); /// Calls through to [dart:ui] with [decodedCacheRatioCap] from [ImageCache]. - Future 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 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 diff --git a/packages/flutter/lib/src/painting/image_provider.dart b/packages/flutter/lib/src/painting/image_provider.dart index a08e4c9176..69790b24d6 100644 --- a/packages/flutter/lib/src/painting/image_provider.dart +++ b/packages/flutter/lib/src/painting/image_provider.dart @@ -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 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 { } key.then((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 { /// 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('Image provider', this); @@ -460,11 +480,82 @@ abstract class AssetBundleImageProvider extends ImageProvider _loadAsync(AssetBundleImageKey key) async { + Future _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 { Map 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 { } @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 { ); } - Future _loadAsync(FileImage key) async { + Future _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 { } @override - ImageStreamCompleter load(MemoryImage key) { + ImageStreamCompleter load(MemoryImage key, DecoderCallback decode) { return MultiFrameImageStreamCompleter( - codec: _loadAsync(key), + codec: _loadAsync(key, decode), scale: key.scale, ); } - Future _loadAsync(MemoryImage key) { + Future _loadAsync(MemoryImage key, DecoderCallback decode) { assert(key == this); - return PaintingBinding.instance.instantiateImageCodec(bytes); + return decode(bytes); } @override diff --git a/packages/flutter/lib/src/widgets/image.dart b/packages/flutter/lib/src/widgets/image.dart index 9dfa4a59ab..9acb5c99fa 100644 --- a/packages/flutter/lib/src/widgets/image.dart +++ b/packages/flutter/lib/src/widgets/image.dart @@ -55,6 +55,13 @@ ImageConfiguration createLocalImageConfiguration(BuildContext context, { Size si ); } +ImageProvider _resizeIfNeeded(int cacheWidth, int cacheHeight, ImageProvider 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 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. diff --git a/packages/flutter/test/painting/binding_test.dart b/packages/flutter/test/painting/binding_test.dart index ea46e6068e..d849abd9d7 100644 --- a/packages/flutter/test/painting/binding_test.dart +++ b/packages/flutter/test/painting/binding_test.dart @@ -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); }); } diff --git a/packages/flutter/test/painting/decoration_test.dart b/packages/flutter/test/painting/decoration_test.dart index c5ae1e5d05..872a8ca68c 100644 --- a/packages/flutter/test/painting/decoration_test.dart +++ b/packages/flutter/test/painting/decoration_test.dart @@ -33,7 +33,7 @@ class SynchronousTestImageProvider extends ImageProvider { } @override - ImageStreamCompleter load(int key) { + ImageStreamCompleter load(int key, DecoderCallback decode) { return OneFrameImageStreamCompleter( SynchronousFuture(TestImageInfo(key, image: TestImage(), scale: 1.0)) ); @@ -47,7 +47,7 @@ class AsyncTestImageProvider extends ImageProvider { } @override - ImageStreamCompleter load(int key) { + ImageStreamCompleter load(int key, DecoderCallback decode) { return OneFrameImageStreamCompleter( Future.value(TestImageInfo(key)) ); @@ -63,7 +63,7 @@ class DelayedImageProvider extends ImageProvider { } @override - ImageStreamCompleter load(DelayedImageProvider key) { + ImageStreamCompleter load(DelayedImageProvider key, DecoderCallback decode) { return OneFrameImageStreamCompleter(_completer.future); } diff --git a/packages/flutter/test/painting/fake_image_provider.dart b/packages/flutter/test/painting/fake_image_provider.dart index 58ebb1a6b8..a8937e278f 100644 --- a/packages/flutter/test/painting/fake_image_provider.dart +++ b/packages/flutter/test/painting/fake_image_provider.dart @@ -26,7 +26,7 @@ class FakeImageProvider extends ImageProvider { } @override - ImageStreamCompleter load(FakeImageProvider key) { + ImageStreamCompleter load(FakeImageProvider key, DecoderCallback decode) { assert(key == this); return MultiFrameImageStreamCompleter( codec: SynchronousFuture(_codec), diff --git a/packages/flutter/test/painting/image_cache_test.dart b/packages/flutter/test/painting/image_cache_test.dart index 13b14bf861..da8758ea16 100644 --- a/packages/flutter/test/painting/image_cache_test.dart +++ b/packages/flutter/test/painting/image_cache_test.dart @@ -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())); + expect(() => imageCache.putIfAbsent(errorImage, () => errorImage.load(errorImage, null)), throwsA(isInstanceOf())); 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); diff --git a/packages/flutter/test/painting/image_provider_test.dart b/packages/flutter/test/painting/image_provider_test.dart index 3571dcf5c6..424bd7febf 100644 --- a/packages/flutter/test/painting/image_provider_test.dart +++ b/packages/flutter/test/painting/image_provider_test.dart @@ -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 completer = Completer(); @@ -195,7 +200,7 @@ void main() { Future 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 _resolveAndGetSize(ImageProvider imageProvider, + {ImageConfiguration configuration = ImageConfiguration.empty}) async { + final ImageStream stream = imageProvider.resolve(configuration); + final Completer completer = Completer(); + 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 {} diff --git a/packages/flutter/test/painting/image_test_utils.dart b/packages/flutter/test/painting/image_test_utils.dart index bce8798e7e..923fd14dbf 100644 --- a/packages/flutter/test/painting/image_test_utils.dart +++ b/packages/flutter/test/painting/image_test_utils.dart @@ -31,7 +31,7 @@ class TestImageProvider extends ImageProvider { } @override - ImageStreamCompleter load(TestImageProvider key) => + ImageStreamCompleter load(TestImageProvider key, DecoderCallback decode) => OneFrameImageStreamCompleter(_completer.future); ImageInfo complete() { diff --git a/packages/flutter/test/painting/mocks_for_image_cache.dart b/packages/flutter/test/painting/mocks_for_image_cache.dart index 656967e8eb..dfc8901e8d 100644 --- a/packages/flutter/test/painting/mocks_for_image_cache.dart +++ b/packages/flutter/test/painting/mocks_for_image_cache.dart @@ -37,7 +37,7 @@ class TestImageProvider extends ImageProvider { } @override - ImageStreamCompleter load(int key) { + ImageStreamCompleter load(int key, DecoderCallback decode) { return OneFrameImageStreamCompleter( SynchronousFuture(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.sync(() => Future.error('loading failed!'))); } } @@ -85,7 +85,7 @@ class TestImage implements ui.Image { class ErrorImageProvider extends ImageProvider { @override - ImageStreamCompleter load(ErrorImageProvider key) { + ImageStreamCompleter load(ErrorImageProvider key, DecoderCallback decode) { throw Error(); } @@ -97,7 +97,7 @@ class ErrorImageProvider extends ImageProvider { class ObtainKeyErrorImageProvider extends ImageProvider { @override - ImageStreamCompleter load(ObtainKeyErrorImageProvider key) { + ImageStreamCompleter load(ObtainKeyErrorImageProvider key, DecoderCallback decode) { throw Error(); } @@ -109,7 +109,7 @@ class ObtainKeyErrorImageProvider extends ImageProvider { @override - ImageStreamCompleter load(LoadErrorImageProvider key) { + ImageStreamCompleter load(LoadErrorImageProvider key, DecoderCallback decode) { throw Error(); } @@ -121,7 +121,7 @@ class LoadErrorImageProvider extends ImageProvider { class LoadErrorCompleterImageProvider extends ImageProvider { @override - ImageStreamCompleter load(LoadErrorCompleterImageProvider key) { + ImageStreamCompleter load(LoadErrorCompleterImageProvider key, DecoderCallback decode) { final Completer completer = Completer.sync(); completer.completeError(Error()); return OneFrameImageStreamCompleter(completer.future); diff --git a/packages/flutter/test/painting/painting_utils.dart b/packages/flutter/test/painting/painting_utils.dart index 378fb0dacb..ba6f953ccd 100644 --- a/packages/flutter/test/painting/painting_utils.dart +++ b/packages/flutter/test/painting/painting_utils.dart @@ -14,7 +14,7 @@ class PaintingBindingSpy extends BindingBase with ServicesBinding, PaintingBindi int get instantiateImageCodecCalledCount => counter; @override - Future instantiateImageCodec(Uint8List list) { + Future instantiateImageCodec(Uint8List list, {int cacheWidth, int cacheHeight}) { counter++; return ui.instantiateImageCodec(list); } diff --git a/packages/flutter/test/painting/shape_decoration_test.dart b/packages/flutter/test/painting/shape_decoration_test.dart index dbdcb62b57..c3f02d8e54 100644 --- a/packages/flutter/test/painting/shape_decoration_test.dart +++ b/packages/flutter/test/painting/shape_decoration_test.dart @@ -107,7 +107,7 @@ class TestImageProvider extends ImageProvider { } @override - ImageStreamCompleter load(TestImageProvider key) { + ImageStreamCompleter load(TestImageProvider key, DecoderCallback decode) { return OneFrameImageStreamCompleter( SynchronousFuture(ImageInfo(image: TestImage(), scale: 1.0)), ); diff --git a/packages/flutter/test/widgets/box_decoration_test.dart b/packages/flutter/test/widgets/box_decoration_test.dart index 110fd3de3b..be7cd3e478 100644 --- a/packages/flutter/test/widgets/box_decoration_test.dart +++ b/packages/flutter/test/widgets/box_decoration_test.dart @@ -28,7 +28,7 @@ class TestImageProvider extends ImageProvider { } @override - ImageStreamCompleter load(TestImageProvider key) { + ImageStreamCompleter load(TestImageProvider key, DecoderCallback decode) { return OneFrameImageStreamCompleter( future.then((void value) => ImageInfo(image: image)) ); diff --git a/packages/flutter/test/widgets/image_data.dart b/packages/flutter/test/widgets/image_data.dart new file mode 100644 index 0000000000..83a83af271 --- /dev/null +++ b/packages/flutter/test/widgets/image_data.dart @@ -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 kTransparentImage = [ + 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 kAnimatedGif = [ + 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, +]; diff --git a/packages/flutter/test/widgets/image_resolution_test.dart b/packages/flutter/test/widgets/image_resolution_test.dart index ebf2db3e82..4508e6d0f8 100644 --- a/packages/flutter/test/widgets/image_resolution_test.dart +++ b/packages/flutter/test/widgets/image_resolution_test.dart @@ -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((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(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)); + }); + } diff --git a/packages/flutter/test/widgets/image_rtl_test.dart b/packages/flutter/test/widgets/image_rtl_test.dart index c2e6daf411..cd7222e65e 100644 --- a/packages/flutter/test/widgets/image_rtl_test.dart +++ b/packages/flutter/test/widgets/image_rtl_test.dart @@ -19,7 +19,7 @@ class TestImageProvider extends ImageProvider { } @override - ImageStreamCompleter load(TestImageProvider key) { + ImageStreamCompleter load(TestImageProvider key, DecoderCallback decode) { return OneFrameImageStreamCompleter( SynchronousFuture(ImageInfo(image: TestImage())) ); diff --git a/packages/flutter/test/widgets/image_test.dart b/packages/flutter/test/widgets/image_test.dart index da2a2f0997..5a400fe227 100644 --- a/packages/flutter/test/widgets/image_test.dart +++ b/packages/flutter/test/widgets/image_test.dart @@ -1199,7 +1199,7 @@ class TestImageProvider extends ImageProvider { } @override - ImageStreamCompleter load(TestImageProvider key) => _streamCompleter; + ImageStreamCompleter load(TestImageProvider key, DecoderCallback decode) => _streamCompleter; void complete() { _completer.complete(ImageInfo(image: TestImage()));