image_resolution_test.dart 13.4 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:convert';
9
import 'dart:ui' as ui show Image;
10

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

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

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

22
final Map<Object?, Object?> testManifest = json.decode('''
23 24
{
  "assets/image.png" : [
25 26 27 28
    {"asset": "assets/1.5x/image.png", "dpr": 1.5},
    {"asset": "assets/2.0x/image.png", "dpr": 2.0},
    {"asset": "assets/3.0x/image.png", "dpr": 3.0},
    {"asset": "assets/4.0x/image.png", "dpr": 4.0}
29 30
  ]
}
31
''') as Map<Object?, Object?>;
32

33
class TestAssetBundle extends CachingAssetBundle {
34 35 36
  TestAssetBundle({ required Map<Object?, Object?> manifest }) {
    this.manifest = const StandardMessageCodec().encodeMessage(manifest)!;
  }
37

38
  late final ByteData manifest;
39

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

62
  @override
63
  String toString() => '${describeIdentity(this)}()';
64 65
}

66 67
class FakeImageStreamCompleter extends ImageStreamCompleter {
  FakeImageStreamCompleter(Future<ImageInfo> image) {
68
    image.then<void>(setImage);
69 70 71
  }
}

72
class TestAssetImage extends AssetImage {
73
  const TestAssetImage(super.name, this.images);
74 75

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

77
  @override
78
  ImageStreamCompleter loadImage(AssetBundleImageKey key, ImageDecoderCallback decode) {
79
    late ImageInfo imageInfo;
80
    key.bundle.load(key.name).then<void>((ByteData data) {
81
      final ui.Image image = images[scaleOf(data)]!;
82
      imageInfo = ImageInfo(image: image, scale: key.scale);
83
    });
84
    return FakeImageStreamCompleter(
85
      SynchronousFuture<ImageInfo>(imageInfo),
86
    );
87
  }
88 89
}

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

94 95
  return MediaQuery(
    data: MediaQueryData(
96 97 98
      size: const Size(windowSize, windowSize),
      devicePixelRatio: ratio,
    ),
99
    child: DefaultAssetBundle(
100
      bundle: bundle ?? TestAssetBundle(manifest: testManifest),
101
      child: Center(
102
        child: inferSize ?
103
          Image(
104
            key: key,
105
            excludeFromSemantics: true,
106
            image: TestAssetImage(imageName, images),
107
          ) :
108
          Image(
109
            key: key,
110
            excludeFromSemantics: true,
111
            image: TestAssetImage(imageName, images),
112 113
            height: imageSize,
            width: imageSize,
114 115 116 117
            fit: BoxFit.fill,
          ),
      ),
    ),
118 119 120
  );
}

121 122 123
Widget buildImageCacheResized(String name, Key key, int width, int height, int cacheWidth, int cacheHeight) {
  return Center(
    child: RepaintBoundary(
124
      child: SizedBox(
125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144
        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,
          ),
        ),
      ),
    ),
  );
}

145
RenderImage getRenderImage(WidgetTester tester, Key key) {
146
  return tester.renderObject<RenderImage>(find.byKey(key));
147 148
}

149
Future<void> pumpTreeToLayout(WidgetTester tester, Widget widget) {
150
  const Duration pumpDuration = Duration.zero;
151
  const EnginePhase pumpPhase = EnginePhase.layout;
152
  return tester.pumpWidget(widget, pumpDuration, pumpPhase);
153 154 155
}

void main() {
156
  const String image = 'assets/image.png';
157

158 159 160 161 162 163 164 165
  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);
    }
  });

166
  testWidgets('Image for device pixel ratio 1.0', (WidgetTester tester) async {
167
    const double ratio = 1.0;
168
    Key key = GlobalKey();
169
    await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, false, images));
170
    expect(getRenderImage(tester, key).size, const Size(200.0, 200.0));
171
    expect(getRenderImage(tester, key).scale, 1.0);
172
    key = GlobalKey();
173
    await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, true, images));
174
    expect(getRenderImage(tester, key).size, const Size(48.0, 48.0));
175
    expect(getRenderImage(tester, key).scale, 1.0);
176 177
  });

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

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

202 203 204
  // 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.
205
  testWidgets('Image for device pixel ratio 1.75', (WidgetTester tester) async {
206
    const double ratio = 1.75;
207
    Key key = GlobalKey();
208
    await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, false, images));
209
    expect(getRenderImage(tester, key).size, const Size(200.0, 200.0));
210
    expect(getRenderImage(tester, key).scale, 2.0);
211
    key = GlobalKey();
212
    await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, true, images));
213
    expect(getRenderImage(tester, key).size, const Size(48.0, 48.0));
214
    expect(getRenderImage(tester, key).scale, 2.0);
215 216
  });

217
  testWidgets('Image for device pixel ratio 2.3', (WidgetTester tester) async {
218
    const double ratio = 2.3;
219
    Key key = GlobalKey();
220
    await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, false, images));
221
    expect(getRenderImage(tester, key).size, const Size(200.0, 200.0));
222
    expect(getRenderImage(tester, key).scale, 2.0);
223
    key = GlobalKey();
224
    await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, true, images));
225
    expect(getRenderImage(tester, key).size, const Size(48.0, 48.0));
226
    expect(getRenderImage(tester, key).scale, 2.0);
227 228
  });

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

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

253 254 255 256
  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.

257
    final Map<Object?, Object?> manifest = json.decode('''
258 259
    {
      "assets/image.png" : [
260 261 262 263 264
        {"asset": "assets/1.0x/image.png", "dpr": 1.0},
        {"asset": "assets/1.5x/image.png", "dpr": 1.5},
        {"asset": "assets/2.0x/image.png", "dpr": 2.0},
        {"asset": "assets/3.0x/image.png", "dpr": 3.0},
        {"asset": "assets/4.0x/image.png", "dpr": 4.0}
265 266
      ]
    }
267
    ''') as Map<Object?, Object?>;
268
    final AssetBundle bundle = TestAssetBundle(manifest: manifest);
269 270

    const double ratio = 1.0;
271
    Key key = GlobalKey();
272
    await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, false, images, bundle));
273
    expect(getRenderImage(tester, key).size, const Size(200.0, 200.0));
274
    // Verify we got the 10x scaled image, since the test ByteData said it should be 10x.
275
    expect(getRenderImage(tester, key).image!.height, 480);
276
    key = GlobalKey();
277
    await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, true, images, bundle));
278
    expect(getRenderImage(tester, key).size, const Size(480.0, 480.0));
279
    // Verify we got the 10x scaled image, since the test ByteData said it should be 10x.
280
    expect(getRenderImage(tester, key).image!.height, 480);
281 282
  });

283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300
  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));
  });

301 302 303 304 305
  // 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 {
306
    final Map<Object?, Object?> manifest = json.decode('''
307 308
      {
        "assets/image.png" : [
309
          {"asset": "assets/1.5x/image.png", "dpr": 1.5}
310 311
        ]
      }
312 313
    ''') as Map<Object?, Object?>;
    final AssetBundle bundle = TestAssetBundle(manifest: manifest);
314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346

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