image_resolution.dart 10.6 KB
Newer Older
1 2 3 4 5 6 7 8 9 10
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';
import 'dart:collection';
import 'dart:convert';
import 'dart:ui' show hashValues;

import 'package:flutter/foundation.dart';
11
import 'package:flutter/services.dart';
12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57

import 'image_provider.dart';

const String _kAssetManifestFileName = 'AssetManifest.json';

/// Fetches an image from an [AssetBundle], having determined the exact image to
/// use based on the context.
///
/// Given a main asset and a set of variants, AssetImage chooses the most
/// appropriate asset for the current context, based on the device pixel ratio
/// and size given in the configuration passed to [resolve].
///
/// To show a specific image from a bundle without any asset resolution, use an
/// [AssetBundleImageProvider].
///
/// ## Naming assets for matching with different pixel densities
///
/// Main assets are presumed to match a nominal pixel ratio of 1.0. To specify
/// assets targeting different pixel ratios, place the variant assets in
/// the application bundle under subdirectories named in the form "Nx", where
/// N is the nominal device pixel ratio for that asset.
///
/// For example, suppose an application wants to use an icon named
/// "heart.png". This icon has representations at 1.0 (the main icon), as well
/// as 1.5 and 2.0 pixel ratios (variants). The asset bundle should then contain
/// the following assets:
///
/// ```
/// heart.png
/// 1.5x/heart.png
/// 2.0x/heart.png
/// ```
///
/// On a device with a 1.0 device pixel ratio, the image chosen would be
/// heart.png; on a device with a 1.3 device pixel ratio, the image chosen
/// would be 1.5x/heart.png.
///
/// The directory level of the asset does not matter as long as the variants are
/// at the equivalent level; that is, the following is also a valid bundle
/// structure:
///
/// ```
/// icons/heart.png
/// icons/1.5x/heart.png
/// icons/2.0x/heart.png
/// ```
58 59 60 61 62
///
/// ## Fetching assets
///
/// When fetching an image provided by the app itself, use the [assetName]
/// argument to name the asset to choose. For instance, consider the structure
63
/// above. First, the `pubspec.yaml` of the project should specify its assets in
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89
/// the `flutter` section:
///
/// ```yaml
/// flutter:
///   assets:
///     - icons/heart.png
/// ```
///
/// Then, to fetch the image, use
/// ```dart
/// new AssetImage('icons/heart.png')
/// ```
///
/// ## Assets in packages
///
/// To fetch an asset from a package, the [package] argument must be provided.
/// For instance, suppose the structure above is inside a package called
/// `my_icons`. Then to fetch the image, use:
///
/// ```dart
/// new AssetImage('icons/heart.png', package: 'my_icons')
/// ```
///
/// Assets used by the package itself should also be fetched using the [package]
/// argument as above.
///
90
/// If the desired asset is specified in the `pubspec.yaml` of the package, it
91
/// is bundled automatically with the app. In particular, assets used by the
92
/// package itself must be specified in its `pubspec.yaml`.
93 94
///
/// A package can also choose to have assets in its 'lib/' folder that are not
95
/// specified in its `pubspec.yaml`. In this case for those images to be
96 97 98 99 100 101 102 103 104
/// bundled, the app has to specify which ones to include. For instance a
/// package named `fancy_backgrounds` could have:
///
/// ```
/// lib/backgrounds/background1.png
/// lib/backgrounds/background2.png
/// lib/backgrounds/background3.png
///```
///
105
/// To include, say the first image, the `pubspec.yaml` of the app should specify
106 107 108 109 110 111 112 113 114 115
/// it in the `assets` section:
///
/// ```yaml
///  assets:
///    - packages/fancy_backgrounds/backgrounds/background1.png
/// ```
///
/// Note that the `lib/` is implied, so it should not be included in the asset
/// path.
///
116 117 118 119
/// See also:
///
///  * [Image.asset] for a shorthand of an [Image] widget backed by [AssetImage]
///    when used without a scale.
120 121 122
class AssetImage extends AssetBundleImageProvider {
  /// Creates an object that fetches an image from an asset bundle.
  ///
123 124 125 126 127 128 129 130
  /// The [assetName] argument must not be null. It should name the main asset
  /// from the set of images to choose from. The [package] argument must be
  /// non-null when fetching an asset that is included in package. See the
  /// documentation for the [AssetImage] class itself for details.
  const AssetImage(this.assetName, {
    this.bundle,
    this.package,
  }) : assert(assetName != null);
131

132
  /// The name of the main asset from the set of images to choose from. See the
133
  /// documentation for the [AssetImage] class itself for details.
134 135 136 137 138 139
  final String assetName;

  /// The name used to generate the key to obtain the asset. For local assets
  /// this is [assetName], and for assets from packages the [assetName] is
  /// prefixed 'packages/<package_name>/'.
  String get keyName => package == null ? assetName : 'packages/$package/$assetName';
140 141 142 143 144 145 146 147

  /// The bundle from which the image will be obtained.
  ///
  /// If the provided [bundle] is null, the bundle provided in the
  /// [ImageConfiguration] passed to the [resolve] call will be used instead. If
  /// that is also null, the [rootBundle] is used.
  ///
  /// The image is obtained by calling [AssetBundle.load] on the given [bundle]
148
  /// using the key given by [keyName].
149 150
  final AssetBundle bundle;

151 152 153 154
  /// The name of the package from which the image is included. See the
  /// documentation for the [AssetImage] class itself for details.
  final String package;

155 156 157 158 159 160 161 162 163 164 165 166 167 168
  // We assume the main asset is designed for a device pixel ratio of 1.0
  static const double _naturalResolution = 1.0;

  @override
  Future<AssetBundleImageKey> obtainKey(ImageConfiguration configuration) {
    // This function tries to return a SynchronousFuture if possible. We do this
    // because otherwise showing an image would always take at least one frame,
    // which would be sad. (This code is called from inside build/layout/paint,
    // which all happens in one call frame; using native Futures would guarantee
    // that we resolve each future in a new call frame, and thus not in this
    // build/layout/paint sequence.)
    final AssetBundle chosenBundle = bundle ?? configuration.bundle ?? rootBundle;
    Completer<AssetBundleImageKey> completer;
    Future<AssetBundleImageKey> result;
169
    chosenBundle.loadStructuredData<Map<String, List<String>>>(_kAssetManifestFileName, _manifestParser).then<void>(
170 171
      (Map<String, List<String>> manifest) {
        final String chosenName = _chooseVariant(
172
          keyName,
173
          configuration,
174
          manifest == null ? null : manifest[keyName]
175 176 177 178 179 180 181 182 183 184
        );
        final double chosenScale = _parseScale(chosenName);
        final AssetBundleImageKey key = new AssetBundleImageKey(
          bundle: chosenBundle,
          name: chosenName,
          scale: chosenScale
        );
        if (completer != null) {
          // We already returned from this function, which means we are in the
          // asynchronous mode. Pass the value to the completer. The completer's
185
          // future is what we returned.
186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212
          completer.complete(key);
        } else {
          // We haven't yet returned, so we must have been called synchronously
          // just after loadStructuredData returned (which means it provided us
          // with a SynchronousFuture). Let's return a SynchronousFuture
          // ourselves.
          result = new SynchronousFuture<AssetBundleImageKey>(key);
        }
      }
    ).catchError((dynamic error, StackTrace stack) {
      // We had an error. (This guarantees we weren't called synchronously.)
      // Forward the error to the caller.
      assert(completer != null);
      assert(result == null);
      completer.completeError(error, stack);
    });
    if (result != null) {
      // The code above ran synchronously, and came up with an answer.
      // Return the SynchronousFuture that we created above.
      return result;
    }
    // The code above hasn't yet run its "then" handler yet. Let's prepare a
    // completer for it to use when it does run.
    completer = new Completer<AssetBundleImageKey>();
    return completer.future;
  }

213 214
  static Future<Map<String, List<String>>> _manifestParser(String jsonData) {
    if (jsonData == null)
215 216
      return null;
    // TODO(ianh): JSON decoding really shouldn't be on the main thread.
217
    final Map<String, dynamic> parsedJson = json.decode(jsonData);
218 219 220 221
    final Iterable<String> keys = parsedJson.keys;
    final Map<String, List<String>> parsedManifest =
        new Map<String, List<String>>.fromIterables(keys,
          keys.map((String key) => new List<String>.from(parsedJson[key])));
222
    // TODO(ianh): convert that data structure to the right types.
223
    return new SynchronousFuture<Map<String, List<String>>>(parsedManifest);
224 225 226
  }

  String _chooseVariant(String main, ImageConfiguration config, List<String> candidates) {
227
    if (config.devicePixelRatio == null || candidates == null || candidates.isEmpty)
228 229 230 231 232
      return main;
    // TODO(ianh): Consider moving this parsing logic into _manifestParser.
    final SplayTreeMap<double, String> mapping = new SplayTreeMap<double, String>();
    for (String candidate in candidates)
      mapping[_parseScale(candidate)] = candidate;
Ian Hickson's avatar
Ian Hickson committed
233 234 235
    // TODO(ianh): implement support for config.locale, config.textDirection,
    // config.size, config.platform (then document this over in the Image.asset
    // docs)
236 237 238 239 240 241 242
    return _findNearest(mapping, config.devicePixelRatio);
  }

  // Return the value for the key in a [SplayTreeMap] nearest the provided key.
  String _findNearest(SplayTreeMap<double, String> candidates, double value) {
    if (candidates.containsKey(value))
      return candidates[value];
243 244
    final double lower = candidates.lastKeyBefore(value);
    final double upper = candidates.firstKeyAfter(value);
245 246 247 248 249 250 251 252 253 254
    if (lower == null)
      return candidates[upper];
    if (upper == null)
      return candidates[lower];
    if (value > (lower + upper) / 2)
      return candidates[upper];
    else
      return candidates[lower];
  }

255
  static final RegExp _extractRatioRegExp = new RegExp(r'/?(\d+(\.\d*)?)x/');
256 257

  double _parseScale(String key) {
258
    final Match match = _extractRatioRegExp.firstMatch(key);
259 260
    if (match != null && match.groupCount > 0)
      return double.parse(match.group(1));
261
    return _naturalResolution; // i.e. default to 1.0x
262 263 264 265 266 267 268
  }

  @override
  bool operator ==(dynamic other) {
    if (other.runtimeType != runtimeType)
      return false;
    final AssetImage typedOther = other;
269
    return keyName == typedOther.keyName
270 271 272 273
        && bundle == typedOther.bundle;
  }

  @override
274
  int get hashCode => hashValues(keyName, bundle);
275 276

  @override
277
  String toString() => '$runtimeType(bundle: $bundle, name: "$keyName")';
278
}