flutter/packages/flutter/lib/src/widgets/asset_vendor.dart

238 lines
7.4 KiB
Dart

// 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 'package:flutter/services.dart';
import 'package:mojo/core.dart' as core;
import 'media_query.dart';
import 'basic.dart';
import 'framework.dart';
// Base class for asset resolvers.
abstract class _AssetResolver {
// 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;
final Map<String, String> keyCache = <String, String>{};
Future<core.MojoDataPipeConsumer> load(String key) async {
if (!keyCache.containsKey(key))
keyCache[key] = await resolver.resolve(key);
return await bundle.load(keyCache[key]);
}
}
// Asset bundle that understands how specific asset keys represent image scale.
class _ResolutionAwareAssetBundle extends _ResolvingAssetBundle {
_ResolutionAwareAssetBundle({
AssetBundle bundle,
_ResolutionAwareAssetResolver resolver
}) : super(
bundle: bundle,
resolver: resolver
);
_ResolutionAwareAssetResolver get resolver => super.resolver;
Future<ImageInfo> fetchImage(String key) async {
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]);
return new ImageInfo(
image: await decodeImageFromDataPipe(pipe),
scale: scale
);
}
}
// 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 });
final AssetBundle bundle;
// 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;
Future _initializer;
Future _loadManifest() async {
String json = await bundle.loadString("AssetManifest.json");
_assetManifest = JSON.decode(json);
}
Future<String> resolve(String name) async {
_initializer ??= _loadManifest();
await _initializer;
// If there's no asset manifest, just return the main asset
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;
// 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/");
double getScale(String key) {
Match match = _extractRatioRegExp.firstMatch(key);
if (match != null && match.groupCount > 0)
return double.parse(match.group(1));
return 1.0;
}
// 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];
}
String chooseVariant(String main, List<String> candidates) {
SplayTreeMap<double, String> mapping = new SplayTreeMap<double, String>();
for (String candidate in candidates)
mapping[getScale(candidate)] = candidate;
mapping[_naturalResolution] = main;
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
/// device pixel ratio - see [MediaQuery].
///
/// 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
/// ```
class AssetVendor extends StatefulComponent {
AssetVendor({
Key key,
this.bundle,
this.devicePixelRatio,
this.child
}) : super(key: key);
final AssetBundle bundle;
final double devicePixelRatio;
final Widget child;
_AssetVendorState createState() => new _AssetVendorState();
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('bundle: $bundle');
if (devicePixelRatio != null)
description.add('devicePixelRatio: $devicePixelRatio');
}
}
class _AssetVendorState extends State<AssetVendor> {
_ResolvingAssetBundle _bundle;
void _initBundle() {
_bundle = new _ResolutionAwareAssetBundle(
bundle: config.bundle,
resolver: new _ResolutionAwareAssetResolver(
bundle: config.bundle,
devicePixelRatio: config.devicePixelRatio
)
);
}
void initState() {
super.initState();
_initBundle();
}
void didUpdateConfig(AssetVendor oldConfig) {
if (config.bundle != oldConfig.bundle ||
config.devicePixelRatio != oldConfig.devicePixelRatio) {
_initBundle();
}
}
Widget build(BuildContext context) {
return new DefaultAssetBundle(bundle: _bundle, child: config.child);
}
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('bundle: $_bundle');
}
}