image_resolution_test.dart 14 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 7
library;

8
import 'dart:ui' as ui show Image;
9

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

16
import '../image_data.dart';
17

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

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

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

36
  final String manifest;
37

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

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

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

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

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

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

87
  @override
88
  ImageStreamCompleter loadImage(AssetBundleImageKey key, ImageDecoderCallback decode) {
89
    late ImageInfo imageInfo;
90
    key.bundle.load(key.name).then<void>((ByteData data) {
91
      final ui.Image image = images[scaleOf(data)]!;
92
      imageInfo = ImageInfo(image: image, scale: key.scale);
93
    });
94
    return FakeImageStreamCompleter(
95
      SynchronousFuture<ImageInfo>(imageInfo),
96
    );
97
  }
98 99
}

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

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

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

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

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

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

168 169 170 171 172 173 174 175
  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);
    }
  });

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

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

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

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

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

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

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

263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286
  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"
      ]
    }
    ''';
    final AssetBundle bundle = TestAssetBundle(manifest: manifest);

    const double ratio = 1.0;
    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, 1.5);
    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, 1.5);
  });

287 288 289 290
  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.

291
    const String manifest = '''
292 293
    {
      "assets/image.png" : [
294 295 296 297 298 299
        "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"
300 301
      ]
    }
302
    ''';
303
    final AssetBundle bundle = TestAssetBundle(manifest: manifest);
304 305

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

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

336 337 338 339 340
  // 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 {
341
    final AssetBundle bundle = TestAssetBundle(manifest: '''
342 343
      {
        "assets/image.png" : [
344 345
          "assets/image.png",
          "assets/1.5x/image.png"
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 381

    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);
  });
382
}