mirror of
https://github.com/status-im/react-native.git
synced 2025-01-16 04:24:15 +00:00
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() {
|
render: function() {
|
||||||
return <MultipleSourcesExample />;
|
return <MultipleSourcesExample />;
|
||||||
},
|
},
|
||||||
platform: 'android',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Legacy local image',
|
title: 'Legacy local image',
|
||||||
|
@ -103,6 +103,11 @@ const Image = React.createClass({
|
|||||||
style: StyleSheetPropType(ImageStylePropTypes),
|
style: StyleSheetPropType(ImageStylePropTypes),
|
||||||
/**
|
/**
|
||||||
* The image source (either a remote URL or a local file resource).
|
* 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,
|
source: ImageSourcePropType,
|
||||||
/**
|
/**
|
||||||
@ -268,15 +273,25 @@ const Image = React.createClass({
|
|||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
const source = resolveAssetSource(this.props.source) || { uri: undefined, width: undefined, height: undefined };
|
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 resizeMode = this.props.resizeMode || (style || {}).resizeMode || 'cover'; // Workaround for flow bug t7737108
|
||||||
const tintColor = (style || {}).tintColor; // 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) {
|
if (this.props.src) {
|
||||||
console.warn('The <Image> component requires a `source` property rather than `src`.');
|
console.warn('The <Image> component requires a `source` property rather than `src`.');
|
||||||
}
|
}
|
||||||
@ -287,7 +302,7 @@ const Image = React.createClass({
|
|||||||
style={style}
|
style={style}
|
||||||
resizeMode={resizeMode}
|
resizeMode={resizeMode}
|
||||||
tintColor={tintColor}
|
tintColor={tintColor}
|
||||||
source={source}
|
source={sources}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -13,44 +13,48 @@
|
|||||||
|
|
||||||
const PropTypes = require('react/lib/ReactPropTypes');
|
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([
|
const ImageSourcePropType = PropTypes.oneOfType([
|
||||||
PropTypes.shape({
|
ImageURISourcePropType,
|
||||||
/**
|
|
||||||
* `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,
|
|
||||||
}),
|
|
||||||
// Opaque type returned by require('./image.jpg')
|
// Opaque type returned by require('./image.jpg')
|
||||||
PropTypes.number,
|
PropTypes.number,
|
||||||
|
// Multiple sources
|
||||||
|
PropTypes.arrayOf(ImageURISourcePropType),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
module.exports = ImageSourcePropType;
|
module.exports = ImageSourcePropType;
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
@property (nonatomic, assign) UIEdgeInsets capInsets;
|
@property (nonatomic, assign) UIEdgeInsets capInsets;
|
||||||
@property (nonatomic, strong) UIImage *defaultImage;
|
@property (nonatomic, strong) UIImage *defaultImage;
|
||||||
@property (nonatomic, assign) UIImageRenderingMode renderingMode;
|
@property (nonatomic, assign) UIImageRenderingMode renderingMode;
|
||||||
@property (nonatomic, strong) RCTImageSource *source;
|
@property (nonatomic, copy) NSArray<RCTImageSource *> *source;
|
||||||
@property (nonatomic, assign) CGFloat blurRadius;
|
@property (nonatomic, assign) CGFloat blurRadius;
|
||||||
@property (nonatomic, assign) RCTResizeMode resizeMode;
|
@property (nonatomic, assign) RCTResizeMode resizeMode;
|
||||||
|
|
||||||
|
@ -38,6 +38,7 @@ static BOOL RCTShouldReloadImageForSizeChange(CGSize currentSize, CGSize idealSi
|
|||||||
|
|
||||||
@interface RCTImageView ()
|
@interface RCTImageView ()
|
||||||
|
|
||||||
|
@property (nonatomic, strong) RCTImageSource *imageSource;
|
||||||
@property (nonatomic, copy) RCTDirectEventBlock onLoadStart;
|
@property (nonatomic, copy) RCTDirectEventBlock onLoadStart;
|
||||||
@property (nonatomic, copy) RCTDirectEventBlock onProgress;
|
@property (nonatomic, copy) RCTDirectEventBlock onProgress;
|
||||||
@property (nonatomic, copy) RCTDirectEventBlock onError;
|
@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]) {
|
if (![source isEqual:_source]) {
|
||||||
_source = source;
|
_source = [source copy];
|
||||||
[self reloadImage];
|
[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
|
- (void)reloadImage
|
||||||
{
|
{
|
||||||
[self cancelImageLoad];
|
[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) {
|
if (_onLoadStart) {
|
||||||
_onLoadStart(nil);
|
_onLoadStart(nil);
|
||||||
}
|
}
|
||||||
@ -228,14 +269,14 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init)
|
|||||||
if (!UIEdgeInsetsEqualToEdgeInsets(_capInsets, UIEdgeInsetsZero)) {
|
if (!UIEdgeInsetsEqualToEdgeInsets(_capInsets, UIEdgeInsetsZero)) {
|
||||||
// Don't resize images that use capInsets
|
// Don't resize images that use capInsets
|
||||||
imageSize = CGSizeZero;
|
imageSize = CGSizeZero;
|
||||||
imageScale = _source.scale;
|
imageScale = _imageSource.scale;
|
||||||
}
|
}
|
||||||
|
|
||||||
RCTImageSource *source = _source;
|
RCTImageSource *source = _imageSource;
|
||||||
CGFloat blurRadius = _blurRadius;
|
CGFloat blurRadius = _blurRadius;
|
||||||
__weak RCTImageView *weakSelf = self;
|
__weak RCTImageView *weakSelf = self;
|
||||||
_reloadImageCancellationBlock =
|
_reloadImageCancellationBlock =
|
||||||
[_bridge.imageLoader loadImageWithURLRequest:_source.request
|
[_bridge.imageLoader loadImageWithURLRequest:source.request
|
||||||
size:imageSize
|
size:imageSize
|
||||||
scale:imageScale
|
scale:imageScale
|
||||||
clipped:NO
|
clipped:NO
|
||||||
@ -245,7 +286,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init)
|
|||||||
|
|
||||||
RCTImageView *strongSelf = weakSelf;
|
RCTImageView *strongSelf = weakSelf;
|
||||||
void (^setImageBlock)(UIImage *) = ^(UIImage *image) {
|
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
|
// Bail out if source has changed since we started loading
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -304,9 +345,13 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init)
|
|||||||
CGSize idealSize = RCTTargetSize(imageSize, self.image.scale, frame.size,
|
CGSize idealSize = RCTTargetSize(imageSize, self.image.scale, frame.size,
|
||||||
RCTScreenScale(), (RCTResizeMode)self.contentMode, YES);
|
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)) {
|
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
|
// 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.
|
// 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(onLoad, RCTDirectEventBlock)
|
||||||
RCT_EXPORT_VIEW_PROPERTY(onLoadEnd, RCTDirectEventBlock)
|
RCT_EXPORT_VIEW_PROPERTY(onLoadEnd, RCTDirectEventBlock)
|
||||||
RCT_EXPORT_VIEW_PROPERTY(resizeMode, RCTResizeMode)
|
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)
|
RCT_CUSTOM_VIEW_PROPERTY(tintColor, UIColor, RCTImageView)
|
||||||
{
|
{
|
||||||
// Default tintColor isn't nil - it's inherited from the superView - but we
|
// 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)
|
@interface RCTConvert (ImageSource)
|
||||||
|
|
||||||
+ (RCTImageSource *)RCTImageSource:(id)json;
|
+ (RCTImageSource *)RCTImageSource:(id)json;
|
||||||
|
+ (NSArray<RCTImageSource *> *)RCTImageSourceArray:(id)json;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
@ -91,4 +91,6 @@
|
|||||||
return imageSource;
|
return imageSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RCT_ARRAY_CONVERTER(RCTImageSource)
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
Loading…
x
Reference in New Issue
Block a user