Commit aef96b80 authored by krisgiesing's avatar krisgiesing

Merge pull request #1505 from krisgiesing/image_scaling

Add scale awareness to images
parents b89d81a9 5161d120
...@@ -82,8 +82,8 @@ void main() { ...@@ -82,8 +82,8 @@ void main() {
// Resizeable image // Resizeable image
image = new RenderImageGrow(null, new Size(100.0, null)); image = new RenderImageGrow(null, new Size(100.0, null));
imageCache.load("http://flutter.io/favicon.ico").first.then((ui.Image dartLogo) { imageCache.load("http://flutter.io/favicon.ico").first.then((ImageInfo dartLogo) {
image.image = dartLogo; image.image = dartLogo.image;
}); });
row.add(new RenderPadding(padding: const EdgeDims.all(10.0), child: image)); row.add(new RenderPadding(padding: const EdgeDims.all(10.0), child: image));
......
...@@ -668,10 +668,10 @@ class BackgroundImage { ...@@ -668,10 +668,10 @@ class BackgroundImage {
_imageResource.removeListener(_handleImageChanged); _imageResource.removeListener(_handleImageChanged);
} }
void _handleImageChanged(ui.Image resolvedImage) { void _handleImageChanged(ImageInfo resolvedImage) {
if (resolvedImage == null) if (resolvedImage == null)
return; return;
_image = resolvedImage; _image = resolvedImage.image;
final List<VoidCallback> localListeners = final List<VoidCallback> localListeners =
new List<VoidCallback>.from(_listeners); new List<VoidCallback>.from(_listeners);
for (VoidCallback listener in localListeners) for (VoidCallback listener in localListeners)
......
...@@ -22,6 +22,7 @@ class RenderImage extends RenderBox { ...@@ -22,6 +22,7 @@ class RenderImage extends RenderBox {
ui.Image image, ui.Image image,
double width, double width,
double height, double height,
double scale: 1.0,
Color color, Color color,
ImageFit fit, ImageFit fit,
FractionalOffset alignment, FractionalOffset alignment,
...@@ -30,6 +31,7 @@ class RenderImage extends RenderBox { ...@@ -30,6 +31,7 @@ class RenderImage extends RenderBox {
}) : _image = image, }) : _image = image,
_width = width, _width = width,
_height = height, _height = height,
_scale = scale,
_color = color, _color = color,
_fit = fit, _fit = fit,
_alignment = alignment, _alignment = alignment,
...@@ -76,6 +78,19 @@ class RenderImage extends RenderBox { ...@@ -76,6 +78,19 @@ class RenderImage extends RenderBox {
markNeedsLayout(); markNeedsLayout();
} }
/// Specifies 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) {
assert(value != null);
if (value == _scale)
return;
_scale = value;
markNeedsLayout();
}
ColorFilter _colorFilter; ColorFilter _colorFilter;
// Should we make the transfer mode configurable? // Should we make the transfer mode configurable?
...@@ -161,8 +176,8 @@ class RenderImage extends RenderBox { ...@@ -161,8 +176,8 @@ class RenderImage extends RenderBox {
if (constraints.isTight || _image == null) if (constraints.isTight || _image == null)
return constraints.smallest; return constraints.smallest;
double width = _image.width.toDouble(); double width = _image.width.toDouble() / _scale;
double height = _image.height.toDouble(); double height = _image.height.toDouble() / _scale;
assert(width > 0.0); assert(width > 0.0);
assert(height > 0.0); assert(height > 0.0);
double aspectRatio = width / height; double aspectRatio = width / height;
......
...@@ -3,7 +3,6 @@ ...@@ -3,7 +3,6 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async'; import 'dart:async';
import 'dart:ui' as ui;
import 'dart:ui_internals' as internals; import 'dart:ui_internals' as internals;
import 'dart:typed_data'; import 'dart:typed_data';
...@@ -43,16 +42,18 @@ class NetworkAssetBundle extends AssetBundle { ...@@ -43,16 +42,18 @@ class NetworkAssetBundle extends AssetBundle {
} }
abstract class CachingAssetBundle extends AssetBundle { abstract class CachingAssetBundle extends AssetBundle {
Map<String, ImageResource> _imageCache = new Map<String, ImageResource>(); final Map<String, ImageResource> imageResourceCache =
Map<String, Future<String>> _stringCache = new Map<String, Future<String>>(); <String, ImageResource>{};
final Map<String, Future<String>> _stringCache =
<String, Future<String>>{};
Future<ui.Image> _fetchImage(String key) async { Future<ImageInfo> fetchImage(String key) async {
return await decodeImageFromDataPipe(await load(key)); return new ImageInfo(image: await decodeImageFromDataPipe(await load(key)));
} }
ImageResource loadImage(String key) { ImageResource loadImage(String key) {
return _imageCache.putIfAbsent(key, () { return imageResourceCache.putIfAbsent(key, () {
return new ImageResource(_fetchImage(key)); return new ImageResource(fetchImage(key));
}); });
} }
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async'; import 'dart:async';
import 'dart:ui' as ui; import 'dart:ui' show hashValues;
import 'package:mojo/mojo/url_response.mojom.dart'; import 'package:mojo/mojo/url_response.mojom.dart';
import 'package:quiver/collection.dart'; import 'package:quiver/collection.dart';
...@@ -15,25 +15,32 @@ import 'image_resource.dart'; ...@@ -15,25 +15,32 @@ import 'image_resource.dart';
/// Implements a way to retrieve an image, for example by fetching it from the network. /// Implements a way to retrieve an image, for example by fetching it from the network.
/// Also used as a key in the image cache. /// Also used as a key in the image cache.
abstract class ImageProvider { abstract class ImageProvider {
Future<ui.Image> loadImage(); Future<ImageInfo> loadImage();
} }
class _UrlFetcher implements ImageProvider { class _UrlFetcher implements ImageProvider {
final String _url; final String _url;
final double _scale;
_UrlFetcher(this._url); _UrlFetcher(this._url, this._scale);
Future<ui.Image> loadImage() async { Future<ImageInfo> loadImage() async {
UrlResponse response = await fetchUrl(_url); UrlResponse response = await fetchUrl(_url);
if (response.statusCode >= 400) { if (response.statusCode >= 400) {
print("Failed (${response.statusCode}) to load image $_url"); print("Failed (${response.statusCode}) to load image $_url");
return null; return null;
} }
return await decodeImageFromDataPipe(response.body); return new ImageInfo(
image: await decodeImageFromDataPipe(response.body),
scale: _scale
);
} }
bool operator ==(other) => other is _UrlFetcher && _url == other._url; bool operator ==(other) {
int get hashCode => _url.hashCode; return other is _UrlFetcher && _url == other._url && _scale == other._scale;
}
int get hashCode => hashValues(_url, _scale);
} }
const int _kDefaultSize = 1000; const int _kDefaultSize = 1000;
...@@ -53,8 +60,8 @@ class _ImageCache { ...@@ -53,8 +60,8 @@ class _ImageCache {
}); });
} }
ImageResource load(String url) { ImageResource load(String url, { double scale: 1.0 }) {
return loadProvider(new _UrlFetcher(url)); return loadProvider(new _UrlFetcher(url, scale));
} }
} }
......
...@@ -7,8 +7,14 @@ import 'dart:ui' as ui; ...@@ -7,8 +7,14 @@ import 'dart:ui' as ui;
import 'print.dart'; import 'print.dart';
class ImageInfo {
ImageInfo({ this.image, this.scale: 1.0 });
final ui.Image image;
final double scale;
}
/// A callback for when the image is available. /// A callback for when the image is available.
typedef void ImageListener(ui.Image image); typedef void ImageListener(ImageInfo image);
/// A handle to an image resource /// A handle to an image resource
/// ///
...@@ -21,15 +27,15 @@ class ImageResource { ...@@ -21,15 +27,15 @@ class ImageResource {
} }
bool _resolved = false; bool _resolved = false;
Future<ui.Image> _futureImage; Future<ImageInfo> _futureImage;
ui.Image _image; ImageInfo _image;
final List<ImageListener> _listeners = new List<ImageListener>(); final List<ImageListener> _listeners = new List<ImageListener>();
/// The first concrete [ui.Image] object represented by this handle. /// The first concrete [ui.Image] object represented by this handle.
/// ///
/// Instead of receivingly only the first image, most clients will want to /// Instead of receivingly only the first image, most clients will want to
/// [addListener] to be notified whenever a a concrete image is available. /// [addListener] to be notified whenever a a concrete image is available.
Future<ui.Image> get first => _futureImage; Future<ImageInfo> get first => _futureImage;
/// Adds a listener callback that is called whenever a concrete [ui.Image] /// Adds a listener callback that is called whenever a concrete [ui.Image]
/// object is available. Note: If a concrete image is available currently, /// object is available. Note: If a concrete image is available currently,
...@@ -50,7 +56,7 @@ class ImageResource { ...@@ -50,7 +56,7 @@ class ImageResource {
_listeners.remove(listener); _listeners.remove(listener);
} }
void _handleImageLoaded(ui.Image image) { void _handleImageLoaded(ImageInfo image) {
_image = image; _image = image;
_resolved = true; _resolved = true;
_notifyListeners(); _notifyListeners();
......
...@@ -29,12 +29,36 @@ class _ResolvingAssetBundle extends CachingAssetBundle { ...@@ -29,12 +29,36 @@ class _ResolvingAssetBundle extends CachingAssetBundle {
final AssetBundle bundle; final AssetBundle bundle;
final _AssetResolver resolver; final _AssetResolver resolver;
Map<String, String> _keyCache = <String, String>{}; final Map<String, String> keyCache = <String, String>{};
Future<core.MojoDataPipeConsumer> load(String key) async { Future<core.MojoDataPipeConsumer> load(String key) async {
if (!_keyCache.containsKey(key)) if (!keyCache.containsKey(key))
_keyCache[key] = await resolver.resolve(key); keyCache[key] = await resolver.resolve(key);
return await bundle.load(_keyCache[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
);
} }
} }
...@@ -58,7 +82,7 @@ abstract class _VariantAssetResolver extends _AssetResolver { ...@@ -58,7 +82,7 @@ abstract class _VariantAssetResolver extends _AssetResolver {
Future<String> resolve(String name) async { Future<String> resolve(String name) async {
_initializer ??= _loadManifest(); _initializer ??= _loadManifest();
await _initializer; 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) if (_assetManifest == null)
return name; return name;
// Allow references directly to variants: if the supplied name is not a // Allow references directly to variants: if the supplied name is not a
...@@ -81,18 +105,15 @@ class _ResolutionAwareAssetResolver extends _VariantAssetResolver { ...@@ -81,18 +105,15 @@ class _ResolutionAwareAssetResolver extends _VariantAssetResolver {
final double devicePixelRatio; 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;
SplayTreeMap<double, String> _buildMapping(List<String> candidates) { static final RegExp _extractRatioRegExp = new RegExp(r"/?(\d+(\.\d*)?)x/");
SplayTreeMap<double, String> result = new SplayTreeMap<double, String>();
for (String candidate in candidates) { double getScale(String key) {
Match match = extractRatioRegExp.firstMatch(candidate); Match match = _extractRatioRegExp.firstMatch(key);
if (match != null && match.groupCount > 0) { if (match != null && match.groupCount > 0)
double resolution = double.parse(match.group(1)); return double.parse(match.group(1));
result[resolution] = candidate; return 1.0;
}
}
return result;
} }
// Return the value for the key in a [SplayTreeMap] nearest the provided key. // Return the value for the key in a [SplayTreeMap] nearest the provided key.
...@@ -112,9 +133,10 @@ class _ResolutionAwareAssetResolver extends _VariantAssetResolver { ...@@ -112,9 +133,10 @@ class _ResolutionAwareAssetResolver extends _VariantAssetResolver {
} }
String chooseVariant(String main, List<String> candidates) { String chooseVariant(String main, List<String> candidates) {
SplayTreeMap<double, String> mapping = _buildMapping(candidates); SplayTreeMap<double, String> mapping = new SplayTreeMap<double, String>();
// We assume the main asset is designed for a device pixel ratio of 1.0 for (String candidate in candidates)
mapping[1.0] = main; mapping[getScale(candidate)] = candidate;
mapping[_naturalResolution] = main;
return _findNearest(mapping, devicePixelRatio); return _findNearest(mapping, devicePixelRatio);
} }
} }
...@@ -172,7 +194,7 @@ class _AssetVendorState extends State<AssetVendor> { ...@@ -172,7 +194,7 @@ class _AssetVendorState extends State<AssetVendor> {
void initState() { void initState() {
super.initState(); super.initState();
_bundle = new _ResolvingAssetBundle( _bundle = new _ResolutionAwareAssetBundle(
bundle: config.bundle, bundle: config.bundle,
resolver: new _ResolutionAwareAssetResolver( resolver: new _ResolutionAwareAssetResolver(
bundle: config.bundle, bundle: config.bundle,
...@@ -184,7 +206,7 @@ class _AssetVendorState extends State<AssetVendor> { ...@@ -184,7 +206,7 @@ class _AssetVendorState extends State<AssetVendor> {
void didUpdateConfig(AssetVendor oldConfig) { void didUpdateConfig(AssetVendor oldConfig) {
if (config.bundle != oldConfig.bundle || if (config.bundle != oldConfig.bundle ||
config.devicePixelRatio != oldConfig.devicePixelRatio) { config.devicePixelRatio != oldConfig.devicePixelRatio) {
_bundle = new _ResolvingAssetBundle( _bundle = new _ResolutionAwareAssetBundle(
bundle: config.bundle, bundle: config.bundle,
resolver: new _ResolutionAwareAssetResolver( resolver: new _ResolutionAwareAssetResolver(
bundle: config.bundle, bundle: config.bundle,
......
...@@ -1520,6 +1520,7 @@ class RawImage extends LeafRenderObjectWidget { ...@@ -1520,6 +1520,7 @@ class RawImage extends LeafRenderObjectWidget {
this.image, this.image,
this.width, this.width,
this.height, this.height,
this.scale: 1.0,
this.color, this.color,
this.fit, this.fit,
this.alignment, this.alignment,
...@@ -1542,6 +1543,11 @@ class RawImage extends LeafRenderObjectWidget { ...@@ -1542,6 +1543,11 @@ class RawImage extends LeafRenderObjectWidget {
/// aspect ratio. /// aspect ratio.
final double height; final double height;
/// Specifies 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. /// If non-null, apply this color filter to the image before painting.
final Color color; final Color color;
...@@ -1571,6 +1577,7 @@ class RawImage extends LeafRenderObjectWidget { ...@@ -1571,6 +1577,7 @@ class RawImage extends LeafRenderObjectWidget {
image: image, image: image,
width: width, width: width,
height: height, height: height,
scale: scale,
color: color, color: color,
fit: fit, fit: fit,
alignment: alignment, alignment: alignment,
...@@ -1581,6 +1588,7 @@ class RawImage extends LeafRenderObjectWidget { ...@@ -1581,6 +1588,7 @@ class RawImage extends LeafRenderObjectWidget {
renderObject.image = image; renderObject.image = image;
renderObject.width = width; renderObject.width = width;
renderObject.height = height; renderObject.height = height;
renderObject.scale = scale;
renderObject.color = color; renderObject.color = color;
renderObject.alignment = alignment; renderObject.alignment = alignment;
renderObject.fit = fit; renderObject.fit = fit;
...@@ -1662,9 +1670,9 @@ class _ImageListenerState extends State<RawImageResource> { ...@@ -1662,9 +1670,9 @@ class _ImageListenerState extends State<RawImageResource> {
config.image.addListener(_handleImageChanged); config.image.addListener(_handleImageChanged);
} }
ui.Image _resolvedImage; ImageInfo _resolvedImage;
void _handleImageChanged(ui.Image resolvedImage) { void _handleImageChanged(ImageInfo resolvedImage) {
setState(() { setState(() {
_resolvedImage = resolvedImage; _resolvedImage = resolvedImage;
}); });
...@@ -1684,9 +1692,10 @@ class _ImageListenerState extends State<RawImageResource> { ...@@ -1684,9 +1692,10 @@ class _ImageListenerState extends State<RawImageResource> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new RawImage( return new RawImage(
image: _resolvedImage, image: _resolvedImage?.image,
width: config.width, width: config.width,
height: config.height, height: config.height,
scale: _resolvedImage == null ? 1.0 : _resolvedImage.scale,
color: config.color, color: config.color,
fit: config.fit, fit: config.fit,
alignment: config.alignment, alignment: config.alignment,
...@@ -1703,6 +1712,7 @@ class NetworkImage extends StatelessComponent { ...@@ -1703,6 +1712,7 @@ class NetworkImage extends StatelessComponent {
this.src, this.src,
this.width, this.width,
this.height, this.height,
this.scale : 1.0,
this.color, this.color,
this.fit, this.fit,
this.alignment, this.alignment,
...@@ -1725,6 +1735,11 @@ class NetworkImage extends StatelessComponent { ...@@ -1725,6 +1735,11 @@ class NetworkImage extends StatelessComponent {
/// aspect ratio. /// aspect ratio.
final double height; final double height;
/// Specifies 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. /// If non-null, apply this color filter to the image before painting.
final Color color; final Color color;
...@@ -1752,7 +1767,7 @@ class NetworkImage extends StatelessComponent { ...@@ -1752,7 +1767,7 @@ class NetworkImage extends StatelessComponent {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new RawImageResource( return new RawImageResource(
image: imageCache.load(src), image: imageCache.load(src, scale: scale),
width: width, width: width,
height: height, height: height,
color: color, color: color,
......
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