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 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 loadBuffer(AssetBundleImageKey key, DecoderBufferCallback decode) {
89
    late ImageInfo imageInfo;
90
    key.bundle.load(key.name).then<void>((ByteData data) {
91 92
      final ui.Image image = images[scaleOf(data)]!;
      assert(image != null, 'Expected ${scaleOf(data)} to have a key in $images');
93
      imageInfo = ImageInfo(image: image, scale: key.scale);
94
    });
95
    return FakeImageStreamCompleter(
96
      SynchronousFuture<ImageInfo>(imageInfo),
97
    );
98
  }
99 100
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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