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() {
// Resizeable image
image = new RenderImageGrow(null, new Size(100.0, null));
imageCache.load("http://flutter.io/favicon.ico").first.then((ui.Image dartLogo) {
image.image = dartLogo;
imageCache.load("http://flutter.io/favicon.ico").first.then((ImageInfo dartLogo) {
image.image = dartLogo.image;
});
row.add(new RenderPadding(padding: const EdgeDims.all(10.0), child: image));
......
......@@ -668,10 +668,10 @@ class BackgroundImage {
_imageResource.removeListener(_handleImageChanged);
}
void _handleImageChanged(ui.Image resolvedImage) {
void _handleImageChanged(ImageInfo resolvedImage) {
if (resolvedImage == null)
return;
_image = resolvedImage;
_image = resolvedImage.image;
final List<VoidCallback> localListeners =
new List<VoidCallback>.from(_listeners);
for (VoidCallback listener in localListeners)
......
......@@ -22,6 +22,7 @@ class RenderImage extends RenderBox {
ui.Image image,
double width,
double height,
double scale: 1.0,
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,19 @@ class RenderImage extends RenderBox {
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;
// Should we make the transfer mode configurable?
......@@ -161,8 +176,8 @@ class RenderImage extends RenderBox {
if (constraints.isTight || _image == null)
return constraints.smallest;
double width = _image.width.toDouble();
double height = _image.height.toDouble();
double width = _image.width.toDouble() / _scale;
double height = _image.height.toDouble() / _scale;
assert(width > 0.0);
assert(height > 0.0);
double aspectRatio = width / height;
......
......@@ -3,7 +3,6 @@
// found in the LICENSE file.
import 'dart:async';
import 'dart:ui' as ui;
import 'dart:ui_internals' as internals;
import 'dart:typed_data';
......@@ -43,16 +42,18 @@ class NetworkAssetBundle extends AssetBundle {
}
abstract class CachingAssetBundle extends AssetBundle {
Map<String, ImageResource> _imageCache = new Map<String, ImageResource>();
Map<String, Future<String>> _stringCache = new Map<String, Future<String>>();
final Map<String, ImageResource> imageResourceCache =
<String, ImageResource>{};
final Map<String, Future<String>> _stringCache =
<String, Future<String>>{};
Future<ui.Image> _fetchImage(String key) async {
return await decodeImageFromDataPipe(await load(key));
Future<ImageInfo> fetchImage(String key) async {
return new ImageInfo(image: await decodeImageFromDataPipe(await load(key)));
}
ImageResource loadImage(String key) {
return _imageCache.putIfAbsent(key, () {
return new ImageResource(_fetchImage(key));
return imageResourceCache.putIfAbsent(key, () {
return new ImageResource(fetchImage(key));
});
}
......
......@@ -3,7 +3,7 @@
// found in the LICENSE file.
import 'dart:async';
import 'dart:ui' as ui;
import 'dart:ui' show hashValues;
import 'package:mojo/mojo/url_response.mojom.dart';
import 'package:quiver/collection.dart';
......@@ -15,25 +15,32 @@ import 'image_resource.dart';
/// Implements a way to retrieve an image, for example by fetching it from the network.
/// Also used as a key in the image cache.
abstract class ImageProvider {
Future<ui.Image> loadImage();
Future<ImageInfo> loadImage();
}
class _UrlFetcher implements ImageProvider {
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);
if (response.statusCode >= 400) {
print("Failed (${response.statusCode}) to load image $_url");
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;
int get hashCode => _url.hashCode;
bool operator ==(other) {
return other is _UrlFetcher && _url == other._url && _scale == other._scale;
}
int get hashCode => hashValues(_url, _scale);
}
const int _kDefaultSize = 1000;
......@@ -53,8 +60,8 @@ class _ImageCache {
});
}
ImageResource load(String url) {
return loadProvider(new _UrlFetcher(url));
ImageResource load(String url, { double scale: 1.0 }) {
return loadProvider(new _UrlFetcher(url, scale));
}
}
......
......@@ -7,8 +7,14 @@ import 'dart:ui' as ui;
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.
typedef void ImageListener(ui.Image image);
typedef void ImageListener(ImageInfo image);
/// A handle to an image resource
///
......@@ -21,15 +27,15 @@ class ImageResource {
}
bool _resolved = false;
Future<ui.Image> _futureImage;
ui.Image _image;
Future<ImageInfo> _futureImage;
ImageInfo _image;
final List<ImageListener> _listeners = new List<ImageListener>();
/// The first concrete [ui.Image] object represented by this handle.
///
/// Instead of receivingly only the first image, most clients will want to
/// [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]
/// object is available. Note: If a concrete image is available currently,
......@@ -50,7 +56,7 @@ class ImageResource {
_listeners.remove(listener);
}
void _handleImageLoaded(ui.Image image) {
void _handleImageLoaded(ImageInfo image) {
_image = image;
_resolved = true;
_notifyListeners();
......
......@@ -29,12 +29,36 @@ class _ResolvingAssetBundle extends CachingAssetBundle {
final AssetBundle bundle;
final _AssetResolver resolver;
Map<String, String> _keyCache = <String, String>{};
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]);
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
);
}
}
......@@ -58,7 +82,7 @@ abstract class _VariantAssetResolver extends _AssetResolver {
Future<String> 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 +105,15 @@ class _ResolutionAwareAssetResolver extends _VariantAssetResolver {
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;
// 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.
......@@ -112,9 +133,10 @@ class _ResolutionAwareAssetResolver extends _VariantAssetResolver {
}
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;
SplayTreeMap<double, String> mapping = new SplayTreeMap<double, String>();
for (String candidate in candidates)
mapping[getScale(candidate)] = candidate;
mapping[_naturalResolution] = main;
return _findNearest(mapping, devicePixelRatio);
}
}
......@@ -172,7 +194,7 @@ class _AssetVendorState extends State<AssetVendor> {
void initState() {
super.initState();
_bundle = new _ResolvingAssetBundle(
_bundle = new _ResolutionAwareAssetBundle(
bundle: config.bundle,
resolver: new _ResolutionAwareAssetResolver(
bundle: config.bundle,
......@@ -184,7 +206,7 @@ class _AssetVendorState extends State<AssetVendor> {
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,
......
......@@ -1520,6 +1520,7 @@ class RawImage extends LeafRenderObjectWidget {
this.image,
this.width,
this.height,
this.scale: 1.0,
this.color,
this.fit,
this.alignment,
......@@ -1542,6 +1543,11 @@ class RawImage extends LeafRenderObjectWidget {
/// aspect ratio.
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.
final Color color;
......@@ -1571,6 +1577,7 @@ class RawImage extends LeafRenderObjectWidget {
image: image,
width: width,
height: height,
scale: scale,
color: color,
fit: fit,
alignment: alignment,
......@@ -1581,6 +1588,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;
......@@ -1662,9 +1670,9 @@ class _ImageListenerState extends State<RawImageResource> {
config.image.addListener(_handleImageChanged);
}
ui.Image _resolvedImage;
ImageInfo _resolvedImage;
void _handleImageChanged(ui.Image resolvedImage) {
void _handleImageChanged(ImageInfo resolvedImage) {
setState(() {
_resolvedImage = resolvedImage;
});
......@@ -1684,9 +1692,10 @@ class _ImageListenerState extends State<RawImageResource> {
Widget build(BuildContext context) {
return new RawImage(
image: _resolvedImage,
image: _resolvedImage?.image,
width: config.width,
height: config.height,
scale: _resolvedImage == null ? 1.0 : _resolvedImage.scale,
color: config.color,
fit: config.fit,
alignment: config.alignment,
......@@ -1703,6 +1712,7 @@ class NetworkImage extends StatelessComponent {
this.src,
this.width,
this.height,
this.scale : 1.0,
this.color,
this.fit,
this.alignment,
......@@ -1725,6 +1735,11 @@ class NetworkImage extends StatelessComponent {
/// aspect ratio.
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.
final Color color;
......@@ -1752,7 +1767,7 @@ class NetworkImage extends StatelessComponent {
Widget build(BuildContext context) {
return new RawImageResource(
image: imageCache.load(src),
image: imageCache.load(src, scale: scale),
width: width,
height: height,
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