44
55import 'dart:async' ;
66import 'dart:collection' ;
7+ import 'dart:convert' ;
78
89import 'package:flutter/foundation.dart' ;
910import 'package:flutter/services.dart' ;
1011
1112import 'image_provider.dart' ;
1213
14+ const String _kAssetManifestFileName = 'AssetManifest.json' ;
15+
1316/// A screen with a device-pixel ratio strictly less than this value is
1417/// considered a low-resolution screen (typically entry-level to mid-range
1518/// laptops, desktop screens up to QHD, low-end tablets such as Kindle Fire).
@@ -281,18 +284,18 @@ class AssetImage extends AssetBundleImageProvider {
281284 Completer <AssetBundleImageKey >? completer;
282285 Future <AssetBundleImageKey >? result;
283286
284- AssetManifest .loadFromAssetBundle (chosenBundle)
285- .then ((AssetManifest manifest) {
286- final Iterable <AssetMetadata >? candidateVariants = manifest.getAssetVariants (keyName);
287- final AssetMetadata chosenVariant = _chooseVariant (
287+ chosenBundle.loadStructuredData <Map <String , List <String >>?>(_kAssetManifestFileName, manifestParser).then <void >(
288+ (Map <String , List <String >>? manifest) {
289+ final String chosenName = _chooseVariant (
288290 keyName,
289291 configuration,
290- candidateVariants,
291- );
292+ manifest == null ? null : manifest[keyName],
293+ )! ;
294+ final double chosenScale = _parseScale (chosenName);
292295 final AssetBundleImageKey key = AssetBundleImageKey (
293296 bundle: chosenBundle,
294- name: chosenVariant.key ,
295- scale: chosenVariant.targetDevicePixelRatio ?? _naturalResolution ,
297+ name: chosenName ,
298+ scale: chosenScale ,
296299 );
297300 if (completer != null ) {
298301 // We already returned from this function, which means we are in the
@@ -306,15 +309,14 @@ class AssetImage extends AssetBundleImageProvider {
306309 // ourselves.
307310 result = SynchronousFuture <AssetBundleImageKey >(key);
308311 }
309- })
310- .onError ((Object error, StackTrace stack) {
311- // We had an error. (This guarantees we weren't called synchronously.)
312- // Forward the error to the caller.
313- assert (completer != null );
314- assert (result == null );
315- completer! .completeError (error, stack);
316- });
317-
312+ },
313+ ).catchError ((Object error, StackTrace stack) {
314+ // We had an error. (This guarantees we weren't called synchronously.)
315+ // Forward the error to the caller.
316+ assert (completer != null );
317+ assert (result == null );
318+ completer! .completeError (error, stack);
319+ });
318320 if (result != null ) {
319321 // The code above ran synchronously, and came up with an answer.
320322 // Return the SynchronousFuture that we created above.
@@ -326,24 +328,35 @@ class AssetImage extends AssetBundleImageProvider {
326328 return completer.future;
327329 }
328330
329- AssetMetadata _chooseVariant (String mainAssetKey, ImageConfiguration config, Iterable <AssetMetadata >? candidateVariants) {
330- if (candidateVariants == null ) {
331- return AssetMetadata (key: mainAssetKey, targetDevicePixelRatio: null , main: true );
331+ /// Parses the asset manifest string into a strongly-typed map.
332+ @visibleForTesting
333+ static Future <Map <String , List <String >>?> manifestParser (String ? jsonData) {
334+ if (jsonData == null ) {
335+ return SynchronousFuture <Map <String , List <String >>?>(null );
332336 }
337+ // TODO(ianh): JSON decoding really shouldn't be on the main thread.
338+ final Map <String , dynamic > parsedJson = json.decode (jsonData) as Map <String , dynamic >;
339+ final Iterable <String > keys = parsedJson.keys;
340+ final Map <String , List <String >> parsedManifest = < String , List <String >> {
341+ for (final String key in keys) key: List <String >.from (parsedJson[key] as List <dynamic >),
342+ };
343+ // TODO(ianh): convert that data structure to the right types.
344+ return SynchronousFuture <Map <String , List <String >>?>(parsedManifest);
345+ }
333346
334- if (config.devicePixelRatio == null ) {
335- return candidateVariants.firstWhere ((AssetMetadata variant) => variant.main);
347+ String ? _chooseVariant (String main, ImageConfiguration config, List <String >? candidates) {
348+ if (config.devicePixelRatio == null || candidates == null || candidates.isEmpty) {
349+ return main;
336350 }
337-
338- final SplayTreeMap <double , AssetMetadata > candidatesByDevicePixelRatio =
339- SplayTreeMap <double , AssetMetadata >();
340- for (final AssetMetadata candidate in candidateVariants) {
341- candidatesByDevicePixelRatio[candidate.targetDevicePixelRatio ?? _naturalResolution] = candidate;
351+ // TODO(ianh): Consider moving this parsing logic into _manifestParser.
352+ final SplayTreeMap <double , String > mapping = SplayTreeMap <double , String >();
353+ for (final String candidate in candidates) {
354+ mapping[_parseScale (candidate)] = candidate;
342355 }
343356 // TODO(ianh): implement support for config.locale, config.textDirection,
344357 // config.size, config.platform (then document this over in the Image.asset
345358 // docs)
346- return _findBestVariant (candidatesByDevicePixelRatio , config.devicePixelRatio! );
359+ return _findBestVariant (mapping , config.devicePixelRatio! );
347360 }
348361
349362 // Returns the "best" asset variant amongst the available `candidates`.
@@ -358,28 +371,48 @@ class AssetImage extends AssetBundleImageProvider {
358371 // lowest key higher than `value`.
359372 // - If the screen has high device pixel ratio, choose the variant with the
360373 // key nearest to `value`.
361- AssetMetadata _findBestVariant (SplayTreeMap <double , AssetMetadata > candidatesByDpr , double value) {
362- if (candidatesByDpr .containsKey (value)) {
363- return candidatesByDpr [value]! ;
374+ String ? _findBestVariant (SplayTreeMap <double , String > candidates , double value) {
375+ if (candidates .containsKey (value)) {
376+ return candidates [value]! ;
364377 }
365- final double ? lower = candidatesByDpr .lastKeyBefore (value);
366- final double ? upper = candidatesByDpr .firstKeyAfter (value);
378+ final double ? lower = candidates .lastKeyBefore (value);
379+ final double ? upper = candidates .firstKeyAfter (value);
367380 if (lower == null ) {
368- return candidatesByDpr [upper]! ;
381+ return candidates [upper];
369382 }
370383 if (upper == null ) {
371- return candidatesByDpr [lower]! ;
384+ return candidates [lower];
372385 }
373386
374387 // On screens with low device-pixel ratios the artifacts from upscaling
375388 // images are more visible than on screens with a higher device-pixel
376389 // ratios because the physical pixels are larger. Choose the higher
377390 // resolution image in that case instead of the nearest one.
378391 if (value < _kLowDprLimit || value > (lower + upper) / 2 ) {
379- return candidatesByDpr [upper]! ;
392+ return candidates [upper];
380393 } else {
381- return candidatesByDpr[lower]! ;
394+ return candidates[lower];
395+ }
396+ }
397+
398+ static final RegExp _extractRatioRegExp = RegExp (r'/?(\d+(\.\d*)?)x$' );
399+
400+ double _parseScale (String key) {
401+ if (key == assetName) {
402+ return _naturalResolution;
403+ }
404+
405+ final Uri assetUri = Uri .parse (key);
406+ String directoryPath = '' ;
407+ if (assetUri.pathSegments.length > 1 ) {
408+ directoryPath = assetUri.pathSegments[assetUri.pathSegments.length - 2 ];
409+ }
410+
411+ final Match ? match = _extractRatioRegExp.firstMatch (directoryPath);
412+ if (match != null && match.groupCount > 0 ) {
413+ return double .parse (match.group (1 )! );
382414 }
415+ return _naturalResolution; // i.e. default to 1.0x
383416 }
384417
385418 @override
0 commit comments