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