diff --git a/Examples/UIExplorer/TextExample.ios.js b/Examples/UIExplorer/TextExample.ios.js index fbf434cde..3de37c423 100644 --- a/Examples/UIExplorer/TextExample.ios.js +++ b/Examples/UIExplorer/TextExample.ios.js @@ -419,7 +419,7 @@ exports.examples = [ return ( - This text contains an inline image . Neat, huh? + This text contains an inline image . Neat, huh? ); diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTImageUtilTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTImageUtilTests.m index 19229d045..810e079ab 100644 --- a/Examples/UIExplorer/UIExplorerUnitTests/RCTImageUtilTests.m +++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTImageUtilTests.m @@ -133,4 +133,52 @@ RCTAssertEqualSizes(a.size, b.size); \ RCTAssertEqualRects(expected, result); } +- (void)testPlaceholderImage +{ + CGSize size = {45, 22}; + CGFloat expectedScale = 1.0; + UIImage *image = RCTGetPlaceholderImage(size, nil); + RCTAssertEqualSizes(size, image.size); + XCTAssertEqual(expectedScale, image.scale); +} + +- (void)testPlaceholderNonintegralSize +{ + CGSize size = {3.0/2, 7.0/3}; + CGFloat expectedScale = 6; + CGSize pixelSize = { + round(size.width * expectedScale), + round(size.height * expectedScale) + }; + UIImage *image = RCTGetPlaceholderImage(size, nil); + RCTAssertEqualSizes(size, image.size); + XCTAssertEqual(pixelSize.width, CGImageGetWidth(image.CGImage)); + XCTAssertEqual(pixelSize.height, CGImageGetHeight(image.CGImage)); + XCTAssertEqual(expectedScale, image.scale); +} + +- (void)testPlaceholderSquareImage +{ + CGSize size = {333, 333}; + CGFloat expectedScale = 1.0/333; + CGSize pixelSize = {1, 1}; + UIImage *image = RCTGetPlaceholderImage(size, nil); + RCTAssertEqualSizes(size, image.size); + XCTAssertEqual(pixelSize.width, CGImageGetWidth(image.CGImage)); + XCTAssertEqual(pixelSize.height, CGImageGetHeight(image.CGImage)); + XCTAssertEqual(expectedScale, image.scale); +} + +- (void)testPlaceholderNonsquareImage +{ + CGSize size = {640, 480}; + CGFloat expectedScale = 1.0/160; + CGSize pixelSize = {4, 3}; + UIImage *image = RCTGetPlaceholderImage(size, nil); + RCTAssertEqualSizes(size, image.size); + XCTAssertEqual(pixelSize.width, CGImageGetWidth(image.CGImage)); + XCTAssertEqual(pixelSize.height, CGImageGetHeight(image.CGImage)); + XCTAssertEqual(expectedScale, image.scale); +} + @end diff --git a/Libraries/Image/Image.ios.js b/Libraries/Image/Image.ios.js index 970ef172c..4830a6dec 100644 --- a/Libraries/Image/Image.ios.js +++ b/Libraries/Image/Image.ios.js @@ -196,10 +196,10 @@ var Image = React.createClass({ render: function() { var source = resolveAssetSource(this.props.source) || {}; - var {width, height} = source; + var {width, height, uri} = source; var style = flattenStyle([{width, height}, styles.base, this.props.style]) || {}; - var isNetwork = source.uri && source.uri.match(/^https?:/); + var isNetwork = uri && uri.match(/^https?:/); var RawImage = isNetwork ? RCTNetworkImageView : RCTImageView; var resizeMode = this.props.resizeMode || (style || {}).resizeMode || 'cover'; // Workaround for flow bug t7737108 var tintColor = (style || {}).tintColor; // Workaround for flow bug t7737108 @@ -211,18 +211,21 @@ var Image = React.createClass({ } if (this.context.isInAParentText) { - return ; - } else { - return ( - - ); + RawImage = RCTVirtualImage; + if (!width || !height) { + console.warn('You must specify a width and height for the image %s', uri); + } } + + return ( + + ); }, }); diff --git a/Libraries/Image/RCTImageUtils.h b/Libraries/Image/RCTImageUtils.h index f870bda67..0c855790e 100644 --- a/Libraries/Image/RCTImageUtils.h +++ b/Libraries/Image/RCTImageUtils.h @@ -93,4 +93,11 @@ RCT_EXTERN UIImage *__nullable RCTTransformImage(UIImage *image, */ RCT_EXTERN BOOL RCTImageHasAlpha(CGImageRef image); +/** + * Create a solid placeholder image of the specified size and color to display + * while loading an image. If color is not specified, image will be transparent. + */ +RCT_EXTERN UIImage *__nullable RCTGetPlaceholderImage(CGSize size, + UIColor *__nullable color); + NS_ASSUME_NONNULL_END diff --git a/Libraries/Image/RCTImageUtils.m b/Libraries/Image/RCTImageUtils.m index d484ea238..a89872508 100644 --- a/Libraries/Image/RCTImageUtils.m +++ b/Libraries/Image/RCTImageUtils.m @@ -16,6 +16,8 @@ #import "RCTLog.h" #import "RCTUtils.h" +static const CGFloat RCTThresholdValue = 0.0001; + static CGFloat RCTCeilValue(CGFloat value, CGFloat scale) { return ceil(value * scale) / scale; @@ -334,3 +336,46 @@ BOOL RCTImageHasAlpha(CGImageRef image) return YES; } } + +UIImage *__nullable RCTGetPlaceholderImage(CGSize size, + UIColor *__nullable color) +{ + if (size.width <= 0 || size.height <= 0) { + return nil; + } + + // If dimensions are nonintegral, increase scale + CGFloat scale = 1; + if (size.width - floor(size.width) > RCTThresholdValue) { + scale *= round(1.0 / (size.width - floor(size.width))); + } + if (size.height - floor(size.height) > RCTThresholdValue) { + scale *= round(1.0 / (size.height - floor(size.height))); + } + + // Use Euclid's algorithm to find the greatest common divisor + // between the specified placeholder width and height; + NSInteger a = size.width * scale; + NSInteger b = size.height * scale; + while (a != 0) { + NSInteger c = a; + a = b % a; + b = c; + } + + // Divide the placeholder image scale by the GCD we found above. This allows + // us to save memory by creating the smallest possible placeholder image + // with the correct aspect ratio, then scaling it up at display time. + scale /= b; + + // Fill image with specified color + CGFloat alpha = CGColorGetAlpha(color.CGColor); + UIGraphicsBeginImageContextWithOptions(size, ABS(1.0 - alpha) < RCTThresholdValue, scale); + if (alpha > 0) { + [color setFill]; + UIRectFill((CGRect){CGPointZero, size}); + } + UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return image; +} diff --git a/Libraries/Image/RCTShadowVirtualImage.h b/Libraries/Image/RCTShadowVirtualImage.h index bd8bb51ae..b9623f736 100644 --- a/Libraries/Image/RCTShadowVirtualImage.h +++ b/Libraries/Image/RCTShadowVirtualImage.h @@ -10,6 +10,7 @@ #import "RCTShadowView.h" #import "RCTImageComponent.h" #import "RCTImageSource.h" +#import "RCTResizeMode.h" @class RCTBridge; @@ -22,5 +23,6 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge; @property (nonatomic, strong) RCTImageSource *source; +@property (nonatomic, assign) RCTResizeMode resizeMode; @end diff --git a/Libraries/Image/RCTShadowVirtualImage.m b/Libraries/Image/RCTShadowVirtualImage.m index d6b0153d1..f950e6377 100644 --- a/Libraries/Image/RCTShadowVirtualImage.m +++ b/Libraries/Image/RCTShadowVirtualImage.m @@ -9,9 +9,11 @@ #import "RCTShadowVirtualImage.h" #import "RCTImageLoader.h" +#import "RCTImageUtils.h" #import "RCTBridge.h" #import "RCTConvert.h" #import "RCTUIManager.h" +#import "RCTUtils.h" @implementation RCTShadowVirtualImage { @@ -31,36 +33,47 @@ RCT_NOT_IMPLEMENTED(-(instancetype)init) -- (void)setSource:(RCTImageSource *)source +- (void)didSetProps:(NSArray *)changedProps { - if (![source isEqual:_source]) { + [super didSetProps:changedProps]; - // Cancel previous request - if (_cancellationBlock) { - _cancellationBlock(); - } - - _source = source; - - __weak RCTShadowVirtualImage *weakSelf = self; - _cancellationBlock = [_bridge.imageLoader loadImageWithTag:source.imageURL.absoluteString - size:source.size - scale:source.scale - resizeMode:RCTResizeModeStretch - progressBlock:nil - completionBlock:^(NSError *error, UIImage *image) { - - dispatch_async(_bridge.uiManager.methodQueue, ^{ - RCTShadowVirtualImage *strongSelf = weakSelf; - if (![source isEqual:strongSelf.source]) { - // Bail out if source has changed since we started loading - return; - } - strongSelf->_image = image; - [strongSelf dirtyText]; - }); - }]; + if (changedProps.count == 0) { + // No need to reload image + return; } + + // Cancel previous request + if (_cancellationBlock) { + _cancellationBlock(); + } + + CGSize imageSize = { + RCTZeroIfNaN(self.width), + RCTZeroIfNaN(self.height), + }; + + if (!_image) { + _image = RCTGetPlaceholderImage(imageSize, nil); + } + + __weak RCTShadowVirtualImage *weakSelf = self; + _cancellationBlock = [_bridge.imageLoader loadImageWithTag:_source.imageURL.absoluteString + size:imageSize + scale:RCTScreenScale() + resizeMode:_resizeMode + progressBlock:nil + completionBlock:^(NSError *error, UIImage *image) { + + dispatch_async(_bridge.uiManager.methodQueue, ^{ + RCTShadowVirtualImage *strongSelf = weakSelf; + if (![_source isEqual:strongSelf.source]) { + // Bail out if source has changed since we started loading + return; + } + strongSelf->_image = image; + [strongSelf dirtyText]; + }); + }]; } - (void)dealloc diff --git a/Libraries/Image/RCTVirtualImageManager.m b/Libraries/Image/RCTVirtualImageManager.m index d497026ab..6311010f4 100644 --- a/Libraries/Image/RCTVirtualImageManager.m +++ b/Libraries/Image/RCTVirtualImageManager.m @@ -20,5 +20,6 @@ RCT_EXPORT_MODULE() } RCT_EXPORT_SHADOW_PROPERTY(source, RCTImageSource) +RCT_EXPORT_SHADOW_PROPERTY(resizeMode, UIViewContentMode) @end diff --git a/Libraries/Text/RCTShadowText.m b/Libraries/Text/RCTShadowText.m index eebbc9c32..a3286a88b 100644 --- a/Libraries/Text/RCTShadowText.m +++ b/Libraries/Text/RCTShadowText.m @@ -213,8 +213,6 @@ static css_dim_t RCTMeasure(void *context, float width, float height) NSTextAttachment *imageAttachment = [NSTextAttachment new]; imageAttachment.image = image; [attributedString appendAttributedString:[NSAttributedString attributedStringWithAttachment:imageAttachment]]; - } else { - //TODO: add placeholder image? } } else { RCTLogError(@" can't have any children except , or raw strings");