
This makes it match the material spec more. https://www.google.com/design/spec/style/icons.html Fixes https://github.com/flutter/flutter/issues/1357
238 lines
7.4 KiB
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');
|
|
}
|
|
}
|