From 652fb975c14d98bf1a2354402f8e6623bed8c1ca Mon Sep 17 00:00:00 2001 From: Gary Qian Date: Fri, 25 Oct 2019 17:33:50 -0700 Subject: [PATCH] FadeInImage cacheWidth and cacheHeight support (#43286) --- .../lib/src/painting/image_provider.dart | 12 +++ .../lib/src/widgets/fade_in_image.dart | 35 +++++++-- packages/flutter/lib/src/widgets/image.dart | 15 +--- .../test/widgets/fade_in_image_test.dart | 75 +++++++++++++++++++ 4 files changed, 121 insertions(+), 16 deletions(-) diff --git a/packages/flutter/lib/src/painting/image_provider.dart b/packages/flutter/lib/src/painting/image_provider.dart index 69790b24d6..a4465dd3fa 100644 --- a/packages/flutter/lib/src/painting/image_provider.dart +++ b/packages/flutter/lib/src/painting/image_provider.dart @@ -540,6 +540,18 @@ class ResizeImage extends ImageProvider<_SizeAwareCacheKey> { /// The height the image should decode to and cache. final int height; + /// Composes the `provider` in a [ResizeImage] only when `cacheWidth` and + /// `cacheHeight` are not both null. + /// + /// When `cacheWidth` and `cacheHeight` are both null, this will return the + /// `provider` directly. + static ImageProvider resizeIfNeeded(int cacheWidth, int cacheHeight, ImageProvider provider) { + if (cacheWidth != null || cacheHeight != null) { + return ResizeImage(provider, width: cacheWidth, height: cacheHeight); + } + return provider; + } + @override ImageStreamCompleter load(_SizeAwareCacheKey key, DecoderCallback decode) { final DecoderCallback decodeResize = (Uint8List bytes, {int cacheWidth, int cacheHeight}) { diff --git a/packages/flutter/lib/src/widgets/fade_in_image.dart b/packages/flutter/lib/src/widgets/fade_in_image.dart index bb800e9f4d..50c5734171 100644 --- a/packages/flutter/lib/src/widgets/fade_in_image.dart +++ b/packages/flutter/lib/src/widgets/fade_in_image.dart @@ -66,6 +66,9 @@ class FadeInImage extends StatelessWidget { /// Creates a widget that displays a [placeholder] while an [image] is loading, /// then fades-out the placeholder and fades-in the image. /// + /// The [placeholder] and [image] may be composed in a [ResizeImage] to provide + /// a custom decode/cache size. + /// /// The [placeholder], [image], [fadeOutDuration], [fadeOutCurve], /// [fadeInDuration], [fadeInCurve], [alignment], [repeat], and /// [matchTextDirection] arguments must not be null. @@ -108,6 +111,13 @@ class FadeInImage extends StatelessWidget { /// The `placeholderScale` and `imageScale` arguments are passed to their /// respective [ImageProvider]s (see also [ImageInfo.scale]). /// + /// If [placeholderCacheWidth], [placeholderCacheHeight], [imageCacheWidth], + /// or [imageCacheHeight] are provided, it indicates to the + /// engine that the respective 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]. + /// /// The [placeholder], [image], [placeholderScale], [imageScale], /// [fadeOutDuration], [fadeOutCurve], [fadeInDuration], [fadeInCurve], /// [alignment], [repeat], and [matchTextDirection] arguments must not be @@ -137,6 +147,10 @@ class FadeInImage extends StatelessWidget { this.alignment = Alignment.center, this.repeat = ImageRepeat.noRepeat, this.matchTextDirection = false, + int placeholderCacheWidth, + int placeholderCacheHeight, + int imageCacheWidth, + int imageCacheHeight, }) : assert(placeholder != null), assert(image != null), assert(placeholderScale != null), @@ -148,8 +162,8 @@ class FadeInImage extends StatelessWidget { assert(alignment != null), assert(repeat != null), assert(matchTextDirection != null), - placeholder = MemoryImage(placeholder, scale: placeholderScale), - image = NetworkImage(image, scale: imageScale), + placeholder = ResizeImage.resizeIfNeeded(placeholderCacheWidth, placeholderCacheHeight, MemoryImage(placeholder, scale: placeholderScale)), + image = ResizeImage.resizeIfNeeded(imageCacheWidth, imageCacheHeight, NetworkImage(image, scale: imageScale)), super(key: key); /// Creates a widget that uses a placeholder image stored in an asset bundle @@ -166,6 +180,13 @@ class FadeInImage extends StatelessWidget { /// resolution will be attempted for the [placeholder] image. Otherwise, the /// exact asset specified will be used. /// + /// If [placeholderCacheWidth], [placeholderCacheHeight], [imageCacheWidth], + /// or [imageCacheHeight] are provided, it indicates to the + /// engine that the respective 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]. + /// /// The [placeholder], [image], [imageScale], [fadeOutDuration], /// [fadeOutCurve], [fadeInDuration], [fadeInCurve], [alignment], [repeat], /// and [matchTextDirection] arguments must not be null. @@ -195,11 +216,15 @@ class FadeInImage extends StatelessWidget { this.alignment = Alignment.center, this.repeat = ImageRepeat.noRepeat, this.matchTextDirection = false, + int placeholderCacheWidth, + int placeholderCacheHeight, + int imageCacheWidth, + int imageCacheHeight, }) : assert(placeholder != null), assert(image != null), placeholder = placeholderScale != null - ? ExactAssetImage(placeholder, bundle: bundle, scale: placeholderScale) - : AssetImage(placeholder, bundle: bundle), + ? ResizeImage.resizeIfNeeded(placeholderCacheWidth, placeholderCacheHeight, ExactAssetImage(placeholder, bundle: bundle, scale: placeholderScale)) + : ResizeImage.resizeIfNeeded(placeholderCacheWidth, placeholderCacheHeight, AssetImage(placeholder, bundle: bundle)), assert(imageScale != null), assert(fadeOutDuration != null), assert(fadeOutCurve != null), @@ -208,7 +233,7 @@ class FadeInImage extends StatelessWidget { assert(alignment != null), assert(repeat != null), assert(matchTextDirection != null), - image = NetworkImage(image, scale: imageScale), + image = ResizeImage.resizeIfNeeded(imageCacheWidth, imageCacheHeight, NetworkImage(image, scale: imageScale)), super(key: key); /// Image displayed while the target [image] is loading. diff --git a/packages/flutter/lib/src/widgets/image.dart b/packages/flutter/lib/src/widgets/image.dart index 9acb5c99fa..e789f2317a 100644 --- a/packages/flutter/lib/src/widgets/image.dart +++ b/packages/flutter/lib/src/widgets/image.dart @@ -55,13 +55,6 @@ 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 @@ -358,7 +351,7 @@ class Image extends StatefulWidget { Map headers, int cacheWidth, int cacheHeight, - }) : image = _resizeIfNeeded(cacheWidth, cacheHeight, NetworkImage(src, scale: scale, headers: headers)), + }) : image = ResizeImage.resizeIfNeeded(cacheWidth, cacheHeight, NetworkImage(src, scale: scale, headers: headers)), assert(alignment != null), assert(repeat != null), assert(matchTextDirection != null), @@ -410,7 +403,7 @@ class Image extends StatefulWidget { this.filterQuality = FilterQuality.low, int cacheWidth, int cacheHeight, - }) : image = _resizeIfNeeded(cacheWidth, cacheHeight, FileImage(file, scale: scale)), + }) : image = ResizeImage.resizeIfNeeded(cacheWidth, cacheHeight, FileImage(file, scale: scale)), loadingBuilder = null, assert(alignment != null), assert(repeat != null), @@ -573,7 +566,7 @@ class Image extends StatefulWidget { this.filterQuality = FilterQuality.low, int cacheWidth, int cacheHeight, - }) : image = _resizeIfNeeded(cacheWidth, cacheHeight, scale != null + }) : image = ResizeImage.resizeIfNeeded(cacheWidth, cacheHeight, scale != null ? ExactAssetImage(name, bundle: bundle, scale: scale, package: package) : AssetImage(name, bundle: bundle, package: package) ), @@ -630,7 +623,7 @@ class Image extends StatefulWidget { this.filterQuality = FilterQuality.low, int cacheWidth, int cacheHeight, - }) : image = _resizeIfNeeded(cacheWidth, cacheHeight, MemoryImage(bytes, scale: scale)), + }) : image = ResizeImage.resizeIfNeeded(cacheWidth, cacheHeight, MemoryImage(bytes, scale: scale)), loadingBuilder = null, assert(alignment != null), assert(repeat != null), diff --git a/packages/flutter/test/widgets/fade_in_image_test.dart b/packages/flutter/test/widgets/fade_in_image_test.dart index 88d8d220d4..9ea8f6ed7e 100644 --- a/packages/flutter/test/widgets/fade_in_image_test.dart +++ b/packages/flutter/test/widgets/fade_in_image_test.dart @@ -3,10 +3,13 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:typed_data'; import 'dart:ui' as ui; import 'package:flutter/widgets.dart'; +import 'package:flutter/painting.dart'; import 'package:flutter_test/flutter_test.dart'; +import '../painting/image_data.dart'; import '../painting/image_test_utils.dart'; const Duration animationDuration = Duration(milliseconds: 50); @@ -51,6 +54,26 @@ class FadeInImageElements { double get opacity => fadeTransition == null ? 1 : fadeTransition.opacity.value; } +class LoadTestImageProvider extends ImageProvider { + LoadTestImageProvider(this.provider); + + final ImageProvider provider; + + ImageStreamCompleter testLoad(dynamic key, DecoderCallback decode) { + return provider.load(key, decode); + } + + @override + Future obtainKey(ImageConfiguration configuration) { + return null; + } + + @override + ImageStreamCompleter load(dynamic key, DecoderCallback decode) { + return null; + } +} + FadeInImageParts findFadeInImage(WidgetTester tester) { final List elements = []; final Iterable rawImageElements = tester.elementList(find.byType(RawImage)); @@ -262,6 +285,58 @@ Future main() async { expect(findFadeInImage(tester).target.opacity, moreOrLessEquals(1)); }); + group(ImageProvider, () { + + testWidgets('memory placeholder cacheWidth and cacheHeight is passed through', (WidgetTester tester) async { + final Uint8List testBytes = Uint8List.fromList(kTransparentImage); + final FadeInImage image = FadeInImage.memoryNetwork( + placeholder: testBytes, + image: 'test.com', + placeholderCacheWidth: 20, + placeholderCacheHeight: 30, + imageCacheWidth: 40, + imageCacheHeight: 50, + ); + + bool called = false; + final DecoderCallback decode = (Uint8List bytes, {int cacheWidth, int cacheHeight}) { + expect(cacheWidth, 20); + expect(cacheHeight, 30); + called = true; + return PaintingBinding.instance.instantiateImageCodec(bytes, cacheWidth: cacheWidth, cacheHeight: cacheHeight); + }; + final ImageProvider resizeImage = image.placeholder; + expect(image.placeholder, isA()); + expect(called, false); + final LoadTestImageProvider testProvider = LoadTestImageProvider(image.placeholder); + testProvider.testLoad(await resizeImage.obtainKey(ImageConfiguration.empty), decode); + expect(called, true); + }); + + testWidgets('do not resize when null cache dimensions', (WidgetTester tester) async { + final Uint8List testBytes = Uint8List.fromList(kTransparentImage); + final FadeInImage image = FadeInImage.memoryNetwork( + placeholder: testBytes, + image: 'test.com', + ); + + bool called = false; + final DecoderCallback decode = (Uint8List bytes, {int cacheWidth, int cacheHeight}) { + expect(cacheWidth, null); + expect(cacheHeight, null); + called = true; + return PaintingBinding.instance.instantiateImageCodec(bytes, cacheWidth: cacheWidth, cacheHeight: cacheHeight); + }; + // image.placeholder should be an instance of MemoryImage instead of ResizeImage + final ImageProvider memoryImage = image.placeholder; + expect(image.placeholder, isA()); + expect(called, false); + final LoadTestImageProvider testProvider = LoadTestImageProvider(image.placeholder); + testProvider.testLoad(await memoryImage.obtainKey(ImageConfiguration.empty), decode); + expect(called, true); + }); + }); + group('semantics', () { testWidgets('only one Semantics node appears within FadeInImage', (WidgetTester tester) async { final TestImageProvider placeholderProvider = TestImageProvider(placeholderImage);