Skip to content

Commit

Permalink
Added support for width & height for text images
Browse files Browse the repository at this point in the history
Summary:
public

Previously, `<Image>` elements embedded inside `<Text>` ignored all style attributes and props apart from `source`. Now, the `width`, `height` and `resizeMode` styles are observed. I've also added a transparent placeholder to be displayed while the image is loading, to prevent the layout from changing after the image has loaded.

Reviewed By: javache

Differential Revision: D2838659

fb-gh-sync-id: c27f9685b6976705ac2b24075922b2bf247e06ba
  • Loading branch information
nicklockwood authored and facebook-github-bot-4 committed Jan 22, 2016
1 parent f20453b commit 2cbc912
Show file tree
Hide file tree
Showing 9 changed files with 157 additions and 40 deletions.
2 changes: 1 addition & 1 deletion Examples/UIExplorer/TextExample.ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -419,7 +419,7 @@ exports.examples = [
return (
<View>
<Text>
This text contains an inline image <Image source={require('./flux.png')}/>. Neat, huh?
This text contains an inline image <Image source={require('./flux.png')} style={{width: 30, height: 11, resizeMode: 'cover'}}/>. Neat, huh?
</Text>
</View>
);
Expand Down
48 changes: 48 additions & 0 deletions Examples/UIExplorer/UIExplorerUnitTests/RCTImageUtilTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -133,4 +133,52 @@ - (void)testScaling
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
29 changes: 16 additions & 13 deletions Libraries/Image/Image.ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -211,18 +211,21 @@ var Image = React.createClass({
}

if (this.context.isInAParentText) {
return <RCTVirtualImage source={source}/>;
} else {
return (
<RawImage
{...this.props}
style={style}
resizeMode={resizeMode}
tintColor={tintColor}
source={source}
/>
);
RawImage = RCTVirtualImage;
if (!width || !height) {
console.warn('You must specify a width and height for the image %s', uri);
}
}

return (
<RawImage
{...this.props}
style={style}
resizeMode={resizeMode}
tintColor={tintColor}
source={source}
/>
);
},
});

Expand Down
7 changes: 7 additions & 0 deletions Libraries/Image/RCTImageUtils.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
45 changes: 45 additions & 0 deletions Libraries/Image/RCTImageUtils.m
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
2 changes: 2 additions & 0 deletions Libraries/Image/RCTShadowVirtualImage.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#import "RCTShadowView.h"
#import "RCTImageComponent.h"
#import "RCTImageSource.h"
#import "RCTResizeMode.h"

@class RCTBridge;

Expand All @@ -22,5 +23,6 @@
- (instancetype)initWithBridge:(RCTBridge *)bridge;

@property (nonatomic, strong) RCTImageSource *source;
@property (nonatomic, assign) RCTResizeMode resizeMode;

@end
61 changes: 37 additions & 24 deletions Libraries/Image/RCTShadowVirtualImage.m
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -31,36 +33,47 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge

RCT_NOT_IMPLEMENTED(-(instancetype)init)

- (void)setSource:(RCTImageSource *)source
- (void)didSetProps:(NSArray<NSString *> *)changedProps
{
if (![source isEqual:_source]) {
[super didSetProps:changedProps];

// Cancel previous request
if (_cancellationBlock) {
_cancellationBlock();
}
if (changedProps.count == 0) {
// No need to reload image
return;
}

_source = source;
// Cancel previous request
if (_cancellationBlock) {
_cancellationBlock();
}

__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) {
CGSize imageSize = {
RCTZeroIfNaN(self.width),
RCTZeroIfNaN(self.height),
};

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 (!_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
Expand Down
1 change: 1 addition & 0 deletions Libraries/Image/RCTVirtualImageManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,6 @@ - (RCTShadowView *)shadowView
}

RCT_EXPORT_SHADOW_PROPERTY(source, RCTImageSource)
RCT_EXPORT_SHADOW_PROPERTY(resizeMode, UIViewContentMode)

@end
2 changes: 0 additions & 2 deletions Libraries/Text/RCTShadowText.m
Original file line number Diff line number Diff line change
Expand Up @@ -213,8 +213,6 @@ - (NSAttributedString *)_attributedStringWithFontFamily:(NSString *)fontFamily
NSTextAttachment *imageAttachment = [NSTextAttachment new];
imageAttachment.image = image;
[attributedString appendAttributedString:[NSAttributedString attributedStringWithAttachment:imageAttachment]];
} else {
//TODO: add placeholder image?
}
} else {
RCTLogError(@"<Text> can't have any children except <Text>, <Image> or raw strings");
Expand Down

0 comments on commit 2cbc912

Please sign in to comment.