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
This commit is contained in:
David Goldman 2016-07-28 13:58:50 -07:00 committed by Facebook Github Bot 2
parent 7e2e0deeb0
commit fd48bc3cff
8 changed files with 120 additions and 54 deletions

View File

@ -587,7 +587,6 @@ exports.examples = [
render: function() {
return <MultipleSourcesExample />;
},
platform: 'android',
},
{
title: 'Legacy local image',

View File

@ -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 <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}
/>
);
},

View File

@ -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;

View File

@ -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;

View File

@ -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<RCTImageSource *> *)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.

View File

@ -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<RCTImageSource *>)
RCT_CUSTOM_VIEW_PROPERTY(tintColor, UIColor, RCTImageView)
{
// Default tintColor isn't nil - it's inherited from the superView - but we

View File

@ -46,5 +46,6 @@ __deprecated_msg("Use request.URL instead.");
@interface RCTConvert (ImageSource)
+ (RCTImageSource *)RCTImageSource:(id)json;
+ (NSArray<RCTImageSource *> *)RCTImageSourceArray:(id)json;
@end

View File

@ -91,4 +91,6 @@
return imageSource;
}
RCT_ARRAY_CONVERTER(RCTImageSource)
@end