image_resolution_test.dart 14.2 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:typed_data';
7
import 'dart:ui' as ui show Image;
8

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

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

17 18
class TestByteData implements ByteData {
  TestByteData(this.scale);
19
  final double scale;
20 21 22

  @override
  dynamic noSuchMethod(Invocation invocation) => null;
23 24
}

25
const String testManifest = '''
26 27
{
  "assets/image.png" : [
28
    "assets/image.png",
29 30 31 32 33 34 35 36
    "assets/1.5x/image.png",
    "assets/2.0x/image.png",
    "assets/3.0x/image.png",
    "assets/4.0x/image.png"
  ]
}
''';

37
class TestAssetBundle extends CachingAssetBundle {
38
  TestAssetBundle({ this.manifest = testManifest });
39 40 41

  final String manifest;

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

  @override
69
  Future<String> loadString(String key, { bool cache = true }) {
70
    if (key == 'AssetManifest.json')
71
      return SynchronousFuture<String>(manifest);
72
    return SynchronousFuture<String>('');
73
  }
74 75

  @override
76
  String toString() => '${describeIdentity(this)}()';
77 78
}

79 80
class FakeImageStreamCompleter extends ImageStreamCompleter {
  FakeImageStreamCompleter(Future<ImageInfo> image) {
81
    image.then<void>(setImage);
82 83 84
  }
}

85
class TestAssetImage extends AssetImage {
86 87 88
  const TestAssetImage(String name, this.images) : super(name);

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

90
  @override
91
  ImageStreamCompleter load(AssetBundleImageKey key, DecoderCallback decode) {
92
    late ImageInfo imageInfo;
93
    key.bundle.load(key.name).then<void>((ByteData data) {
94
      final TestByteData testData = data as TestByteData;
95
      final ui.Image image = images[testData.scale]!;
96
      assert(image != null, 'Expected ${testData.scale} to have a key in $images');
97
      imageInfo = ImageInfo(image: image, scale: key.scale);
98
    });
99 100
    return FakeImageStreamCompleter(
      SynchronousFuture<ImageInfo>(imageInfo)
101
    );
102
  }
103 104
}

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

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

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

161
RenderImage getRenderImage(WidgetTester tester, Key key) {
162
  return tester.renderObject<RenderImage>(find.byKey(key));
163 164
}

165
Future<void> pumpTreeToLayout(WidgetTester tester, Widget widget) {
166
  const Duration pumpDuration = Duration.zero;
167
  const EnginePhase pumpPhase = EnginePhase.layout;
168
  return tester.pumpWidget(widget, pumpDuration, pumpPhase);
169 170 171
}

void main() {
172
  const String image = 'assets/image.png';
173

174 175 176 177 178 179 180 181
  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);
    }
  });

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

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

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

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

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

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

257
  testWidgets('Image for device pixel ratio 5.1', (WidgetTester tester) async {
258
    const double ratio = 5.1;
259
    Key key = GlobalKey();
260
    await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, false, images));
261
    expect(getRenderImage(tester, key).size, const Size(200.0, 200.0));
262
    expect(getRenderImage(tester, key).scale, 4.0);
263
    key = GlobalKey();
264
    await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, true, images));
265
    expect(getRenderImage(tester, key).size, const Size(48.0, 48.0));
266
    expect(getRenderImage(tester, key).scale, 4.0);
267 268
  });

269 270 271 272 273 274 275 276 277 278 279
  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"
      ]
    }
    ''';
280
    final AssetBundle bundle = TestAssetBundle(manifest: manifest);
281 282

    const double ratio = 1.0;
283
    Key key = GlobalKey();
284
    await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, false, images, bundle));
285
    expect(getRenderImage(tester, key).size, const Size(200.0, 200.0));
286
    expect(getRenderImage(tester, key).scale, 1.5);
287
    key = GlobalKey();
288
    await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, true, images, bundle));
289
    expect(getRenderImage(tester, key).size, const Size(48.0, 48.0));
290
    expect(getRenderImage(tester, key).scale, 1.5);
291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308
  });

  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"
      ]
    }
    ''';
309
    final AssetBundle bundle = TestAssetBundle(manifest: manifest);
310 311

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

324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341
  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));
  });

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 381 382 383 384 385 386 387
  // 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);
  });
388
}