diff --git a/Examples/UIExplorer/js/ImageExample.js b/Examples/UIExplorer/js/ImageExample.js
index df54d10f9ea..d480d22798b 100644
--- a/Examples/UIExplorer/js/ImageExample.js
+++ b/Examples/UIExplorer/js/ImageExample.js
@@ -587,7 +587,6 @@ exports.examples = [
render: function() {
return ;
},
- platform: 'android',
},
{
title: 'Legacy local image',
diff --git a/Libraries/Image/Image.ios.js b/Libraries/Image/Image.ios.js
index 0fc1d20c5c3..71da2e384ca 100644
--- a/Libraries/Image/Image.ios.js
+++ b/Libraries/Image/Image.ios.js
@@ -103,6 +103,11 @@ const Image = React.createClass({
style: StyleSheetPropType(ImageStylePropTypes),
/**
* The image source (either a remote URL or a local file resource).
+ *
+ * This prop can also contain several remote URLs, specified together with
+ * their width and height and potentially with scale/other URI arguments.
+ * The native side will then choose the best `uri` to display based on the
+ * measured size of the image container.
*/
source: ImageSourcePropType,
/**
@@ -268,15 +273,25 @@ const Image = React.createClass({
render: function() {
const source = resolveAssetSource(this.props.source) || { uri: undefined, width: undefined, height: undefined };
- const {width, height, uri} = source;
- const style = flattenStyle([{width, height}, styles.base, this.props.style]) || {};
+
+ let sources;
+ let style;
+ if (Array.isArray(source)) {
+ style = flattenStyle([styles.base, this.props.style]) || {};
+ sources = source;
+ } else {
+ const {width, height, uri} = source;
+ style = flattenStyle([{width, height}, styles.base, this.props.style]) || {};
+ sources = [source];
+
+ if (uri === '') {
+ console.warn('source.uri should not be an empty string');
+ }
+ }
+
const resizeMode = this.props.resizeMode || (style || {}).resizeMode || 'cover'; // Workaround for flow bug t7737108
const tintColor = (style || {}).tintColor; // Workaround for flow bug t7737108
- if (uri === '') {
- console.warn('source.uri should not be an empty string');
- }
-
if (this.props.src) {
console.warn('The component requires a `source` property rather than `src`.');
}
@@ -287,7 +302,7 @@ const Image = React.createClass({
style={style}
resizeMode={resizeMode}
tintColor={tintColor}
- source={source}
+ source={sources}
/>
);
},
diff --git a/Libraries/Image/ImageSourcePropType.js b/Libraries/Image/ImageSourcePropType.js
index 7cd593ff420..3d3af808bdf 100644
--- a/Libraries/Image/ImageSourcePropType.js
+++ b/Libraries/Image/ImageSourcePropType.js
@@ -13,44 +13,48 @@
const PropTypes = require('react/lib/ReactPropTypes');
+const ImageURISourcePropType = PropTypes.shape({
+ /**
+ * `uri` is a string representing the resource identifier for the image, which
+ * could be an http address, a local file path, or the name of a static image
+ * resource (which should be wrapped in the `require('./path/to/image.png')`
+ * function).
+ */
+ uri: PropTypes.string,
+ /**
+ * `method` is the HTTP Method to use. Defaults to GET if not specified.
+ */
+ method: PropTypes.string,
+ /**
+ * `headers` is an object representing the HTTP headers to send along with the
+ * request for a remote image.
+ */
+ headers: PropTypes.objectOf(PropTypes.string),
+ /**
+ * `body` is the HTTP body to send with the request. This must be a valid
+ * UTF-8 string, and will be sent exactly as specified, with no
+ * additional encoding (e.g. URL-escaping or base64) applied.
+ */
+ body: PropTypes.string,
+ /**
+ * `width` and `height` can be specified if known at build time, in which case
+ * these will be used to set the default `` component dimensions.
+ */
+ width: PropTypes.number,
+ height: PropTypes.number,
+ /**
+ * `scale` is used to indicate the scale factor of the image. Defaults to 1.0 if
+ * unspecified, meaning that one image pixel equates to one display point / DIP.
+ */
+ scale: PropTypes.number,
+});
+
const ImageSourcePropType = PropTypes.oneOfType([
- PropTypes.shape({
- /**
- * `uri` is a string representing the resource identifier for the image, which
- * could be an http address, a local file path, or the name of a static image
- * resource (which should be wrapped in the `require('./path/to/image.png')`
- * function).
- */
- uri: PropTypes.string,
- /**
- * `method` is the HTTP Method to use. Defaults to GET if not specified.
- */
- method: PropTypes.string,
- /**
- * `headers` is an object representing the HTTP headers to send along with the
- * request for a remote image.
- */
- headers: PropTypes.objectOf(PropTypes.string),
- /**
- * `body` is the HTTP body to send with the request. This must be a valid
- * UTF-8 string, and will be sent exactly as specified, with no
- * additional encoding (e.g. URL-escaping or base64) applied.
- */
- body: PropTypes.string,
- /**
- * `width` and `height` can be specified if known at build time, in which case
- * these will be used to set the default `` component dimensions.
- */
- width: PropTypes.number,
- height: PropTypes.number,
- /**
- * `scale` is used to indicate the scale factor of the image. Defaults to 1.0 if
- * unspecified, meaning that one image pixel equates to one display point / DIP.
- */
- scale: PropTypes.number,
- }),
+ ImageURISourcePropType,
// Opaque type returned by require('./image.jpg')
PropTypes.number,
+ // Multiple sources
+ PropTypes.arrayOf(ImageURISourcePropType),
]);
module.exports = ImageSourcePropType;
diff --git a/Libraries/Image/RCTImageView.h b/Libraries/Image/RCTImageView.h
index 94060a8448b..d527f513258 100644
--- a/Libraries/Image/RCTImageView.h
+++ b/Libraries/Image/RCTImageView.h
@@ -20,7 +20,7 @@
@property (nonatomic, assign) UIEdgeInsets capInsets;
@property (nonatomic, strong) UIImage *defaultImage;
@property (nonatomic, assign) UIImageRenderingMode renderingMode;
-@property (nonatomic, strong) RCTImageSource *source;
+@property (nonatomic, copy) NSArray *source;
@property (nonatomic, assign) CGFloat blurRadius;
@property (nonatomic, assign) RCTResizeMode resizeMode;
diff --git a/Libraries/Image/RCTImageView.m b/Libraries/Image/RCTImageView.m
index 51c78d9d4de..465c044ec41 100644
--- a/Libraries/Image/RCTImageView.m
+++ b/Libraries/Image/RCTImageView.m
@@ -38,6 +38,7 @@ static BOOL RCTShouldReloadImageForSizeChange(CGSize currentSize, CGSize idealSi
@interface RCTImageView ()
+@property (nonatomic, strong) RCTImageSource *imageSource;
@property (nonatomic, copy) RCTDirectEventBlock onLoadStart;
@property (nonatomic, copy) RCTDirectEventBlock onProgress;
@property (nonatomic, copy) RCTDirectEventBlock onError;
@@ -148,10 +149,10 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init)
}
}
-- (void)setSource:(RCTImageSource *)source
+- (void)setSource:(NSArray *)source
{
if (![source isEqual:_source]) {
- _source = source;
+ _source = [source copy];
[self reloadImage];
}
}
@@ -204,11 +205,51 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init)
}
}
+- (BOOL)hasMultipleSources
+{
+ return _source.count > 1;
+}
+
+- (RCTImageSource *)imageSourceForSize:(CGSize)size
+{
+ if (![self hasMultipleSources]) {
+ return _source.firstObject;
+ }
+ // Need to wait for layout pass before deciding.
+ if (CGSizeEqualToSize(size, CGSizeZero)) {
+ return nil;
+ }
+ const CGFloat scale = RCTScreenScale();
+ const CGFloat targetImagePixels = size.width * size.height * scale * scale;
+
+ RCTImageSource *bestSource = nil;
+ CGFloat bestFit = CGFLOAT_MAX;
+ for (RCTImageSource *source in _source) {
+ CGSize imgSize = source.size;
+ const CGFloat imagePixels =
+ imgSize.width * imgSize.height * source.scale * source.scale;
+ const CGFloat fit = ABS(1 - (imagePixels / targetImagePixels));
+
+ if (fit < bestFit) {
+ bestFit = fit;
+ bestSource = source;
+ }
+ }
+ return bestSource;
+}
+
+- (BOOL)desiredImageSourceDidChange
+{
+ return ![[self imageSourceForSize:self.frame.size] isEqual:_imageSource];
+}
+
- (void)reloadImage
{
[self cancelImageLoad];
- if (_source && self.frame.size.width > 0 && self.frame.size.height > 0) {
+ _imageSource = [self imageSourceForSize:self.frame.size];
+
+ if (_imageSource && self.frame.size.width > 0 && self.frame.size.height > 0) {
if (_onLoadStart) {
_onLoadStart(nil);
}
@@ -228,14 +269,14 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init)
if (!UIEdgeInsetsEqualToEdgeInsets(_capInsets, UIEdgeInsetsZero)) {
// Don't resize images that use capInsets
imageSize = CGSizeZero;
- imageScale = _source.scale;
+ imageScale = _imageSource.scale;
}
- RCTImageSource *source = _source;
+ RCTImageSource *source = _imageSource;
CGFloat blurRadius = _blurRadius;
__weak RCTImageView *weakSelf = self;
_reloadImageCancellationBlock =
- [_bridge.imageLoader loadImageWithURLRequest:_source.request
+ [_bridge.imageLoader loadImageWithURLRequest:source.request
size:imageSize
scale:imageScale
clipped:NO
@@ -245,7 +286,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init)
RCTImageView *strongSelf = weakSelf;
void (^setImageBlock)(UIImage *) = ^(UIImage *image) {
- if (![source isEqual:strongSelf.source]) {
+ if (![source isEqual:strongSelf.imageSource]) {
// Bail out if source has changed since we started loading
return;
}
@@ -304,9 +345,13 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init)
CGSize idealSize = RCTTargetSize(imageSize, self.image.scale, frame.size,
RCTScreenScale(), (RCTResizeMode)self.contentMode, YES);
- if (RCTShouldReloadImageForSizeChange(imageSize, idealSize)) {
+ if ([self desiredImageSourceDidChange]) {
+ // Reload to swap to the proper image source.
+ _targetSize = idealSize;
+ [self reloadImage];
+ } else if (RCTShouldReloadImageForSizeChange(imageSize, idealSize)) {
if (RCTShouldReloadImageForSizeChange(_targetSize, idealSize)) {
- RCTLogInfo(@"[PERF IMAGEVIEW] Reloading image %@ as size %@", _source.request.URL.absoluteString, NSStringFromCGSize(idealSize));
+ RCTLogInfo(@"[PERF IMAGEVIEW] Reloading image %@ as size %@", _imageSource.request.URL.absoluteString, NSStringFromCGSize(idealSize));
// If the existing image or an image being loaded are not the right
// size, reload the asset in case there is a better size available.
diff --git a/Libraries/Image/RCTImageViewManager.m b/Libraries/Image/RCTImageViewManager.m
index cbeadae4525..37210fc07bd 100644
--- a/Libraries/Image/RCTImageViewManager.m
+++ b/Libraries/Image/RCTImageViewManager.m
@@ -34,7 +34,7 @@ RCT_EXPORT_VIEW_PROPERTY(onError, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onLoad, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onLoadEnd, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(resizeMode, RCTResizeMode)
-RCT_EXPORT_VIEW_PROPERTY(source, RCTImageSource)
+RCT_EXPORT_VIEW_PROPERTY(source, NSArray)
RCT_CUSTOM_VIEW_PROPERTY(tintColor, UIColor, RCTImageView)
{
// Default tintColor isn't nil - it's inherited from the superView - but we
diff --git a/React/Base/RCTImageSource.h b/React/Base/RCTImageSource.h
index e5b52c50216..5d749678ffe 100644
--- a/React/Base/RCTImageSource.h
+++ b/React/Base/RCTImageSource.h
@@ -46,5 +46,6 @@ __deprecated_msg("Use request.URL instead.");
@interface RCTConvert (ImageSource)
+ (RCTImageSource *)RCTImageSource:(id)json;
++ (NSArray *)RCTImageSourceArray:(id)json;
@end
diff --git a/React/Base/RCTImageSource.m b/React/Base/RCTImageSource.m
index 173bad517ce..9841467d3b0 100644
--- a/React/Base/RCTImageSource.m
+++ b/React/Base/RCTImageSource.m
@@ -91,4 +91,6 @@
return imageSource;
}
+RCT_ARRAY_CONVERTER(RCTImageSource)
+
@end