asset_vendor.dart 8.07 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:ui' as ui show Image;
9 10 11 12

import 'package:flutter/services.dart';
import 'package:mojo/core.dart' as core;

13
import 'media_query.dart';
14 15 16 17
import 'basic.dart';
import 'framework.dart';

// Base class for asset resolvers.
Ian Hickson's avatar
Ian Hickson committed
18
abstract class _AssetResolver { // ignore: one_member_abstracts
19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
  // Return a resolved asset key for the asset named [name].
  Future<String> resolve(String name);
}

// Asset bundle capable of producing assets via the resolution logic of an
// asset resolver.
//
// Wraps an underlying [AssetBundle] and forwards calls after resolving the
// asset key.
class _ResolvingAssetBundle extends CachingAssetBundle {

  _ResolvingAssetBundle({ this.bundle, this.resolver });
  final AssetBundle bundle;
  final _AssetResolver resolver;

Kris Giesing's avatar
Kris Giesing committed
34
  final Map<String, String> keyCache = <String, String>{};
35

36
  @override
37
  Future<core.MojoDataPipeConsumer> load(String key) async {
38 39 40 41 42 43
    if (!keyCache.containsKey(key))
      keyCache[key] = await resolver.resolve(key);
    return await bundle.load(keyCache[key]);
  }
}

44 45 46 47 48
/// Abstraction for reading images out of a Mojo data pipe.
///
/// Useful for mocking purposes in unit tests.
typedef Future<ui.Image> ImageDecoder(core.MojoDataPipeConsumer pipe);

49 50 51 52
// Asset bundle that understands how specific asset keys represent image scale.
class _ResolutionAwareAssetBundle extends _ResolvingAssetBundle {
  _ResolutionAwareAssetBundle({
    AssetBundle bundle,
53 54
    _ResolutionAwareAssetResolver resolver,
    ImageDecoder imageDecoder
Kris Giesing's avatar
Kris Giesing committed
55 56
  }) : _imageDecoder = imageDecoder,
  super(
57 58
    bundle: bundle,
    resolver: resolver
Kris Giesing's avatar
Kris Giesing committed
59
  );
60

61
  @override
62 63
  _ResolutionAwareAssetResolver get resolver => super.resolver;

64 65
  final ImageDecoder _imageDecoder;

66
  @override
67
  Future<ImageInfo> fetchImage(String key) async {
68 69 70 71
    core.MojoDataPipeConsumer pipe = await load(key);
    // At this point the key should be in our key cache, and the image
    // resource should be in our image cache
    double scale = resolver.getScale(keyCache[key]);
72
    return new ImageInfo(
73
      image: await _imageDecoder(pipe),
74 75
      scale: scale
    );
76 77 78 79 80 81 82
  }
}

// Base class for resolvers that use the asset manifest to retrieve a list
// of asset variants to choose from.
abstract class _VariantAssetResolver extends _AssetResolver {
  _VariantAssetResolver({ this.bundle });
Ian Hickson's avatar
Ian Hickson committed
83

84
  final AssetBundle bundle;
Ian Hickson's avatar
Ian Hickson committed
85

86 87 88 89 90
  // TODO(kgiesing): Ideally, this cache would be on an object with the same
  // lifetime as the asset bundle it wraps. However, that won't matter until we
  // need to change AssetVendors frequently; as of this writing we only have
  // one.
  Map<String, List<String>> _assetManifest;
Ian Hickson's avatar
Ian Hickson committed
91

92
  Future<Null> _initializer;
93

94
  Future<Null> _loadManifest() async {
95 96 97 98
    String json = await bundle.loadString("AssetManifest.json");
    _assetManifest = JSON.decode(json);
  }

99
  @override
100 101 102
  Future<String> resolve(String name) async {
    _initializer ??= _loadManifest();
    await _initializer;
103
    // If there's no asset manifest, just return the main asset
104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125
    if (_assetManifest == null)
      return name;
    // Allow references directly to variants: if the supplied name is not a
    // key, just return it
    List<String> variants = _assetManifest[name];
    if (variants == null)
      return name;
    else
      return chooseVariant(name, variants);
  }

  String chooseVariant(String main, List<String> variants);
}

// Asset resolver that understands how to determine the best match for the
// current device pixel ratio
class _ResolutionAwareAssetResolver extends _VariantAssetResolver {
  _ResolutionAwareAssetResolver({ AssetBundle bundle, this.devicePixelRatio })
    : super(bundle: bundle);

  final double devicePixelRatio;

126 127 128
  // We assume the main asset is designed for a device pixel ratio of 1.0
  static const double _naturalResolution = 1.0;
  static final RegExp _extractRatioRegExp = new RegExp(r"/?(\d+(\.\d*)?)x/");
129

130 131
  double getScale(String key) {
    Match match = _extractRatioRegExp.firstMatch(key);
Kris Giesing's avatar
Kris Giesing committed
132
    if (match != null && match.groupCount > 0)
133 134
      return double.parse(match.group(1));
    return 1.0;
135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152
  }

  // 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];
    double lower = candidates.lastKeyBefore(value);
    double upper = candidates.firstKeyAfter(value);
    if (lower == null)
      return candidates[upper];
    if (upper == null)
      return candidates[lower];
    if (value > (lower + upper) / 2)
      return candidates[upper];
    else
      return candidates[lower];
  }

153
  @override
154
  String chooseVariant(String main, List<String> candidates) {
155
    SplayTreeMap<double, String> mapping = new SplayTreeMap<double, String>();
Kris Giesing's avatar
Kris Giesing committed
156
    for (String candidate in candidates)
157 158
      mapping[getScale(candidate)] = candidate;
    mapping[_naturalResolution] = main;
159 160 161 162 163 164 165 166 167
    return _findNearest(mapping, devicePixelRatio);
  }
}

/// Establishes an asset resolution strategy for its descendants.
///
/// Given a main asset and a set of variants, AssetVendor chooses the most
/// appropriate asset for the current context. The current asset resolution
/// strategy knows how to find the asset most closely matching the current
168
/// device pixel ratio - see [MediaQuery].
169 170 171 172 173 174 175 176 177 178 179
///
/// 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:
///
180
/// ```
181 182 183
/// heart.png
/// 1.5x/heart.png
/// 2.0x/heart.png
184
/// ```
185 186 187 188 189 190 191 192 193
///
/// 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:
///
194
/// ```
195 196 197
/// icons/heart.png
/// icons/1.5x/heart.png
/// icons/2.0x/heart.png
198
/// ```
199
class AssetVendor extends StatefulWidget {
200 201 202 203
  AssetVendor({
    Key key,
    this.bundle,
    this.devicePixelRatio,
204 205
    this.child,
    this.imageDecoder: decodeImageFromDataPipe
206 207 208
  }) : super(key: key);

  final AssetBundle bundle;
209

210
  final double devicePixelRatio;
211 212

  /// The widget below this widget in the tree.
213
  final Widget child;
214

215
  final ImageDecoder imageDecoder;
216

217
  @override
218
  _AssetVendorState createState() => new _AssetVendorState();
219

220
  @override
221 222 223 224 225 226
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    description.add('bundle: $bundle');
    if (devicePixelRatio != null)
      description.add('devicePixelRatio: $devicePixelRatio');
  }
227 228 229 230 231 232
}

class _AssetVendorState extends State<AssetVendor> {

  _ResolvingAssetBundle _bundle;

233
  void _initBundle() {
234
    _bundle = new _ResolutionAwareAssetBundle(
235
      bundle: config.bundle,
236
      imageDecoder: config.imageDecoder,
237 238 239 240 241 242 243
      resolver: new _ResolutionAwareAssetResolver(
        bundle: config.bundle,
        devicePixelRatio: config.devicePixelRatio
      )
    );
  }

244
  @override
245 246 247 248 249
  void initState() {
    super.initState();
    _initBundle();
  }

250
  @override
251 252 253
  void didUpdateConfig(AssetVendor oldConfig) {
    if (config.bundle != oldConfig.bundle ||
        config.devicePixelRatio != oldConfig.devicePixelRatio) {
254
      _initBundle();
255 256 257
    }
  }

258
  @override
259 260 261
  Widget build(BuildContext context) {
    return new DefaultAssetBundle(bundle: _bundle, child: config.child);
  }
262

263
  @override
264 265 266 267
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    description.add('bundle: $_bundle');
  }
268
}