Commit 1b75de1b authored by krisgiesing's avatar krisgiesing

Merge pull request #1295 from krisgiesing/resolution_awareness

Add support for asset variants; add resolution aware asset image management
parents 36433952 9572b496
This diff was suppressed by a .gitattributes entry.
This diff was suppressed by a .gitattributes entry.
This diff was suppressed by a .gitattributes entry.
This diff was suppressed by a .gitattributes entry.
This diff was suppressed by a .gitattributes entry.
name: widgets
assets:
- assets/starcircle.png
material-design-icons:
- name: action/account_circle
- name: action/alarm
......
// Copyright (c) 2016, the Flutter project authors. Please see the AUTHORS file
// for details. 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:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
class ExampleApp extends StatefulComponent {
ExampleState createState() => new ExampleState();
}
const List<double> _ratios = const <double>[ 1.0, 1.8, 1.3, 2.4, 2.5, 2.6, 3.9 ];
class ExampleState extends State<ExampleApp> {
int _index = 0;
double _ratio = _ratios[0];
final EdgeDims padding = new EdgeDims.TRBL(
ui.window.padding.top,
ui.window.padding.right,
ui.window.padding.bottom,
ui.window.padding.left
);
void _handlePressed() {
setState(() {
_index++;
_index = _index % _ratios.length;
_ratio = _ratios[_index];
});
}
Widget build(BuildContext context) {
const double size = 200.0; // 200 logical pixels
TextStyle style = new TextStyle(color: const Color(0xFF0000000));
return new MediaQuery(
data: new MediaQueryData(
size: ui.window.size,
devicePixelRatio: _ratio,
padding: padding
),
child: new AssetVendor(
bundle: rootBundle,
devicePixelRatio: _ratio,
child: new Material(
child: new Padding(
padding: const EdgeDims.symmetric(vertical: 48.0),
child: new Column(
children: <Widget>[
new AssetImage(
name: 'assets/2.0x/starcircle.png',
height: size,
width: size,
fit: ImageFit.fill
),
new Text('Image designed for pixel ratio 2.0', style: style),
new AssetImage(
name: 'assets/starcircle.png',
height: size,
width: size,
fit: ImageFit.fill
),
new Text(
'Image variant for pixel ratio: ' + _ratio.toString(),
style: style
),
new RaisedButton(
child: new Text('Change pixel ratio', style: style),
onPressed: _handlePressed
)
],
justifyContent: FlexJustifyContent.spaceBetween
)
)
)
)
);
}
}
main() {
runApp(new ExampleApp());
}
......@@ -65,13 +65,10 @@ class Icon extends StatelessComponent {
category = parts[0];
subtype = parts[1];
}
// TODO(eseidel): This clearly isn't correct. Not sure what would be.
// Should we use the ios images on ios?
String density = 'drawable-xxhdpi';
String colorSuffix = _getColorSuffix(context);
int iconSize = _kIconSize[size];
return new AssetImage(
name: '$category/$density/ic_${subtype}_${colorSuffix}_${iconSize}dp.png',
name: '$category/ic_${subtype}_${colorSuffix}_${iconSize}dp.png',
width: iconSize.toDouble(),
height: iconSize.toDouble(),
color: color
......
......@@ -160,8 +160,9 @@ class _MaterialAppState extends State<MaterialApp> implements BindingObserver {
duration: kThemeAnimationDuration,
child: new DefaultTextStyle(
style: _errorTextStyle,
child: new DefaultAssetBundle(
child: new AssetVendor(
bundle: _defaultBundle,
devicePixelRatio: ui.window.devicePixelRatio,
child: new Title(
title: config.title,
color: theme.primaryColor,
......
......@@ -42,24 +42,7 @@ class NetworkAssetBundle extends AssetBundle {
}
}
Future _fetchAndUnpackBundle(String relativeUrl, AssetBundleProxy bundle) async {
core.MojoDataPipeConsumer bundleData = (await fetchUrl(relativeUrl)).body;
AssetUnpackerProxy unpacker = new AssetUnpackerProxy.unbound();
shell.connectToService("mojo:asset_bundle", unpacker);
unpacker.ptr.unpackZipStream(bundleData, bundle);
unpacker.close();
}
class MojoAssetBundle extends AssetBundle {
MojoAssetBundle(this._bundle);
factory MojoAssetBundle.fromNetwork(String relativeUrl) {
AssetBundleProxy bundle = new AssetBundleProxy.unbound();
_fetchAndUnpackBundle(relativeUrl, bundle);
return new MojoAssetBundle(bundle);
}
AssetBundleProxy _bundle;
abstract class CachingAssetBundle extends AssetBundle {
Map<String, ImageResource> _imageCache = new Map<String, ImageResource>();
Map<String, Future<String>> _stringCache = new Map<String, Future<String>>();
......@@ -79,15 +62,35 @@ class MojoAssetBundle extends AssetBundle {
return new String.fromCharCodes(new Uint8List.view(data.buffer));
}
Future<core.MojoDataPipeConsumer> load(String key) async {
return (await _bundle.ptr.getAsStream(key)).assetData;
}
Future<String> loadString(String key) {
return _stringCache.putIfAbsent(key, () => _fetchString(key));
}
}
class MojoAssetBundle extends CachingAssetBundle {
MojoAssetBundle(this._bundle);
factory MojoAssetBundle.fromNetwork(String relativeUrl) {
AssetBundleProxy bundle = new AssetBundleProxy.unbound();
_fetchAndUnpackBundle(relativeUrl, bundle);
return new MojoAssetBundle(bundle);
}
static Future _fetchAndUnpackBundle(String relativeUrl, AssetBundleProxy bundle) async {
core.MojoDataPipeConsumer bundleData = (await fetchUrl(relativeUrl)).body;
AssetUnpackerProxy unpacker = new AssetUnpackerProxy.unbound();
shell.connectToService("mojo:asset_bundle", unpacker);
unpacker.ptr.unpackZipStream(bundleData, bundle);
unpacker.close();
}
AssetBundleProxy _bundle;
Future<core.MojoDataPipeConsumer> load(String key) async {
return (await _bundle.ptr.getAsStream(key)).assetData;
}
}
AssetBundle _initRootBundle() {
try {
AssetBundleProxy bundle = new AssetBundleProxy.fromHandle(
......
// 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 '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;
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]);
}
}
// 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 always
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;
static final RegExp extractRatioRegExp = new RegExp(r"/?(\d+(\.\d*)?)x/");
SplayTreeMap<double, String> _buildMapping(List<String> candidates) {
SplayTreeMap<double, String> result = new SplayTreeMap<double, String>();
for (String candidate in candidates) {
Match match = extractRatioRegExp.firstMatch(candidate);
if (match != null && match.groupCount > 0) {
double resolution = double.parse(match.group(1));
result[resolution] = candidate;
}
}
return result;
}
// 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 = _buildMapping(candidates);
// We assume the main asset is designed for a device pixel ratio of 1.0
mapping[1.0] = 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, as given by [MediaQueryData].
///
/// 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();
}
class _AssetVendorState extends State<AssetVendor> {
_ResolvingAssetBundle _bundle;
void initState() {
super.initState();
_bundle = new _ResolvingAssetBundle(
bundle: config.bundle,
resolver: new _ResolutionAwareAssetResolver(
bundle: config.bundle,
devicePixelRatio: config.devicePixelRatio
)
);
}
void didUpdateConfig(AssetVendor oldConfig) {
if (config.bundle != oldConfig.bundle ||
config.devicePixelRatio != oldConfig.devicePixelRatio) {
_bundle = new _ResolvingAssetBundle(
bundle: config.bundle,
resolver: new _ResolutionAwareAssetResolver(
bundle: config.bundle,
devicePixelRatio: config.devicePixelRatio
)
);
}
}
Widget build(BuildContext context) {
return new DefaultAssetBundle(bundle: _bundle, child: config.child);
}
}
......@@ -1826,7 +1826,7 @@ class AsyncImage extends StatelessComponent {
/// Displays an image from an [AssetBundle].
///
/// By default, asset image will load the image from the cloest enclosing
/// By default, asset image will load the image from the closest enclosing
/// [DefaultAssetBundle].
class AssetImage extends StatelessComponent {
// Don't add asserts here unless absolutely necessary, since it will
......
......@@ -5,6 +5,7 @@
/// The Flutter widget framework.
library widgets;
export 'src/widgets/asset_vendor.dart';
export 'src/widgets/basic.dart';
export 'src/widgets/binding.dart';
export 'src/widgets/dismissable.dart';
......
......@@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
......@@ -24,42 +25,77 @@ const String defaultSnapshotPath = 'build/snapshot_blob.bin';
const String defaultPrivateKeyPath = 'privatekey.der';
const String _kSnapshotKey = 'snapshot_blob.bin';
const List<String> _kDensities = const ['drawable-xxhdpi'];
Map<String, double> _kIconDensities = {
'mdpi': 1.0,
'hdpi' : 1.5,
'xhdpi' : 2.0,
'xxhdpi' : 3.0,
'xxxhdpi' : 4.0
};
const List<String> _kThemes = const ['white', 'black'];
const List<int> _kSizes = const [18, 24, 36, 48];
class _Asset {
final String source;
final String base;
final String key;
_Asset({ this.base, this.key });
_Asset({ this.source, this.base, this.key });
}
Iterable<_Asset> _parseAssets(Map manifestDescriptor, String manifestPath) sync* {
if (manifestDescriptor == null || !manifestDescriptor.containsKey('assets'))
return;
Map<_Asset, List<_Asset>> _parseAssets(Map manifestDescriptor, String manifestPath) {
Map<_Asset, List<_Asset>> result = <_Asset, List<_Asset>>{};
if (manifestDescriptor == null)
return result;
String basePath = path.dirname(path.absolute(manifestPath));
for (String asset in manifestDescriptor['assets'])
yield new _Asset(base: basePath, key: asset);
if (manifestDescriptor.containsKey('assets')) {
for (String asset in manifestDescriptor['assets']) {
_Asset baseAsset = new _Asset(base: basePath, key: asset);
List<_Asset> variants = <_Asset>[];
result[baseAsset] = variants;
// Find asset variants
String assetPath = path.join(basePath, asset);
String assetFilename = path.basename(assetPath);
Directory assetDir = new Directory(path.dirname(assetPath));
List<FileSystemEntity> files = assetDir.listSync(recursive: true);
for (FileSystemEntity entity in files) {
if (path.basename(entity.path) == assetFilename &&
FileSystemEntity.isFileSync(entity.path) &&
entity.path != assetPath) {
String key = path.relative(entity.path, from: basePath);
variants.add(new _Asset(base: basePath, key: key));
}
}
}
}
return result;
}
class _MaterialAsset {
class _MaterialAsset extends _Asset {
final String name;
final String density;
final String theme;
final int size;
_MaterialAsset(Map descriptor)
: name = descriptor['name'],
density = descriptor['density'],
theme = descriptor['theme'],
size = descriptor['size'];
_MaterialAsset(this.name, this.density, this.theme, this.size, String assetBase)
: super(base: assetBase);
String get source {
List<String> parts = name.split('/');
String category = parts[0];
String subtype = parts[1];
return '$category/drawable-$density/ic_${subtype}_${theme}_${size}dp.png';
}
String get key {
List<String> parts = name.split('/');
String category = parts[0];
String subtype = parts[1];
return '$category/$density/ic_${subtype}_${theme}_${size}dp.png';
double devicePixelRatio = _kIconDensities[density];
if (devicePixelRatio == 1.0)
return '$category/ic_${subtype}_${theme}_${size}dp.png';
else
return '$category/${devicePixelRatio}x/ic_${subtype}_${theme}_${size}dp.png';
}
}
......@@ -69,28 +105,30 @@ List _generateValues(Map assetDescriptor, String key, List defaults) {
return defaults;
}
Iterable<_MaterialAsset> _generateMaterialAssets(Map assetDescriptor) sync* {
Map currentAssetDescriptor = new Map.from(assetDescriptor);
for (String density in _generateValues(assetDescriptor, 'density', _kDensities)) {
currentAssetDescriptor['density'] = density;
for (String theme in _generateValues(assetDescriptor, 'theme', _kThemes)) {
currentAssetDescriptor['theme'] = theme;
for (int size in _generateValues(assetDescriptor, 'size', _kSizes)) {
currentAssetDescriptor['size'] = size;
yield new _MaterialAsset(currentAssetDescriptor);
void _accumulateMaterialAssets(Map<_Asset, List<_Asset>> result, Map assetDescriptor, String assetBase) {
String name = assetDescriptor['name'];
for (String theme in _generateValues(assetDescriptor, 'theme', _kThemes)) {
for (int size in _generateValues(assetDescriptor, 'size', _kSizes)) {
_MaterialAsset main = new _MaterialAsset(name, 'mdpi', theme, size, assetBase);
List<_Asset> variants = <_Asset>[];
result[main] = variants;
for (String density in _generateValues(assetDescriptor, 'density', _kIconDensities.keys)) {
if (density == 'mdpi')
continue;
variants.add(new _MaterialAsset(name, density, theme, size, assetBase));
}
}
}
}
Iterable<_MaterialAsset> _parseMaterialAssets(Map manifestDescriptor) sync* {
Map<_Asset, List<_Asset>> _parseMaterialAssets(Map manifestDescriptor, String assetBase) {
Map<_Asset, List<_Asset>> result = <_Asset, List<_Asset>>{};
if (manifestDescriptor == null || !manifestDescriptor.containsKey('material-design-icons'))
return;
return result;
for (Map assetDescriptor in manifestDescriptor['material-design-icons']) {
for (_MaterialAsset asset in _generateMaterialAssets(assetDescriptor)) {
yield asset;
}
_accumulateMaterialAssets(result, assetDescriptor, assetBase);
}
return result;
}
dynamic _loadManifest(String manifestPath) {
......@@ -100,11 +138,30 @@ dynamic _loadManifest(String manifestPath) {
return loadYaml(manifestDescriptor);
}
ArchiveFile _createFile(String key, String assetBase) {
File file = new File('$assetBase/$key');
if (!file.existsSync())
return null;
bool _addAssetFile(Archive archive, _Asset asset) {
String source = asset.source ?? asset.key;
File file = new File('${asset.base}/$source');
if (!file.existsSync()) {
printError('Cannot find asset "$source" in directory "${path.absolute(asset.base)}".');
return false;
}
List<int> content = file.readAsBytesSync();
archive.addFile(
new ArchiveFile.noCompress(asset.key, content.length, content)
);
return true;
}
ArchiveFile _createAssetManifest(Map<_Asset, List<_Asset>> assets) {
String key = 'AssetManifest.json';
Map<String, List<String>> json = <String, List<String>>{};
for (_Asset main in assets.keys) {
List<String> variants = <String>[];
for (_Asset variant in assets[main])
variants.add(variant.key);
json[main.key] = variants;
}
List<int> content = UTF8.encode(JSON.encode(json));
return new ArchiveFile.noCompress(key, content.length, content);
}
......@@ -162,8 +219,8 @@ Future<int> build(
Map manifestDescriptor = _loadManifest(manifestPath);
Iterable<_Asset> assets = _parseAssets(manifestDescriptor, manifestPath);
Iterable<_MaterialAsset> materialAssets = _parseMaterialAssets(manifestDescriptor);
Map<_Asset, List<_Asset>> assets = _parseAssets(manifestDescriptor, manifestPath);
assets.addAll(_parseMaterialAssets(manifestDescriptor, assetBase));
Archive archive = new Archive();
......@@ -181,20 +238,16 @@ Future<int> build(
archive.addFile(_createSnapshotFile(snapshotPath));
}
for (_Asset asset in assets) {
ArchiveFile file = _createFile(asset.key, asset.base);
if (file == null) {
printError('Cannot find asset "${asset.key}" in directory "${path.absolute(asset.base)}".');
for (_Asset asset in assets.keys) {
if (!_addAssetFile(archive, asset))
return 1;
for (_Asset variant in assets[asset]) {
if (!_addAssetFile(archive, variant))
return 1;
}
archive.addFile(file);
}
for (_MaterialAsset asset in materialAssets) {
ArchiveFile file = _createFile(asset.key, assetBase);
if (file != null)
archive.addFile(file);
}
archive.addFile(_createAssetManifest(assets));
await CipherParameters.get().seedRandom();
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment