Add useLogicalPixels to ResizeImage for logical pixel cache sizing (#184549)
fix https://github.com/flutter/flutter/issues/56239
Add `useLogicalPixels` to `ResizeImage` and `useLogicalCacheSize` to
`Image.{network, file, asset, memory}` and `FadeInImage.{memoryNetwork,
assetNetwork}` constructors.
When `true`, the cache dimensions are interpreted as logical pixels
rather than physical pixels, so the cached resolution follows the
surrounding device pixel ratio without requiring `BuildContext` at the
call site. Default is `false`; existing callers are unaffected.
<img width="300" alt="example"
src="https://github.com/user-attachments/assets/43c10cd6-8b8e-4c87-93fd-b2be80030af2"
/>
<details>
<summary>app(lib/main.dart)</summary>
```dart
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(home: const MyWidget());
}
}
class MyWidget extends StatelessWidget {
const MyWidget({super.key});
@override
Widget build(BuildContext context) {
final dpr = MediaQuery.devicePixelRatioOf(context);
const url =
'https://images.unsplash.com/photo-1550613097-fe6c2c321cd3'
'?auto=format&fit=crop&w=1200&q=80';
return Scaffold(
appBar: AppBar(title: Text('devicePixelRatio: $dpr')),
body: Center(
child: Column(
spacing: 8,
children: [
const Text('No cacheWidth (reference)'),
Image.network(url, width: 300, fit: BoxFit.cover),
const Text('cacheWidth: 300, useLogicalCacheSize: false'),
Image.network(url, width: 300, fit: BoxFit.cover, cacheWidth: 300),
const Text('cacheWidth: 300, useLogicalCacheSize: true'),
Image.network(
url,
width: 300,
fit: BoxFit.cover,
cacheWidth: 300,
useLogicalCacheSize: true,
),
],
),
),
);
}
}
```
</details>
## Pre-launch Checklist
- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [AI contribution guidelines] and understand my
responsibilities, or I am not using AI tools.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.
If you need help, consider asking for advice on the #hackers-new channel
on [Discord].
**Note**: The Flutter team is currently trialing the use of [Gemini Code
Assist for
GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code).
Comments from the `gemini-code-assist` bot should not be taken as
authoritative feedback from the Flutter team. If you find its comments
useful you can update your code accordingly, but if you are unsure or
disagree with the feedback, please feel free to wait for a Flutter team
member's review for guidance on which automated comments should be
addressed.
<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[AI contribution guidelines]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
---------
Co-authored-by: Justin McCandless <jmccandless@google.com>
Co-authored-by: Victor Sanni <victorsanniay@gmail.com>diff --git a/packages/flutter/lib/src/painting/image_provider.dart b/packages/flutter/lib/src/painting/image_provider.dart
index f168dc5..11b710f 100644
--- a/packages/flutter/lib/src/painting/image_provider.dart
+++ b/packages/flutter/lib/src/painting/image_provider.dart
@@ -1263,23 +1263,28 @@
/// use less memory if resized to a size smaller than the native size.
///
/// At least one of `width` and `height` must be non-null.
+ ///
+ /// {@macro flutter.painting.ResizeImage.useLogicalSize}
const ResizeImage(
this.imageProvider, {
this.width,
this.height,
this.policy = ResizeImagePolicy.exact,
this.allowUpscaling = false,
+ this.useLogicalSize = false,
}) : assert(width != null || height != null);
/// The [ImageProvider] that this class wraps.
final ImageProvider imageProvider;
- /// The width the image should decode to and cache.
+ /// The width the image should decode to and cache, in physical pixels unless
+ /// [useLogicalSize] is true.
///
/// At least one of this and [height] must be non-null.
final int? width;
- /// The height the image should decode to and cache.
+ /// The height the image should decode to and cache, in physical pixels unless
+ /// [useLogicalSize] is true.
///
/// At least one of this and [width] must be non-null.
final int? height;
@@ -1298,18 +1303,41 @@
/// to use an appropriate [Image.fit].
final bool allowUpscaling;
+ /// {@template flutter.painting.ResizeImage.useLogicalSize}
+ /// Whether [width] and [height] are interpreted as logical pixels.
+ ///
+ /// When `false` (the default), [width] and [height] are treated as physical
+ /// pixels. On high-density displays, this can result in images being decoded
+ /// at fewer pixels than the screen can render, producing lower-quality output.
+ /// When `true`, [width] and [height] are multiplied by the device pixel ratio
+ /// from the current [ImageConfiguration], so the image is decoded at the
+ /// resolution that matches the rendered display.
+ ///
+ /// Typically in the framework, pixel dimensions are interpreted as logical
+ /// rather than physical. However, this defaults to physical for backwards
+ /// compatibility.
+ /// {@endtemplate}
+ final bool useLogicalSize;
+
/// 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.
+ /// `provider` directly. If `useLogicalSize` is true, the cache dimensions
+ /// are interpreted as logical pixels instead of physical pixels.
static ImageProvider<Object> resizeIfNeeded(
int? cacheWidth,
int? cacheHeight,
- ImageProvider<Object> provider,
- ) {
+ ImageProvider<Object> provider, {
+ bool useLogicalSize = false,
+ }) {
if (cacheWidth != null || cacheHeight != null) {
- return ResizeImage(provider, width: cacheWidth, height: cacheHeight);
+ return ResizeImage(
+ provider,
+ width: cacheWidth,
+ height: cacheHeight,
+ useLogicalSize: useLogicalSize,
+ );
}
return provider;
}
@@ -1333,8 +1361,8 @@
);
return decode(
buffer,
- cacheWidth: width,
- cacheHeight: height,
+ cacheWidth: key._width,
+ cacheHeight: key._height,
allowUpscaling: this.allowUpscaling,
);
}
@@ -1352,6 +1380,9 @@
@override
ImageStreamCompleter loadImage(ResizeImageKey key, ImageDecoderCallback decode) {
+ final int? effectiveWidth = key._width;
+ final int? effectiveHeight = key._height;
+
Future<ui.Codec> decodeResize(
ui.ImmutableBuffer buffer, {
ui.TargetImageSizeCallback? getTargetSize,
@@ -1366,8 +1397,8 @@
getTargetSize: (int intrinsicWidth, int intrinsicHeight) {
switch (policy) {
case ResizeImagePolicy.exact:
- int? targetWidth = width;
- int? targetHeight = height;
+ var targetWidth = effectiveWidth;
+ var targetHeight = effectiveHeight;
if (!allowUpscaling) {
if (targetWidth != null && targetWidth > intrinsicWidth) {
@@ -1381,8 +1412,8 @@
return ui.TargetImageSize(width: targetWidth, height: targetHeight);
case ResizeImagePolicy.fit:
final double aspectRatio = intrinsicWidth / intrinsicHeight;
- final int maxWidth = width ?? intrinsicWidth;
- final int maxHeight = height ?? intrinsicHeight;
+ final int maxWidth = effectiveWidth ?? intrinsicWidth;
+ final int maxHeight = effectiveHeight ?? intrinsicHeight;
var targetWidth = intrinsicWidth;
var targetHeight = intrinsicHeight;
@@ -1397,12 +1428,12 @@
}
if (allowUpscaling) {
- if (width == null) {
- assert(height != null);
- targetHeight = height!;
+ if (effectiveWidth == null) {
+ assert(effectiveHeight != null);
+ targetHeight = effectiveHeight!;
targetWidth = (targetHeight * aspectRatio).floor();
- } else if (height == null) {
- targetWidth = width!;
+ } else if (effectiveHeight == null) {
+ targetWidth = effectiveWidth;
targetHeight = targetWidth ~/ aspectRatio;
} else {
final int derivedMaxWidth = (maxHeight * aspectRatio).floor();
@@ -1443,6 +1474,9 @@
@override
Future<ResizeImageKey> obtainKey(ImageConfiguration configuration) {
+ final double devicePixelRatio = useLogicalSize ? (configuration.devicePixelRatio ?? 1.0) : 1.0;
+ final int? effectiveWidth = width != null ? (width! * devicePixelRatio).ceil() : null;
+ final int? effectiveHeight = height != null ? (height! * devicePixelRatio).ceil() : null;
Completer<ResizeImageKey>? completer;
// If the imageProvider.obtainKey future is synchronous, then we will be able to fill in result with
// a value before completer is initialized below.
@@ -1452,11 +1486,13 @@
// This future has completed synchronously (completer was never assigned),
// so we can directly create the synchronous result to return.
result = SynchronousFuture<ResizeImageKey>(
- ResizeImageKey._(key, policy, width, height, allowUpscaling),
+ ResizeImageKey._(key, policy, effectiveWidth, effectiveHeight, allowUpscaling),
);
} else {
// This future did not synchronously complete.
- completer.complete(ResizeImageKey._(key, policy, width, height, allowUpscaling));
+ completer.complete(
+ ResizeImageKey._(key, policy, effectiveWidth, effectiveHeight, allowUpscaling),
+ );
}
});
if (result != null) {
@@ -1481,11 +1517,13 @@
width == other.width &&
height == other.height &&
policy == other.policy &&
- allowUpscaling == other.allowUpscaling;
+ allowUpscaling == other.allowUpscaling &&
+ useLogicalSize == other.useLogicalSize;
}
@override
- int get hashCode => Object.hash(imageProvider, width, height, policy, allowUpscaling);
+ int get hashCode =>
+ Object.hash(imageProvider, width, height, policy, allowUpscaling, useLogicalSize);
}
/// The strategy for [Image.network] and [NetworkImage] to decide whether to
diff --git a/packages/flutter/lib/src/widgets/fade_in_image.dart b/packages/flutter/lib/src/widgets/fade_in_image.dart
index b189965..18fe040 100644
--- a/packages/flutter/lib/src/widgets/fade_in_image.dart
+++ b/packages/flutter/lib/src/widgets/fade_in_image.dart
@@ -116,6 +116,11 @@
/// and [height] regardless of these parameters. These parameters are primarily
/// intended to reduce the memory usage of [ImageCache].
///
+ /// If [useLogicalCacheSize] is true, the cache dimensions are interpreted
+ /// as logical pixels instead of physical pixels. Set this to true when
+ /// the cache dimensions describe the image's on-screen size in logical
+ /// pixels. The flag applies to both the placeholder and the image.
+ ///
/// The [placeholder], [image], [placeholderScale], [imageScale],
/// [fadeOutDuration], [fadeOutCurve], [fadeInDuration], [fadeInCurve],
/// [alignment], [repeat], and [matchTextDirection] arguments must not be
@@ -158,15 +163,18 @@
int? placeholderCacheHeight,
int? imageCacheWidth,
int? imageCacheHeight,
+ bool useLogicalCacheSize = false,
}) : placeholder = ResizeImage.resizeIfNeeded(
placeholderCacheWidth,
placeholderCacheHeight,
MemoryImage(placeholder, scale: placeholderScale),
+ useLogicalSize: useLogicalCacheSize,
),
image = ResizeImage.resizeIfNeeded(
imageCacheWidth,
imageCacheHeight,
NetworkImage(image, scale: imageScale),
+ useLogicalSize: useLogicalCacheSize,
);
/// Creates a widget that uses a placeholder image stored in an asset bundle
@@ -190,6 +198,11 @@
/// and [height] regardless of these parameters. These parameters are primarily
/// intended to reduce the memory usage of [ImageCache].
///
+ /// If [useLogicalCacheSize] is true, the cache dimensions are interpreted
+ /// as logical pixels instead of physical pixels. Set this to true when
+ /// the cache dimensions describe the image's on-screen size in logical
+ /// pixels. The flag applies to both the placeholder and the image.
+ ///
/// See also:
///
/// * [Image.asset], which has more details about loading images from
@@ -228,21 +241,25 @@
int? placeholderCacheHeight,
int? imageCacheWidth,
int? imageCacheHeight,
+ bool useLogicalCacheSize = false,
}) : placeholder = placeholderScale != null
? ResizeImage.resizeIfNeeded(
placeholderCacheWidth,
placeholderCacheHeight,
ExactAssetImage(placeholder, bundle: bundle, scale: placeholderScale),
+ useLogicalSize: useLogicalCacheSize,
)
: ResizeImage.resizeIfNeeded(
placeholderCacheWidth,
placeholderCacheHeight,
AssetImage(placeholder, bundle: bundle),
+ useLogicalSize: useLogicalCacheSize,
),
image = ResizeImage.resizeIfNeeded(
imageCacheWidth,
imageCacheHeight,
NetworkImage(image, scale: imageScale),
+ useLogicalSize: useLogicalCacheSize,
);
/// 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 b0444c1..b72bae6 100644
--- a/packages/flutter/lib/src/widgets/image.dart
+++ b/packages/flutter/lib/src/widgets/image.dart
@@ -405,6 +405,11 @@
/// regardless of these parameters. These parameters are primarily intended
/// to reduce the memory usage of [ImageCache].
///
+ /// If [useLogicalCacheSize] is true, [cacheWidth] and [cacheHeight] are
+ /// interpreted as logical pixels instead of physical pixels. Set this to
+ /// true when [cacheWidth] and [cacheHeight] describe the image's on-screen
+ /// size in logical pixels.
+ ///
/// 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.
@@ -467,6 +472,7 @@
Map<String, String>? headers,
int? cacheWidth,
int? cacheHeight,
+ bool useLogicalCacheSize = false,
WebHtmlElementStrategy webHtmlElementStrategy = WebHtmlElementStrategy.never,
}) : image = ResizeImage.resizeIfNeeded(
cacheWidth,
@@ -477,6 +483,7 @@
headers: headers,
webHtmlElementStrategy: webHtmlElementStrategy,
),
+ useLogicalSize: useLogicalCacheSize,
),
assert(cacheWidth == null || cacheWidth > 0),
assert(cacheHeight == null || cacheHeight > 0);
@@ -505,6 +512,11 @@
/// regardless of these parameters. These parameters are primarily intended
/// to reduce the memory usage of [ImageCache].
///
+ /// If [useLogicalCacheSize] is true, [cacheWidth] and [cacheHeight] are
+ /// interpreted as logical pixels instead of physical pixels. Set this to
+ /// true when [cacheWidth] and [cacheHeight] describe the image's on-screen
+ /// size in logical pixels.
+ ///
/// Loading an image from a file creates an in memory copy of the file,
/// which is retained in the [ImageCache]. The underlying file is not
/// monitored for changes. If it does change, the application should evict
@@ -536,13 +548,19 @@
this.filterQuality = FilterQuality.medium,
int? cacheWidth,
int? cacheHeight,
+ bool useLogicalCacheSize = false,
}) : // FileImage is not supported on Flutter Web therefore neither this method.
assert(
!kIsWeb,
'Image.file is not supported on Flutter Web. '
'Consider using either Image.asset or Image.network instead.',
),
- image = ResizeImage.resizeIfNeeded(cacheWidth, cacheHeight, FileImage(file, scale: scale)),
+ image = ResizeImage.resizeIfNeeded(
+ cacheWidth,
+ cacheHeight,
+ FileImage(file, scale: scale),
+ useLogicalSize: useLogicalCacheSize,
+ ),
loadingBuilder = null,
assert(cacheWidth == null || cacheWidth > 0),
assert(cacheHeight == null || cacheHeight > 0);
@@ -585,6 +603,11 @@
/// regardless of these parameters. These parameters are primarily intended
/// to reduce the memory usage of [ImageCache].
///
+ /// If [useLogicalCacheSize] is true, [cacheWidth] and [cacheHeight] are
+ /// interpreted as logical pixels instead of physical pixels. Set this to
+ /// true when [cacheWidth] and [cacheHeight] describe the image's on-screen
+ /// size in logical pixels.
+ ///
/// Either the [width] and [height] arguments should be specified, or the
/// widget should be placed in a context that sets tight layout constraints.
/// Otherwise, the image dimensions will change as the image is loaded, which
@@ -699,12 +722,14 @@
this.filterQuality = FilterQuality.medium,
int? cacheWidth,
int? cacheHeight,
+ bool useLogicalCacheSize = false,
}) : image = ResizeImage.resizeIfNeeded(
cacheWidth,
cacheHeight,
scale != null
? ExactAssetImage(name, bundle: bundle, scale: scale, package: package)
: AssetImage(name, bundle: bundle, package: package),
+ useLogicalSize: useLogicalCacheSize,
),
loadingBuilder = null,
assert(cacheWidth == null || cacheWidth > 0),
@@ -738,6 +763,11 @@
/// 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].
+ ///
+ /// If [useLogicalCacheSize] is true, [cacheWidth] and [cacheHeight] are
+ /// interpreted as logical pixels instead of physical pixels. Set this to
+ /// true when [cacheWidth] and [cacheHeight] describe the image's on-screen
+ /// size in logical pixels.
Image.memory(
Uint8List bytes, {
super.key,
@@ -761,10 +791,12 @@
this.filterQuality = FilterQuality.medium,
int? cacheWidth,
int? cacheHeight,
+ bool useLogicalCacheSize = false,
}) : image = ResizeImage.resizeIfNeeded(
cacheWidth,
cacheHeight,
MemoryImage(bytes, scale: scale),
+ useLogicalSize: useLogicalCacheSize,
),
loadingBuilder = null,
assert(cacheWidth == null || cacheWidth > 0),
diff --git a/packages/flutter/test/painting/image_provider_resize_image_test.dart b/packages/flutter/test/painting/image_provider_resize_image_test.dart
index 604aecc..6b09e1a 100644
--- a/packages/flutter/test/painting/image_provider_resize_image_test.dart
+++ b/packages/flutter/test/painting/image_provider_resize_image_test.dart
@@ -102,6 +102,101 @@
expect(resizedImageSize, resizeDims);
});
+ // Regression test for https://github.com/flutter/flutter/issues/56239
+ test('useLogicalSize accounts for devicePixelRatio when decoding', () async {
+ // Source image is 50x50.
+ final bytes = Uint8List.fromList(kBlueSquarePng);
+ await _expectImageSize(MemoryImage(bytes), const Size(50, 50));
+
+ // Request 25x25 logical pixels on a 2x device with devicePixelRatio scaling.
+ // Should decode at 50x50 physical pixels (25 * 2).
+ final resizedImage = ResizeImage(
+ MemoryImage(bytes),
+ width: 25,
+ height: 25,
+ allowUpscaling: true,
+ useLogicalSize: true,
+ );
+ const config = ImageConfiguration(devicePixelRatio: 2.0);
+ final Size resizedImageSize = await _resolveAndGetSize(resizedImage, configuration: config);
+ expect(resizedImageSize, const Size(50, 50));
+ });
+
+ // Regression test for https://github.com/flutter/flutter/issues/56239
+ test('useLogicalSize does not upscale beyond intrinsic size by default', () async {
+ // Source image is 50x50, request 25x25 on a 3x device with devicePixelRatio scaling.
+ // Effective = 75x75, but allowUpscaling=false (default), so clamped to 50x50.
+ final bytes = Uint8List.fromList(kBlueSquarePng);
+ final resizedImage = ResizeImage(
+ MemoryImage(bytes),
+ width: 25,
+ height: 25,
+ useLogicalSize: true,
+ );
+ const config = ImageConfiguration(devicePixelRatio: 3.0);
+ final Size resizedImageSize = await _resolveAndGetSize(resizedImage, configuration: config);
+ expect(resizedImageSize, const Size(50, 50));
+ });
+
+ test('without useLogicalSize ignores devicePixelRatio', () async {
+ // Source image is 50x50, request 25x25 on a 2x device WITHOUT devicePixelRatio scaling.
+ // Should decode at exactly 25x25 (not 50x50).
+ final bytes = Uint8List.fromList(kBlueSquarePng);
+ final resizedImage = ResizeImage(MemoryImage(bytes), width: 25, height: 25);
+ const config = ImageConfiguration(devicePixelRatio: 2.0);
+ final Size resizedImageSize = await _resolveAndGetSize(resizedImage, configuration: config);
+ expect(resizedImageSize, const Size(25, 25));
+ });
+
+ test('produces equal keys when effective dimensions match across configurations', () async {
+ final bytes = Uint8List.fromList(kBlueSquarePng);
+ const config = ImageConfiguration(devicePixelRatio: 2.0);
+ final ResizeImageKey logicalKey = await ResizeImage(
+ MemoryImage(bytes),
+ width: 25,
+ height: 25,
+ useLogicalSize: true,
+ ).obtainKey(config);
+ final ResizeImageKey physicalKey = await ResizeImage(
+ MemoryImage(bytes),
+ width: 50,
+ height: 50,
+ ).obtainKey(config);
+ expect(logicalKey, physicalKey);
+ expect(logicalKey.hashCode, physicalKey.hashCode);
+ });
+
+ test('produces different keys when devicePixelRatio differs', () async {
+ final bytes = Uint8List.fromList(kBlueSquarePng);
+ final resizedImage = ResizeImage(
+ MemoryImage(bytes),
+ width: 25,
+ height: 25,
+ useLogicalSize: true,
+ );
+ final ResizeImageKey at1x = await resizedImage.obtainKey(
+ const ImageConfiguration(devicePixelRatio: 1.0),
+ );
+ final ResizeImageKey at2x = await resizedImage.obtainKey(
+ const ImageConfiguration(devicePixelRatio: 2.0),
+ );
+ expect(at1x, isNot(at2x));
+ });
+
+ test('useLogicalSize with policy=fit constrains by effective dimensions', () async {
+ final rawImage = MemoryImage(Uint8List.fromList(kBlueSquarePng));
+ final resizedImage = ResizeImage(
+ rawImage,
+ width: 12,
+ height: 25,
+ policy: ResizeImagePolicy.fit,
+ useLogicalSize: true,
+ );
+ const config = ImageConfiguration(devicePixelRatio: 2.0);
+ final Size resizedImageSize = await _resolveAndGetSize(resizedImage, configuration: config);
+ expect(resizedImageSize, const Size(24, 24));
+ });
+
test('refuses upscaling when allowUpscaling=false', () async {
final bytes = Uint8List.fromList(kTransparentImage);
final imageProvider = MemoryImage(bytes);
diff --git a/packages/flutter/test/widgets/fade_in_image_test.dart b/packages/flutter/test/widgets/fade_in_image_test.dart
index db9d1fe..9be7429 100644
--- a/packages/flutter/test/widgets/fade_in_image_test.dart
+++ b/packages/flutter/test/widgets/fade_in_image_test.dart
@@ -515,6 +515,51 @@
expect(called, true);
});
+
+ test('memoryNetwork forwards useLogicalCacheSize to placeholder and image', () {
+ final testBytes = Uint8List.fromList(kTransparentImage);
+ for (final flag in <bool>[false, true]) {
+ final image = FadeInImage.memoryNetwork(
+ placeholder: testBytes,
+ image: 'test.com',
+ placeholderCacheWidth: 20,
+ placeholderCacheHeight: 30,
+ imageCacheWidth: 40,
+ imageCacheHeight: 50,
+ useLogicalCacheSize: flag,
+ );
+ expect(
+ image.placeholder,
+ isA<ResizeImage>().having((r) => r.useLogicalSize, 'useLogicalSize', flag),
+ );
+ expect(
+ image.image,
+ isA<ResizeImage>().having((r) => r.useLogicalSize, 'useLogicalSize', flag),
+ );
+ }
+ });
+
+ test('assetNetwork forwards useLogicalCacheSize to placeholder and image', () {
+ for (final flag in <bool>[false, true]) {
+ final image = FadeInImage.assetNetwork(
+ placeholder: 'asset.png',
+ image: 'test.com',
+ placeholderCacheWidth: 20,
+ placeholderCacheHeight: 30,
+ imageCacheWidth: 40,
+ imageCacheHeight: 50,
+ useLogicalCacheSize: flag,
+ );
+ expect(
+ image.placeholder,
+ isA<ResizeImage>().having((r) => r.useLogicalSize, 'useLogicalSize', flag),
+ );
+ expect(
+ image.image,
+ isA<ResizeImage>().having((r) => r.useLogicalSize, 'useLogicalSize', flag),
+ );
+ }
+ });
});
group('semantics', () {
diff --git a/packages/flutter/test/widgets/image_test.dart b/packages/flutter/test/widgets/image_test.dart
index f62007d..fb0d657 100644
--- a/packages/flutter/test/widgets/image_test.dart
+++ b/packages/flutter/test/widgets/image_test.dart
@@ -2944,6 +2944,62 @@
);
});
+ testWidgets('Image.network forwards useLogicalCacheSize to ResizeImage', (
+ WidgetTester tester,
+ ) async {
+ for (final flag in <bool>[false, true]) {
+ expect(
+ Image.network(
+ 'https://example.com/test.png',
+ cacheWidth: 100,
+ cacheHeight: 100,
+ useLogicalCacheSize: flag,
+ ).image,
+ isA<ResizeImage>().having((r) => r.useLogicalSize, 'useLogicalSize', flag),
+ );
+ }
+ });
+
+ testWidgets('Image.asset forwards useLogicalCacheSize to ResizeImage', (
+ WidgetTester tester,
+ ) async {
+ for (final flag in <bool>[false, true]) {
+ expect(
+ Image.asset(
+ 'asset.png',
+ cacheWidth: 100,
+ cacheHeight: 100,
+ useLogicalCacheSize: flag,
+ ).image,
+ isA<ResizeImage>().having((r) => r.useLogicalSize, 'useLogicalSize', flag),
+ );
+ }
+ });
+
+ testWidgets('Image.memory forwards useLogicalCacheSize to ResizeImage', (
+ WidgetTester tester,
+ ) async {
+ final bytes = Uint8List.fromList(kTransparentImage);
+ for (final flag in <bool>[false, true]) {
+ expect(
+ Image.memory(bytes, cacheWidth: 100, cacheHeight: 100, useLogicalCacheSize: flag).image,
+ isA<ResizeImage>().having((r) => r.useLogicalSize, 'useLogicalSize', flag),
+ );
+ }
+ });
+
+ testWidgets('Image.file forwards useLogicalCacheSize to ResizeImage', (
+ WidgetTester tester,
+ ) async {
+ final file = File.fromUri(Uri.parse('/home/flutter/dash.png'));
+ for (final flag in <bool>[false, true]) {
+ expect(
+ Image.file(file, cacheWidth: 100, cacheHeight: 100, useLogicalCacheSize: flag).image,
+ isA<ResizeImage>().having((r) => r.useLogicalSize, 'useLogicalSize', flag),
+ );
+ }
+ }, skip: kIsWeb); // Image.file is not supported on Flutter Web.
+
testWidgets(
'Animated GIFs do not require layout for subsequent frames',
experimentalLeakTesting: LeakTesting.settings