Added support for width & height for text images

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
This commit is contained in:
Nick Lockwood 2016-01-22 11:31:40 -08:00 committed by facebook-github-bot-4
parent f20453baee
commit 2cbc912756
9 changed files with 160 additions and 43 deletions

View File

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

View File

@ -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

View File

@ -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 <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}
/>
);
},
});

View File

@ -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

View File

@ -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;
}

View File

@ -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

View File

@ -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<NSString *> *)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

View File

@ -20,5 +20,6 @@ RCT_EXPORT_MODULE()
}
RCT_EXPORT_SHADOW_PROPERTY(source, RCTImageSource)
RCT_EXPORT_SHADOW_PROPERTY(resizeMode, UIViewContentMode)
@end

View File

@ -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(@"<Text> can't have any children except <Text>, <Image> or raw strings");