Skip to content

Commit

Permalink
Implement multi-source Images on iOS
Browse files Browse the repository at this point in the history
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
David Goldman authored and Facebook Github Bot 2 committed Jul 28, 2016
1 parent 7e2e0de commit fd48bc3
Show file tree
Hide file tree
Showing 8 changed files with 120 additions and 54 deletions.
1 change: 0 additions & 1 deletion Examples/UIExplorer/js/ImageExample.js
Expand Up @@ -587,7 +587,6 @@ exports.examples = [
render: function() {
return <MultipleSourcesExample />;
},
platform: 'android',
},
{
title: 'Legacy local image',
Expand Down
29 changes: 22 additions & 7 deletions Libraries/Image/Image.ios.js
Expand Up @@ -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,
/**
Expand Down Expand Up @@ -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`.');
}
Expand All @@ -287,7 +302,7 @@ const Image = React.createClass({
style={style}
resizeMode={resizeMode}
tintColor={tintColor}
source={source}
source={sources}
/>
);
},
Expand Down
74 changes: 39 additions & 35 deletions Libraries/Image/ImageSourcePropType.js
Expand Up @@ -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;
2 changes: 1 addition & 1 deletion Libraries/Image/RCTImageView.h
Expand Up @@ -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;

Expand Down
63 changes: 54 additions & 9 deletions Libraries/Image/RCTImageView.m
Expand Up @@ -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;
Expand Down Expand Up @@ -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];
}
}
Expand Down Expand Up @@ -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);
}
Expand All @@ -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
Expand All @@ -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;
}
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion Libraries/Image/RCTImageViewManager.m
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions React/Base/RCTImageSource.h
Expand Up @@ -46,5 +46,6 @@ __deprecated_msg("Use request.URL instead.");
@interface RCTConvert (ImageSource)

+ (RCTImageSource *)RCTImageSource:(id)json;
+ (NSArray<RCTImageSource *> *)RCTImageSourceArray:(id)json;

@end
2 changes: 2 additions & 0 deletions React/Base/RCTImageSource.m
Expand Up @@ -91,4 +91,6 @@ + (RCTImageSource *)RCTImageSource:(id)json
return imageSource;
}

RCT_ARRAY_CONVERTER(RCTImageSource)

@end

1 comment on commit fd48bc3

@xitaoque
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.