diff --git a/examples/widgets/assets/1.5x/starcircle.png b/examples/widgets/assets/1.5x/starcircle.png new file mode 100644 index 0000000000..6db147df17 Binary files /dev/null and b/examples/widgets/assets/1.5x/starcircle.png differ diff --git a/examples/widgets/assets/2.0x/starcircle.png b/examples/widgets/assets/2.0x/starcircle.png new file mode 100644 index 0000000000..82d9109d23 Binary files /dev/null and b/examples/widgets/assets/2.0x/starcircle.png differ diff --git a/examples/widgets/assets/3.0x/starcircle.png b/examples/widgets/assets/3.0x/starcircle.png new file mode 100644 index 0000000000..71cfad626d Binary files /dev/null and b/examples/widgets/assets/3.0x/starcircle.png differ diff --git a/examples/widgets/assets/4.0x/starcircle.png b/examples/widgets/assets/4.0x/starcircle.png new file mode 100644 index 0000000000..a5730b2efb Binary files /dev/null and b/examples/widgets/assets/4.0x/starcircle.png differ diff --git a/examples/widgets/assets/starcircle.png b/examples/widgets/assets/starcircle.png new file mode 100644 index 0000000000..4aaaddb991 Binary files /dev/null and b/examples/widgets/assets/starcircle.png differ diff --git a/examples/widgets/flutter.yaml b/examples/widgets/flutter.yaml index bdaa15bb9c..773cebdad7 100644 --- a/examples/widgets/flutter.yaml +++ b/examples/widgets/flutter.yaml @@ -1,4 +1,6 @@ name: widgets +assets: + - assets/starcircle.png material-design-icons: - name: action/account_circle - name: action/alarm diff --git a/examples/widgets/resolution_awareness.dart b/examples/widgets/resolution_awareness.dart new file mode 100644 index 0000000000..888de8fd6d --- /dev/null +++ b/examples/widgets/resolution_awareness.dart @@ -0,0 +1,86 @@ +// 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 _ratios = const [ 1.0, 1.8, 1.3, 2.4, 2.5, 2.6, 3.9 ]; + +class ExampleState extends State { + + 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: [ + 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()); +} diff --git a/packages/flutter/lib/src/material/icon.dart b/packages/flutter/lib/src/material/icon.dart index e7bbf9f378..f9e8fd0664 100644 --- a/packages/flutter/lib/src/material/icon.dart +++ b/packages/flutter/lib/src/material/icon.dart @@ -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 diff --git a/packages/flutter/lib/src/material/material_app.dart b/packages/flutter/lib/src/material/material_app.dart index fd6c14603e..d3d321b443 100644 --- a/packages/flutter/lib/src/material/material_app.dart +++ b/packages/flutter/lib/src/material/material_app.dart @@ -160,8 +160,9 @@ class _MaterialAppState extends State 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, diff --git a/packages/flutter/lib/src/services/asset_bundle.dart b/packages/flutter/lib/src/services/asset_bundle.dart index 26f4e30be0..d7e79a0f6b 100644 --- a/packages/flutter/lib/src/services/asset_bundle.dart +++ b/packages/flutter/lib/src/services/asset_bundle.dart @@ -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 _imageCache = new Map(); Map> _stringCache = new Map>(); @@ -79,15 +62,35 @@ class MojoAssetBundle extends AssetBundle { return new String.fromCharCodes(new Uint8List.view(data.buffer)); } - Future load(String key) async { - return (await _bundle.ptr.getAsStream(key)).assetData; - } - Future 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 load(String key) async { + return (await _bundle.ptr.getAsStream(key)).assetData; + } +} + AssetBundle _initRootBundle() { try { AssetBundleProxy bundle = new AssetBundleProxy.fromHandle( diff --git a/packages/flutter/lib/src/widgets/asset_vendor.dart b/packages/flutter/lib/src/widgets/asset_vendor.dart new file mode 100644 index 0000000000..94be9819e4 --- /dev/null +++ b/packages/flutter/lib/src/widgets/asset_vendor.dart @@ -0,0 +1,200 @@ +// 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 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 _keyCache = {}; + + Future 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> _assetManifest; + Future _initializer; + + Future _loadManifest() async { + String json = await bundle.loadString("AssetManifest.json"); + _assetManifest = JSON.decode(json); + } + + Future 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 variants = _assetManifest[name]; + if (variants == null) + return name; + else + return chooseVariant(name, variants); + } + + String chooseVariant(String main, List 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 _buildMapping(List candidates) { + SplayTreeMap result = new SplayTreeMap(); + 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 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 candidates) { + SplayTreeMap 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 { + + _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); + } +} diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart index 450aee4d37..41cc8b0691 100644 --- a/packages/flutter/lib/src/widgets/basic.dart +++ b/packages/flutter/lib/src/widgets/basic.dart @@ -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 diff --git a/packages/flutter/lib/widgets.dart b/packages/flutter/lib/widgets.dart index 71fbe43d14..f3328999cd 100644 --- a/packages/flutter/lib/widgets.dart +++ b/packages/flutter/lib/widgets.dart @@ -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'; diff --git a/packages/flutter_tools/lib/src/flx.dart b/packages/flutter_tools/lib/src/flx.dart index a6ed6b93a0..baa757e6fd 100644 --- a/packages/flutter_tools/lib/src/flx.dart +++ b/packages/flutter_tools/lib/src/flx.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 _kDensities = const ['drawable-xxhdpi']; +Map _kIconDensities = { + 'mdpi': 1.0, + 'hdpi' : 1.5, + 'xhdpi' : 2.0, + 'xxhdpi' : 3.0, + 'xxxhdpi' : 4.0 +}; const List _kThemes = const ['white', 'black']; const List _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 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 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 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 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> json = >{}; + for (_Asset main in assets.keys) { + List variants = []; + for (_Asset variant in assets[main]) + variants.add(variant.key); + json[main.key] = variants; + } + List content = UTF8.encode(JSON.encode(json)); return new ArchiveFile.noCompress(key, content.length, content); } @@ -162,8 +219,8 @@ Future 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 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();