image_resolution.dart 10.9 KB
Newer Older
1 2 3 4 5 6 7
// 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';
8
import 'dart:io';
9 10 11
import 'dart:ui' show hashValues;

import 'package:flutter/foundation.dart';
12
import 'package:flutter/services.dart';
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 58

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
/// ```
59
///
60 61 62 63
/// assets/icons/3.0x/heart.png would be a valid variant of
/// assets/icons/heart.png.
///
///
64 65 66 67
/// ## 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
68
/// above. First, the `pubspec.yaml` of the project should specify its assets in
69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94
/// 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.
///
95
/// If the desired asset is specified in the `pubspec.yaml` of the package, it
96
/// is bundled automatically with the app. In particular, assets used by the
97
/// package itself must be specified in its `pubspec.yaml`.
98 99
///
/// A package can also choose to have assets in its 'lib/' folder that are not
100
/// specified in its `pubspec.yaml`. In this case for those images to be
101 102 103 104 105 106 107 108 109
/// 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
///```
///
110
/// To include, say the first image, the `pubspec.yaml` of the app should specify
111 112 113 114 115 116 117 118 119 120
/// 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.
///
121 122 123 124
/// See also:
///
///  * [Image.asset] for a shorthand of an [Image] widget backed by [AssetImage]
///    when used without a scale.
125 126 127
class AssetImage extends AssetBundleImageProvider {
  /// Creates an object that fetches an image from an asset bundle.
  ///
128 129 130 131 132 133 134 135
  /// 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);
136

137
  /// The name of the main asset from the set of images to choose from. See the
138
  /// documentation for the [AssetImage] class itself for details.
139 140 141 142 143 144
  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';
145 146 147 148 149 150 151 152

  /// 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]
153
  /// using the key given by [keyName].
154 155
  final AssetBundle bundle;

156 157 158 159
  /// The name of the package from which the image is included. See the
  /// documentation for the [AssetImage] class itself for details.
  final String package;

160 161 162 163 164 165 166 167 168 169 170 171 172 173
  // 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;
174

175
    chosenBundle.loadStructuredData<Map<String, List<String>>>(_kAssetManifestFileName, _manifestParser).then<void>(
176 177
      (Map<String, List<String>> manifest) {
        final String chosenName = _chooseVariant(
178
          keyName,
179
          configuration,
180
          manifest == null ? null : manifest[keyName]
181 182 183 184 185 186 187 188 189 190
        );
        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
191
          // future is what we returned.
192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218
          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;
  }

219 220
  static Future<Map<String, List<String>>> _manifestParser(String jsonData) {
    if (jsonData == null)
221 222
      return null;
    // TODO(ianh): JSON decoding really shouldn't be on the main thread.
223
    final Map<String, dynamic> parsedJson = json.decode(jsonData);
224 225 226 227
    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])));
228
    // TODO(ianh): convert that data structure to the right types.
229
    return new SynchronousFuture<Map<String, List<String>>>(parsedManifest);
230 231 232
  }

  String _chooseVariant(String main, ImageConfiguration config, List<String> candidates) {
233
    if (config.devicePixelRatio == null || candidates == null || candidates.isEmpty)
234 235 236 237 238
      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
239 240 241
    // TODO(ianh): implement support for config.locale, config.textDirection,
    // config.size, config.platform (then document this over in the Image.asset
    // docs)
242 243 244 245 246 247 248
    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];
249 250
    final double lower = candidates.lastKeyBefore(value);
    final double upper = candidates.firstKeyAfter(value);
251 252 253 254 255 256 257 258 259 260
    if (lower == null)
      return candidates[upper];
    if (upper == null)
      return candidates[lower];
    if (value > (lower + upper) / 2)
      return candidates[upper];
    else
      return candidates[lower];
  }

261
  static final RegExp _extractRatioRegExp = new RegExp(r'/?(\d+(\.\d*)?)x$');
262 263

  double _parseScale(String key) {
264 265 266 267 268 269 270 271 272

    if ( key == assetName){
      return _naturalResolution;
    }

    final File assetPath = new File(key);
    final Directory assetDir = assetPath.parent;

    final Match match = _extractRatioRegExp.firstMatch(assetDir.path);
273 274
    if (match != null && match.groupCount > 0)
      return double.parse(match.group(1));
275
    return _naturalResolution; // i.e. default to 1.0x
276 277 278 279 280 281 282
  }

  @override
  bool operator ==(dynamic other) {
    if (other.runtimeType != runtimeType)
      return false;
    final AssetImage typedOther = other;
283
    return keyName == typedOther.keyName
284 285 286 287
        && bundle == typedOther.bundle;
  }

  @override
288
  int get hashCode => hashValues(keyName, bundle);
289 290

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