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:
parent
7e2e0deeb0
commit
fd48bc3cff
|
@ -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]) || {};
|
||||
|
||||
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}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -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 @@ 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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 @@
|
|||
return imageSource;
|
||||
}
|
||||
|
||||
RCT_ARRAY_CONVERTER(RCTImageSource)
|
||||
|
||||
@end
|
||||
|
|
Loading…
Reference in New Issue