From bf115ec5b0d141f1bd70a57fc64f1c2bab97eb11 Mon Sep 17 00:00:00 2001 From: Kris Giesing Date: Mon, 1 Feb 2016 11:19:47 -0800 Subject: [PATCH] Add scale awareness to images --- packages/flutter/lib/src/rendering/image.dart | 19 +++++- .../lib/src/services/asset_bundle.dart | 8 +-- .../lib/src/services/image_resource.dart | 3 +- .../flutter/lib/src/widgets/asset_vendor.dart | 63 +++++++++++++------ packages/flutter/lib/src/widgets/basic.dart | 14 +++++ 5 files changed, 80 insertions(+), 27 deletions(-) diff --git a/packages/flutter/lib/src/rendering/image.dart b/packages/flutter/lib/src/rendering/image.dart index 3254ac73a70..01e94b193b6 100644 --- a/packages/flutter/lib/src/rendering/image.dart +++ b/packages/flutter/lib/src/rendering/image.dart @@ -22,6 +22,7 @@ class RenderImage extends RenderBox { ui.Image image, double width, double height, + double scale, Color color, ImageFit fit, FractionalOffset alignment, @@ -30,6 +31,7 @@ class RenderImage extends RenderBox { }) : _image = image, _width = width, _height = height, + _scale = scale, _color = color, _fit = fit, _alignment = alignment, @@ -76,6 +78,18 @@ class RenderImage extends RenderBox { markNeedsLayout(); } + /// If non-null, specify the image's scale. + /// + /// Used when determining the best display size for the image. + double get scale => _scale; + double _scale; + void set scale (double value) { + if (value == _scale) + return; + _scale = value; + markNeedsLayout(); + } + ColorFilter _colorFilter; // Should we make the transfer mode configurable? @@ -161,8 +175,9 @@ class RenderImage extends RenderBox { if (constraints.isTight || _image == null) return constraints.smallest; - double width = _image.width.toDouble(); - double height = _image.height.toDouble(); + double scale = _scale ?? 1.0; + double width = _image.width.toDouble() / scale; + double height = _image.height.toDouble() / scale; assert(width > 0.0); assert(height > 0.0); double aspectRatio = width / height; diff --git a/packages/flutter/lib/src/services/asset_bundle.dart b/packages/flutter/lib/src/services/asset_bundle.dart index d7e79a0f6bd..866aaba173a 100644 --- a/packages/flutter/lib/src/services/asset_bundle.dart +++ b/packages/flutter/lib/src/services/asset_bundle.dart @@ -43,16 +43,16 @@ class NetworkAssetBundle extends AssetBundle { } abstract class CachingAssetBundle extends AssetBundle { - Map _imageCache = new Map(); + Map imageCache = new Map(); Map> _stringCache = new Map>(); - Future _fetchImage(String key) async { + Future fetchImage(String key) async { return await decodeImageFromDataPipe(await load(key)); } ImageResource loadImage(String key) { - return _imageCache.putIfAbsent(key, () { - return new ImageResource(_fetchImage(key)); + return imageCache.putIfAbsent(key, () { + return new ImageResource(fetchImage(key)); }); } diff --git a/packages/flutter/lib/src/services/image_resource.dart b/packages/flutter/lib/src/services/image_resource.dart index 9732f3b0bb6..91493612c7a 100644 --- a/packages/flutter/lib/src/services/image_resource.dart +++ b/packages/flutter/lib/src/services/image_resource.dart @@ -16,13 +16,14 @@ typedef void ImageListener(ui.Image image); /// image object might change over time, either because the image is animating /// or because the underlying image resource was mutated. class ImageResource { - ImageResource(this._futureImage) { + ImageResource(this._futureImage, { this.scale : 1.0 }) { _futureImage.then(_handleImageLoaded, onError: (exception, stack) => _handleImageError('Failed to load image:', exception, stack)); } bool _resolved = false; Future _futureImage; ui.Image _image; + double scale; final List _listeners = new List(); /// The first concrete [ui.Image] object represented by this handle. diff --git a/packages/flutter/lib/src/widgets/asset_vendor.dart b/packages/flutter/lib/src/widgets/asset_vendor.dart index 94be9819e41..e005381aedf 100644 --- a/packages/flutter/lib/src/widgets/asset_vendor.dart +++ b/packages/flutter/lib/src/widgets/asset_vendor.dart @@ -5,6 +5,7 @@ import 'dart:async'; import 'dart:collection'; import 'dart:convert'; +import 'dart:ui' as ui; import 'package:flutter/services.dart'; import 'package:mojo/core.dart' as core; @@ -29,12 +30,34 @@ class _ResolvingAssetBundle extends CachingAssetBundle { final AssetBundle bundle; final _AssetResolver resolver; - Map _keyCache = {}; + Map keyCache = {}; Future load(String key) async { - if (!_keyCache.containsKey(key)) - _keyCache[key] = await resolver.resolve(key); - return await bundle.load(_keyCache[key]); + 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 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]); + this.imageCache[key].scale = scale; + return await decodeImageFromDataPipe(pipe); } } @@ -58,7 +81,7 @@ abstract class _VariantAssetResolver extends _AssetResolver { Future resolve(String name) async { _initializer ??= _loadManifest(); await _initializer; - // If there's no asset manifest, just return the main asset always + // 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 @@ -81,18 +104,16 @@ class _ResolutionAwareAssetResolver extends _VariantAssetResolver { final double devicePixelRatio; - static final RegExp extractRatioRegExp = new RegExp(r"/?(\d+(\.\d*)?)x/"); + // 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/"); - 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; - } + double getScale(String key) { + Match match = _extractRatioRegExp.firstMatch(key); + if (match != null && match.groupCount > 0) { + return double.parse(match.group(1)); } - return result; + return 1.0; } // Return the value for the key in a [SplayTreeMap] nearest the provided key. @@ -112,9 +133,11 @@ class _ResolutionAwareAssetResolver extends _VariantAssetResolver { } 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; + SplayTreeMap mapping = new SplayTreeMap(); + for (String candidate in candidates) { + mapping[getScale(candidate)] = candidate; + } + mapping[_naturalResolution] = main; return _findNearest(mapping, devicePixelRatio); } } @@ -172,7 +195,7 @@ class _AssetVendorState extends State { void initState() { super.initState(); - _bundle = new _ResolvingAssetBundle( + _bundle = new _ResolutionAwareAssetBundle( bundle: config.bundle, resolver: new _ResolutionAwareAssetResolver( bundle: config.bundle, @@ -184,7 +207,7 @@ class _AssetVendorState extends State { void didUpdateConfig(AssetVendor oldConfig) { if (config.bundle != oldConfig.bundle || config.devicePixelRatio != oldConfig.devicePixelRatio) { - _bundle = new _ResolvingAssetBundle( + _bundle = new _ResolutionAwareAssetBundle( bundle: config.bundle, resolver: new _ResolutionAwareAssetResolver( bundle: config.bundle, diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart index 070dbd7ec11..a7dc6a07968 100644 --- a/packages/flutter/lib/src/widgets/basic.dart +++ b/packages/flutter/lib/src/widgets/basic.dart @@ -1508,6 +1508,7 @@ class RawImage extends LeafRenderObjectWidget { this.image, this.width, this.height, + this.scale, this.color, this.fit, this.alignment, @@ -1530,6 +1531,11 @@ class RawImage extends LeafRenderObjectWidget { /// aspect ratio. final double height; + /// If non-null, specify the image's scale. + /// + /// Used when determining the best display size for the image. + final double scale; + /// If non-null, apply this color filter to the image before painting. final Color color; @@ -1559,6 +1565,7 @@ class RawImage extends LeafRenderObjectWidget { image: image, width: width, height: height, + scale: scale, color: color, fit: fit, alignment: alignment, @@ -1569,6 +1576,7 @@ class RawImage extends LeafRenderObjectWidget { renderObject.image = image; renderObject.width = width; renderObject.height = height; + renderObject.scale = scale; renderObject.color = color; renderObject.alignment = alignment; renderObject.fit = fit; @@ -1650,11 +1658,16 @@ class _ImageListenerState extends State { config.image.addListener(_handleImageChanged); } + // Note, we remember the resolved image and the scale together. If the + // source image resource is updated, we shouldn't grab its scale until + // it provides the associated resolved image. ui.Image _resolvedImage; + double _scale; void _handleImageChanged(ui.Image resolvedImage) { setState(() { _resolvedImage = resolvedImage; + _scale = config.image.scale; }); } @@ -1675,6 +1688,7 @@ class _ImageListenerState extends State { image: _resolvedImage, width: config.width, height: config.height, + scale: _scale, color: config.color, fit: config.fit, alignment: config.alignment,