image_resolution_test.dart 14.1 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
@TestOn('!chrome')
6
import 'dart:ui' as ui show Image;
7

8
import 'package:flutter/foundation.dart';
9 10 11
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
12
import 'package:flutter_test/flutter_test.dart';
13

14
import '../image_data.dart';
15

16
ByteData testByteData(double scale) => ByteData(8)..setFloat64(0, scale);
17
double scaleOf(ByteData data) => data.getFloat64(0);
18

19
const String testManifest = '''
20 21
{
  "assets/image.png" : [
22
    "assets/image.png",
23 24 25 26 27 28 29 30
    "assets/1.5x/image.png",
    "assets/2.0x/image.png",
    "assets/3.0x/image.png",
    "assets/4.0x/image.png"
  ]
}
''';

31
class TestAssetBundle extends CachingAssetBundle {
32
  TestAssetBundle({ this.manifest = testManifest });
33 34 35

  final String manifest;

36
  @override
37
  Future<ByteData> load(String key) {
38
    late ByteData data;
39 40
    switch (key) {
      case 'assets/image.png':
41
        data = testByteData(1.0);
42
        break;
43
      case 'assets/1.0x/image.png':
44
        data = testByteData(10.0); // see "...with a main asset and a 1.0x asset"
45
        break;
46
      case 'assets/1.5x/image.png':
47
        data = testByteData(1.5);
48 49
        break;
      case 'assets/2.0x/image.png':
50
        data = testByteData(2.0);
51 52
        break;
      case 'assets/3.0x/image.png':
53
        data = testByteData(3.0);
54 55
        break;
      case 'assets/4.0x/image.png':
56
        data = testByteData(4.0);
57 58
        break;
    }
59
    return SynchronousFuture<ByteData>(data);
60 61 62
  }

  @override
63
  Future<String> loadString(String key, { bool cache = true }) {
64
    if (key == 'AssetManifest.json') {
65
      return SynchronousFuture<String>(manifest);
66
    }
67
    return SynchronousFuture<String>('');
68
  }
69 70

  @override
71
  String toString() => '${describeIdentity(this)}()';
72 73
}

74 75
class FakeImageStreamCompleter extends ImageStreamCompleter {
  FakeImageStreamCompleter(Future<ImageInfo> image) {
76
    image.then<void>(setImage);
77 78 79
  }
}

80
class TestAssetImage extends AssetImage {
81
  const TestAssetImage(super.name, this.images);
82 83

  final Map<double, ui.Image> images;
84

85
  @override
86
  ImageStreamCompleter loadBuffer(AssetBundleImageKey key, DecoderBufferCallback decode) {
87
    late ImageInfo imageInfo;
88
    key.bundle.load(key.name).then<void>((ByteData data) {
89 90
      final ui.Image image = images[scaleOf(data)]!;
      assert(image != null, 'Expected ${scaleOf(data)} to have a key in $images');
91
      imageInfo = ImageInfo(image: image, scale: key.scale);
92
    });
93
    return FakeImageStreamCompleter(
94
      SynchronousFuture<ImageInfo>(imageInfo),
95
    );
96
  }
97 98
}

99
Widget buildImageAtRatio(String imageName, Key key, double ratio, bool inferSize, Map<double, ui.Image> images, [ AssetBundle? bundle ]) {
100 101 102
  const double windowSize = 500.0; // 500 logical pixels
  const double imageSize = 200.0; // 200 logical pixels

103 104
  return MediaQuery(
    data: MediaQueryData(
105 106 107
      size: const Size(windowSize, windowSize),
      devicePixelRatio: ratio,
    ),
108 109 110
    child: DefaultAssetBundle(
      bundle: bundle ?? TestAssetBundle(),
      child: Center(
111
        child: inferSize ?
112
          Image(
113
            key: key,
114
            excludeFromSemantics: true,
115
            image: TestAssetImage(imageName, images),
116
          ) :
117
          Image(
118
            key: key,
119
            excludeFromSemantics: true,
120
            image: TestAssetImage(imageName, images),
121 122
            height: imageSize,
            width: imageSize,
123 124 125 126
            fit: BoxFit.fill,
          ),
      ),
    ),
127 128 129
  );
}

130 131 132
Widget buildImageCacheResized(String name, Key key, int width, int height, int cacheWidth, int cacheHeight) {
  return Center(
    child: RepaintBoundary(
133
      child: SizedBox(
134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153
        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,
          ),
        ),
      ),
    ),
  );
}

154
RenderImage getRenderImage(WidgetTester tester, Key key) {
155
  return tester.renderObject<RenderImage>(find.byKey(key));
156 157
}

158
Future<void> pumpTreeToLayout(WidgetTester tester, Widget widget) {
159
  const Duration pumpDuration = Duration.zero;
160
  const EnginePhase pumpPhase = EnginePhase.layout;
161
  return tester.pumpWidget(widget, pumpDuration, pumpPhase);
162 163 164
}

void main() {
165
  const String image = 'assets/image.png';
166

167 168 169 170 171 172 173 174
  final Map<double, ui.Image> images = <double, ui.Image>{};
  setUpAll(() async {
    for (final double scale in const <double>[0.5, 1.0, 1.5, 2.0, 4.0, 10.0]) {
      final int dimension = (48 * scale).floor();
      images[scale] = await createTestImage(width: dimension, height: dimension);
    }
  });

175
  testWidgets('Image for device pixel ratio 1.0', (WidgetTester tester) async {
176
    const double ratio = 1.0;
177
    Key key = GlobalKey();
178
    await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, false, images));
179
    expect(getRenderImage(tester, key).size, const Size(200.0, 200.0));
180
    expect(getRenderImage(tester, key).scale, 1.0);
181
    key = GlobalKey();
182
    await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, true, images));
183
    expect(getRenderImage(tester, key).size, const Size(48.0, 48.0));
184
    expect(getRenderImage(tester, key).scale, 1.0);
185 186
  });

187
  testWidgets('Image for device pixel ratio 0.5', (WidgetTester tester) async {
188
    const double ratio = 0.5;
189
    Key key = GlobalKey();
190
    await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, false, images));
191
    expect(getRenderImage(tester, key).size, const Size(200.0, 200.0));
192
    expect(getRenderImage(tester, key).scale, 1.0);
193
    key = GlobalKey();
194
    await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, true, images));
195
    expect(getRenderImage(tester, key).size, const Size(48.0, 48.0));
196
    expect(getRenderImage(tester, key).scale, 1.0);
197 198
  });

199
  testWidgets('Image for device pixel ratio 1.5', (WidgetTester tester) async {
200
    const double ratio = 1.5;
201
    Key key = GlobalKey();
202
    await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, false, images));
203
    expect(getRenderImage(tester, key).size, const Size(200.0, 200.0));
204
    expect(getRenderImage(tester, key).scale, 1.5);
205
    key = GlobalKey();
206
    await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, true, images));
207
    expect(getRenderImage(tester, key).size, const Size(48.0, 48.0));
208
    expect(getRenderImage(tester, key).scale, 1.5);
209 210
  });

211 212 213
  // 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.
214
  testWidgets('Image for device pixel ratio 1.75', (WidgetTester tester) async {
215
    const double ratio = 1.75;
216
    Key key = GlobalKey();
217
    await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, false, images));
218
    expect(getRenderImage(tester, key).size, const Size(200.0, 200.0));
219
    expect(getRenderImage(tester, key).scale, 2.0);
220
    key = GlobalKey();
221
    await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, true, images));
222
    expect(getRenderImage(tester, key).size, const Size(48.0, 48.0));
223
    expect(getRenderImage(tester, key).scale, 2.0);
224 225
  });

226
  testWidgets('Image for device pixel ratio 2.3', (WidgetTester tester) async {
227
    const double ratio = 2.3;
228
    Key key = GlobalKey();
229
    await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, false, images));
