Unverified Commit 496efca1 authored by Yegor's avatar Yegor Committed by GitHub

choose higher-res images on low-DPR screens (#69799)

parent e8efde6a
......@@ -2,7 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:collection';
import 'dart:convert';
......@@ -15,6 +14,11 @@ import 'image_provider.dart';
const String _kAssetManifestFileName = 'AssetManifest.json';
/// A screen with a device-pixel ratio strictly less than this value is
/// considered a low-resolution screen (typically entry-level to mid-range
/// laptops, desktop screens up to QHD, low-end tablets such as Kindle Fire).
const double _kLowDprLimit = 2.0;
/// Fetches an image from an [AssetBundle], having determined the exact image to
/// use based on the context.
///
......@@ -34,18 +38,52 @@ const String _kAssetManifestFileName = 'AssetManifest.json';
///
/// For example, suppose an application wants to use an icon named
/// "heart.png". This icon has representations at 1.0 (the main icon), as well
/// as 1.5 and 2.0 pixel ratios (variants). The asset bundle should then contain
/// the following assets:
/// as 2.0 and 4.0 pixel ratios (variants). The asset bundle should then
/// contain the following assets:
///
/// ```
/// heart.png
/// 1.5x/heart.png
/// 2.0x/heart.png
/// 4.0x/heart.png
/// ```
///
/// On a device with a 1.0 device pixel ratio, the image chosen would be
/// heart.png; on a device with a 1.3 device pixel ratio, the image chosen
/// would be 1.5x/heart.png.
/// heart.png; on a device with a 2.0 device pixel ratio, the image chosen
/// would be 2.0x/heart.png; on a device with a 4.0 device pixel ratio, the
/// image chosen would be 4.0x/heart.png.
///
/// On a device with a device pixel ratio that does not exactly match an
/// available asset the "best match" is chosen. Which asset is the best depends
/// on the screen. Low-resolution screens (those with device pixel ratio
/// strictly less than 2.0) use a different matching algorithm from the
/// high-resolution screen. Because in low-resolution screens the physical
/// pixels are visible to the user upscaling artifacts (e.g. blurred edges) are
/// more pronounced. Therefore, a higher resolution asset is chosen, if
/// available. For higher-resolution screens, where individual physical pixels
/// are not visible to the user, the asset variant with the pixel ratio that's
/// the closest to the screen's device pixel ratio is chosen.
///
/// For example, for a screen with device pixel ratio 1.25 the image chosen
/// would be 2.0x/heart.png, even though heart.png (i.e. 1.0) is closer. This
/// is because the screen is considered low-resolution. For a screen with
/// device pixel ratio of 2.25 the image chosen would also be 2.0x/heart.png.
/// This is because the screen is considered to be a high-resolution screen,
/// and therefore upscaling a 2.0x image to 2.25 won't result in visible
/// upscaling artifacts. However, for a screen with device-pixel ratio 3.25 the
/// image chosen would be 4.0x/heart.png because it's closer to 4.0 than it is
/// to 2.0.
///
/// Choosing a higher-resolution image than necessary may waste significantly
/// more memory if the difference between the screen device pixel ratio and
/// the device pixel ratio of the image is high. To reduce memory usage,
/// consider providing more variants of the image. In the example above adding
/// a 3.0x/heart.png variant would improve memory usage for screens with device
/// pixel ratios between 3.0 and 3.5.
///
/// [ImageConfiguration] can be used to customize the selection of the image
/// variant by setting [ImageConfiguration.devicePixelRatio] to value different
/// from the default. The default value is derived from
/// [MediaQueryData.devicePixelRatio] by [createLocalImageConfiguration].
///
/// The directory level of the asset does not matter as long as the variants are
/// at the equivalent level; that is, the following is also a valid bundle
......@@ -240,11 +278,22 @@ class AssetImage extends AssetBundleImageProvider {
// TODO(ianh): implement support for config.locale, config.textDirection,
// config.size, config.platform (then document this over in the Image.asset
// docs)
return _findNearest(mapping, config.devicePixelRatio!);
return _findBestVariant(mapping, config.devicePixelRatio!);
}
// Return the value for the key in a [SplayTreeMap] nearest the provided key.
String? _findNearest(SplayTreeMap<double, String> candidates, double value) {
// Returns the "best" asset variant amongst the availabe `candidates`.
//
// The best variant is chosen as follows:
// - Choose a variant whose key matches `value` exactly, if available.
// - If `value` is less than the lowest key, choose the variant with the
// lowest key.
// - If `value` is greater than the highest key, choose the variant with
// the highest key.
// - If the screen has low device pixel ratio, chosse the variant with the
// lowest key higher than `value`.
// - If the screen has high device pixel ratio, choose the variant with the
// key nearest to `value`.
String? _findBestVariant(SplayTreeMap<double, String> candidates, double value) {
if (candidates.containsKey(value))
return candidates[value]!;
final double? lower = candidates.lastKeyBefore(value);
......@@ -253,7 +302,12 @@ class AssetImage extends AssetBundleImageProvider {
return candidates[upper];
if (upper == null)
return candidates[lower];
if (value > (lower + upper) / 2)
// On screens with low device-pixel ratios the artifacts from upscaling
// images are more visible than on screens with a higher device-pixel
// ratios because the physical pixels are larger. Choose the higher
// resolution image in that case instead of the nearest one.
if (value < _kLowDprLimit || value > (lower + upper) / 2)
return candidates[upper];
else
return candidates[lower];
......
......@@ -215,16 +215,19 @@ void main() {
expect(getRenderImage(tester, key).scale, 1.5);
});
// A 1.75 DPR screen is typically a low-resolution screen, such that physical
// pixels are visible to the user. For such screens we prefer to pick the
// higher resolution image, if available.
testWidgets('Image for device pixel ratio 1.75', (WidgetTester tester) async {
const double ratio = 1.75;
Key key = GlobalKey();
await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, false, images));
expect(getRenderImage(tester, key).size, const Size(200.0, 200.0));
expect(getRenderImage(tester, key).scale, 1.5);
expect(getRenderImage(tester, key).scale, 2.0);
key = GlobalKey();
await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, true, images));
expect(getRenderImage(tester, key).size, const Size(48.0, 48.0));
expect(getRenderImage(tester, key).scale, 1.5);
expect(getRenderImage(tester, key).scale, 2.0);
});
testWidgets('Image for device pixel ratio 2.3', (WidgetTester tester) async {
......@@ -336,4 +339,50 @@ void main() {
expect(getRenderImage(tester, key).size, const Size(5.0, 5.0));
});
// For low-resolution screens we prefer higher-resolution images due to
// visible physical pixel size (see the test for 1.75 DPR above). However,
// if higher resolution assets are not available we will pick the best
// available.
testWidgets('Low-resolution assets', (WidgetTester tester) async {
final AssetBundle bundle = TestAssetBundle(manifest: '''
{
"assets/image.png" : [
"assets/image.png",
"assets/1.5x/image.png"
]
}
''');
Future<void> testRatio({required double ratio, required double expectedScale}) async {
Key key = GlobalKey();
await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, false, images, bundle));
expect(getRenderImage(tester, key).size, const Size(200.0, 200.0));
expect(getRenderImage(tester, key).scale, expectedScale);
key = GlobalKey();
await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, true, images, bundle));
expect(getRenderImage(tester, key).size, const Size(48.0, 48.0));
expect(getRenderImage(tester, key).scale, expectedScale);
}
// Choose higher resolution image as it's the lowest available.
await testRatio(ratio: 0.25, expectedScale: 1.0);
await testRatio(ratio: 0.5, expectedScale: 1.0);
await testRatio(ratio: 0.75, expectedScale: 1.0);
await testRatio(ratio: 1.0, expectedScale: 1.0);
// Choose higher resolution image even though a lower resolution
// image is closer.
await testRatio(ratio: 1.20, expectedScale: 1.5);
// Choose higher resolution image because it's closer.
await testRatio(ratio: 1.25, expectedScale: 1.5);
await testRatio(ratio: 1.5, expectedScale: 1.5);
// Choose lower resolution image because no higher resolution assets
// are not available.
await testRatio(ratio: 1.75, expectedScale: 1.5);
await testRatio(ratio: 2.0, expectedScale: 1.5);
await testRatio(ratio: 2.25, expectedScale: 1.5);
await testRatio(ratio: 10.0, expectedScale: 1.5);
});
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment