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");