Permalink
Browse files

Implement multi-source Images on iOS

Summary: Mirrors Android's support for multiple sources for Image, allowing us to fetch new images as the size of the view changes.

Reviewed By: mmmulani

Differential Revision: D3615134

fbshipit-source-id: 3d0bf2b75f63a4379e0e49f2dab9aea351b31d5f
  • Loading branch information...
1 parent 7e2e0de commit fd48bc3cff29797f5f2f251ad00c1ba3caf5ea4d David Goldman committed with Facebook Github Bot 2 Jul 28, 2016
@@ -587,7 +587,6 @@ exports.examples = [
render: function() {
return <MultipleSourcesExample />;
},
- platform: 'android',
},
{
title: 'Legacy local image',
@@ -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]) || {};
- 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');
+ 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 (this.props.src) {
console.warn('The <Image> 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}
/>
);
},
@@ -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 `<Image/>` 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 `<Image/>` 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;
@@ -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<RCTImageSource *> *source;
@property (nonatomic, assign) CGFloat blurRadius;
@property (nonatomic, assign) RCTResizeMode resizeMode;
@@ -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 @@ - (void)setRenderingMode:(UIImageRenderingMode)renderingMode
}
}
-- (void)setSource:(RCTImageSource *)source
+- (void)setSource:(NSArray<RCTImageSource *> *)source
{
if (![source isEqual:_source]) {
- _source = source;
+ _source = [source copy];
[self reloadImage];
}
}
@@ -204,11 +205,51 @@ - (void)clearImageIfDetached
}
}
+- (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 @@ - (void)reloadImage
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 @@ - (void)reloadImage
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 @@ - (void)reactSetFrame:(CGRect)frame
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.
@@ -34,7 +34,7 @@ - (UIView *)view
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<RCTImageSource *>)
RCT_CUSTOM_VIEW_PROPERTY(tintColor, UIColor, RCTImageView)
{
// Default tintColor isn't nil - it's inherited from the superView - but we
@@ -46,5 +46,6 @@ __deprecated_msg("Use request.URL instead.");
@interface RCTConvert (ImageSource)
+ (RCTImageSource *)RCTImageSource:(id)json;
++ (NSArray<RCTImageSource *> *)RCTImageSourceArray:(id)json;
@end
@@ -91,4 +91,6 @@ + (RCTImageSource *)RCTImageSource:(id)json
return imageSource;
}
+RCT_ARRAY_CONVERTER(RCTImageSource)
+
@end

1 comment on commit fd48bc3

@xitaoque

Maybe this change results no cache for Image component on iOS. See details on http://stackoverflow.com/questions/39556350/why-isnt-there-any-cache-for-image-on-ios-with-rn-0-33

Please sign in to comment.