230
    expect(getRenderImage(tester, key).size, const Size(200.0, 200.0));
231
    expect(getRenderImage(tester, key).scale, 2.0);
232
    key = GlobalKey();
233
    await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, true, images));
234
    expect(getRenderImage(tester, key).size, const Size(48.0, 48.0));
235
    expect(getRenderImage(tester, key).scale, 2.0);
236 237
  });

238
  testWidgets('Image for device pixel ratio 3.7', (WidgetTester tester) async {
239
    const double ratio = 3.7;
240
    Key key = GlobalKey();
241
    await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, false, images));
242
    expect(getRenderImage(tester, key).size, const Size(200.0, 200.0));
243
    expect(getRenderImage(tester, key).scale, 4.0);
244
    key = GlobalKey();
245
    await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, true, images));
246
    expect(getRenderImage(tester, key).size, const Size(48.0, 48.0));
247
    expect(getRenderImage(tester, key).scale, 4.0);
248 249
  });

250
  testWidgets('Image for device pixel ratio 5.1', (WidgetTester tester) async {
251
    const double ratio = 5.1;
252
    Key key = GlobalKey();
253
    await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, false, images));
254
    expect(getRenderImage(tester, key).size, const Size(200.0, 200.0));
255
    expect(getRenderImage(tester, key).scale, 4.0);
256
    key = GlobalKey();
257
    await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, true, images));
258
    expect(getRenderImage(tester, key).size, const Size(48.0, 48.0));
259
    expect(getRenderImage(tester, key).scale, 4.0);
260 261
  });

262 263 264 265 266 267 268 269 270 271 272
  testWidgets('Image for device pixel ratio 1.0, with no main asset', (WidgetTester tester) async {
    const String manifest = '''
    {
      "assets/image.png" : [
        "assets/1.5x/image.png",
        "assets/2.0x/image.png",
        "assets/3.0x/image.png",
        "assets/4.0x/image.png"
      ]
    }
    ''';
273
    final AssetBundle bundle = TestAssetBundle(manifest: manifest);
274 275

    const double ratio = 1.0;
276
    Key key = GlobalKey();
277
    await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, false, images, bundle));
278
    expect(getRenderImage(tester, key).size, const Size(200.0, 200.0));
279
    expect(getRenderImage(tester, key).scale, 1.5);
280
    key = GlobalKey();
281
    await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, true, images, bundle));
282
    expect(getRenderImage(tester, key).size, const Size(48.0, 48.0));
283
    expect(getRenderImage(tester, key).scale, 1.5);
284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301
  });

  testWidgets('Image for device pixel ratio 1.0, with a main asset and a 1.0x asset', (WidgetTester tester) async {
    // If both a main asset and a 1.0x asset are specified, then prefer
    // the 1.0x asset.

    const String manifest = '''
    {
      "assets/image.png" : [
        "assets/image.png",
        "assets/1.0x/image.png",
        "assets/1.5x/image.png",
        "assets/2.0x/image.png",
        "assets/3.0x/image.png",
        "assets/4.0x/image.png"
      ]
    }
    ''';
302
    final AssetBundle bundle = TestAssetBundle(manifest: manifest);
303 304

    const double ratio = 1.0;
305
    Key key = GlobalKey();
306
    await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, false, images, bundle));
307
    expect(getRenderImage(tester, key).size, const Size(200.0, 200.0));
308
    // Verify we got the 10x scaled image, since the test ByteData said it should be 10x.
309
    expect(getRenderImage(tester, key).image!.height, 480);
310
    key = GlobalKey();
311
    await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, true, images, bundle));
312
    expect(getRenderImage(tester, key).size, const Size(480.0, 480.0));
313
    // Verify we got the 10x scaled image, since the test ByteData said it should be 10x.
314
    expect(getRenderImage(tester, key).image!.height, 480);
315 316
  });

317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334
  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));
  });

335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380
  // 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);
  });
381
}