Skip to content

Commit

Permalink
Added RCTImageSource
Browse files Browse the repository at this point in the history
Summary:
public

The +[RCTConvert UIImage:] function, while convenient, is inherently limited by being synchronous, which means that it cannot be used to load remote images, and may not be efficient for local images either. It's also unable to access the bridge, which means that it cannot take advantage of the modular image-loading pipeline.

This diff introduces a new RCTImageSource class which can be used to pass image source objects over the bridge and defer loading until later.

I've also added automatic application of the `resolveAssetSource()` function based on prop type, and fixed up the image logic in NavigatorIOS and TabBarIOS.

Reviewed By: javache

Differential Revision: D2631541

fb-gh-sync-id: 6604635e8bb5394425102487f1ee7cd729321877
  • Loading branch information
nicklockwood authored and facebook-github-bot-4 committed Dec 8, 2015
1 parent dcebe8c commit b672294
Show file tree
Hide file tree
Showing 23 changed files with 434 additions and 276 deletions.
39 changes: 17 additions & 22 deletions Libraries/Components/Navigation/NavigatorIOS.ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,12 @@ var NavigatorIOS = React.createClass({
this.navigationContext = new NavigationContext();
},

getDefaultProps: function(): Object {
return {
translucent: true,
};
},

getInitialState: function(): State {
return {
idStack: [getuid()],
Expand Down Expand Up @@ -591,37 +597,26 @@ var NavigatorIOS = React.createClass({
},

_routeToStackItem: function(route: Route, i: number) {
var Component = route.component;
var shouldUpdateChild = this.state.updatingAllIndicesAtOrBeyond != null &&
var {component, wrapperStyle, passProps, ...route} = route;
var {itemWrapperStyle, ...props} = this.props;
var shouldUpdateChild =
this.state.updatingAllIndicesAtOrBeyond &&
this.state.updatingAllIndicesAtOrBeyond >= i;

var Component = component;
return (
<StaticContainer key={'nav' + i} shouldUpdate={shouldUpdateChild}>
<RCTNavigatorItem
title={route.title}
{...route}
{...props}
style={[
styles.stackItem,
this.props.itemWrapperStyle,
route.wrapperStyle
]}
backButtonIcon={resolveAssetSource(route.backButtonIcon)}
backButtonTitle={route.backButtonTitle}
leftButtonIcon={resolveAssetSource(route.leftButtonIcon)}
leftButtonTitle={route.leftButtonTitle}
onNavLeftButtonTap={route.onLeftButtonPress}
rightButtonIcon={resolveAssetSource(route.rightButtonIcon)}
rightButtonTitle={route.rightButtonTitle}
onNavRightButtonTap={route.onRightButtonPress}
navigationBarHidden={this.props.navigationBarHidden}
shadowHidden={this.props.shadowHidden}
tintColor={this.props.tintColor}
barTintColor={this.props.barTintColor}
translucent={this.props.translucent !== false}
titleTextColor={this.props.titleTextColor}>
itemWrapperStyle,
wrapperStyle
]}>
<Component
navigator={this.navigator}
route={route}
{...route.passProps}
{...passProps}
/>
</RCTNavigatorItem>
</StaticContainer>
Expand Down
28 changes: 9 additions & 19 deletions Libraries/Components/TabBarIOS/TabBarItemIOS.ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ var React = require('React');
var StaticContainer = require('StaticContainer.react');
var StyleSheet = require('StyleSheet');
var View = require('View');
var resolveAssetSource = require('resolveAssetSource');

var requireNativeComponent = require('requireNativeComponent');

Expand Down Expand Up @@ -52,10 +51,7 @@ var TabBarItemIOS = React.createClass({
/**
* A custom icon for the tab. It is ignored when a system icon is defined.
*/
icon: React.PropTypes.oneOfType([
React.PropTypes.string,
Image.propTypes.source,
]),
icon: Image.propTypes.source,
/**
* A custom icon when the tab is selected. It is ignored when a system
* icon is defined. If left empty, the icon will be tinted in blue.
Expand Down Expand Up @@ -101,29 +97,23 @@ var TabBarItemIOS = React.createClass({
},

render: function() {
var tabContents = null;
var {style, children, ...props} = this.props;

// if the tab has already been shown once, always continue to show it so we
// preserve state between tab transitions
if (this.state.hasBeenSelected) {
tabContents =
var tabContents =
<StaticContainer shouldUpdate={this.props.selected}>
{this.props.children}
{children}
</StaticContainer>;
} else {
tabContents = <View />;
var tabContents = <View />;
}

var badge = typeof this.props.badge === 'number' ?
'' + this.props.badge :
this.props.badge;


return (
<RCTTabBarItem
{...this.props}
icon={this.props.systemIcon || resolveAssetSource(this.props.icon)}
selectedIcon={resolveAssetSource(this.props.selectedIcon)}
badge={badge}
style={[styles.tab, this.props.style]}>
{...props}
style={[styles.tab, style]}>
{tabContents}
</RCTTabBarItem>
);
Expand Down
41 changes: 11 additions & 30 deletions Libraries/Image/Image.ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,16 @@ var Image = React.createClass({
PropTypes.number,
]),
/**
* A static image to display while downloading the final image off the
* network.
* A static image to display while loading the image source.
* @platform ios
*/
defaultSource: PropTypes.shape({
uri: PropTypes.string,
}),
defaultSource: PropTypes.oneOfType([
PropTypes.shape({
uri: PropTypes.string,
}),
// Opaque type returned by require('./image.jpg')
PropTypes.number,
]),
/**
* When true, indicates the image is an accessibility element.
* @platform ios
Expand Down Expand Up @@ -155,23 +158,10 @@ var Image = React.createClass({
},

render: function() {
for (var prop in cfg.nativeOnly) {
if (this.props[prop] !== undefined) {
console.warn('Prop `' + prop + ' = ' + this.props[prop] + '` should ' +
'not be set directly on Image.');
}
}
var source = resolveAssetSource(this.props.source) || {};
var defaultSource = (this.props.defaultSource && resolveAssetSource(this.props.defaultSource)) || {};

var {width, height} = source;
var style = flattenStyle([{width, height}, styles.base, this.props.style]) || {};

if (source.uri === '') {
console.warn('source.uri should not be an empty string');
return <View {...this.props} style={style} />;
}

var isNetwork = source.uri && source.uri.match(/^https?:/);
var RawImage = isNetwork ? RCTNetworkImageView : RCTImageView;
var resizeMode = this.props.resizeMode || (style || {}).resizeMode || 'cover'; // Workaround for flow bug t7737108
Expand All @@ -192,8 +182,7 @@ var Image = React.createClass({
style={style}
resizeMode={resizeMode}
tintColor={tintColor}
src={source.uri}
defaultImageSrc={defaultSource.uri}
source={source}
/>
);
}
Expand All @@ -206,16 +195,8 @@ var styles = StyleSheet.create({
},
});

var cfg = {
nativeOnly: {
src: true,
defaultImageSrc: true,
imageTag: true,
progressHandlerRegistered: true,
},
};
var RCTImageView = requireNativeComponent('RCTImageView', Image, cfg);
var RCTNetworkImageView = NativeModules.NetworkImageViewManager ? requireNativeComponent('RCTNetworkImageView', Image, cfg) : RCTImageView;
var RCTImageView = requireNativeComponent('RCTImageView', Image);
var RCTNetworkImageView = NativeModules.NetworkImageViewManager ? requireNativeComponent('RCTNetworkImageView', Image) : RCTImageView;
var RCTVirtualImage = requireNativeComponent('RCTVirtualImage', Image);

module.exports = Image;
11 changes: 6 additions & 5 deletions Libraries/Image/RCTImageUtils.m
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,11 @@ CGSize RCTSizeInPixels(CGSize pointSize, CGFloat scale)

if (CGSizeEqualToSize(destSize, CGSizeZero)) {
destSize = sourceSize;
if (!destScale) {
destScale = 1;
}
} else if (!destScale) {
destScale = RCTScreenScale();
}

// calculate target size
Expand All @@ -253,13 +258,9 @@ CGSize RCTSizeInPixels(CGSize pointSize, CGFloat scale)
return nil;
}

// adjust scale
size_t actualWidth = CGImageGetWidth(imageRef);
CGFloat scale = actualWidth / targetSize.width * destScale;

// return image
UIImage *image = [UIImage imageWithCGImage:imageRef
scale:scale
scale:destScale
orientation:UIImageOrientationUp];
CGImageRelease(imageRef);
return image;
Expand Down
3 changes: 2 additions & 1 deletion Libraries/Image/RCTImageView.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#import "RCTImageComponent.h"

@class RCTBridge;
@class RCTImageSource;

@interface RCTImageView : UIImageView <RCTImageComponent>

Expand All @@ -19,6 +20,6 @@
@property (nonatomic, assign) UIEdgeInsets capInsets;
@property (nonatomic, strong) UIImage *defaultImage;
@property (nonatomic, assign) UIImageRenderingMode renderingMode;
@property (nonatomic, copy) NSString *src;
@property (nonatomic, strong) RCTImageSource *source;

@end
72 changes: 43 additions & 29 deletions Libraries/Image/RCTImageView.m
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#import "RCTConvert.h"
#import "RCTEventDispatcher.h"
#import "RCTImageLoader.h"
#import "RCTImageSource.h"
#import "RCTImageUtils.h"
#import "RCTUtils.h"

Expand Down Expand Up @@ -107,6 +108,12 @@ - (void)setCapInsets:(UIEdgeInsets)capInsets
}
}

- (void)setTintColor:(UIColor *)tintColor
{
super.tintColor = tintColor;
self.renderingMode = tintColor ? UIImageRenderingModeAlwaysTemplate : UIImageRenderingModeAlwaysOriginal;
}

- (void)setRenderingMode:(UIImageRenderingMode)renderingMode
{
if (_renderingMode != renderingMode) {
Expand All @@ -115,28 +122,29 @@ - (void)setRenderingMode:(UIImageRenderingMode)renderingMode
}
}

- (void)setSrc:(NSString *)src
- (void)setSource:(RCTImageSource *)source
{
if (![src isEqual:_src]) {
_src = [src copy];
if (![source isEqual:_source]) {
_source = source;
[self reloadImage];
}
}

+ (BOOL)srcNeedsReload:(NSString *)src
- (BOOL)sourceNeedsReload
{
NSString *scheme = _source.imageURL.scheme;
return
[src hasPrefix:@"http://"] ||
[src hasPrefix:@"https://"] ||
[src hasPrefix:@"assets-library://"] ||
[src hasPrefix:@"ph://"];
[scheme isEqualToString:@"http"] ||
[scheme isEqualToString:@"https"] ||
[scheme isEqualToString:@"assets-library"] ||
[scheme isEqualToString:@"ph"];
}

- (void)setContentMode:(UIViewContentMode)contentMode
{
if (self.contentMode != contentMode) {
super.contentMode = contentMode;
if ([RCTImageView srcNeedsReload:_src]) {
if ([self sourceNeedsReload]) {
[self reloadImage];
}
}
Expand All @@ -162,7 +170,7 @@ - (void)reloadImage
{
[self cancelImageLoad];

if (_src && self.frame.size.width > 0 && self.frame.size.height > 0) {
if (_source && self.frame.size.width > 0 && self.frame.size.height > 0) {
if (_onLoadStart) {
_onLoadStart(nil);
}
Expand All @@ -177,31 +185,37 @@ - (void)reloadImage
};
}

_reloadImageCancellationBlock = [_bridge.imageLoader loadImageWithTag:_src
RCTImageSource *source = _source;
__weak RCTImageView *weakSelf = self;
_reloadImageCancellationBlock = [_bridge.imageLoader loadImageWithTag:_source.imageURL.absoluteString
size:self.bounds.size
scale:RCTScreenScale()
resizeMode:self.contentMode
progressBlock:progressHandler
completionBlock:^(NSError *error, UIImage *image) {
if (error) {
if (_onError) {
_onError(@{ @"error": error.localizedDescription });
}
} else {
if (_onLoad) {
_onLoad(nil);
}
}
if (_onLoadEnd) {
_onLoadEnd(nil);
}

dispatch_async(dispatch_get_main_queue(), ^{
RCTImageView *strongSelf = weakSelf;
if (![source isEqual:strongSelf.source]) {
// Bail out if source has changed since we started loading
return;
}
if (image.reactKeyframeAnimation) {
[self.layer addAnimation:image.reactKeyframeAnimation forKey:@"contents"];
[strongSelf.layer addAnimation:image.reactKeyframeAnimation forKey:@"contents"];
} else {
[self.layer removeAnimationForKey:@"contents"];
self.image = image;
[strongSelf.layer removeAnimationForKey:@"contents"];
strongSelf.image = image;
}
if (error) {
if (strongSelf->_onError) {
strongSelf->_onError(@{ @"error": error.localizedDescription });
}
} else {
if (strongSelf->_onLoad) {
strongSelf->_onLoad(nil);
}
}
if (strongSelf->_onLoadEnd) {
strongSelf->_onLoadEnd(nil);
}
});
}];
Expand All @@ -217,13 +231,13 @@ - (void)reactSetFrame:(CGRect)frame
if (!self.image || self.image == _defaultImage) {
_targetSize = frame.size;
[self reloadImage];
} else if ([RCTImageView srcNeedsReload:_src]) {
} else if ([self sourceNeedsReload]) {
CGSize imageSize = self.image.size;
CGSize idealSize = RCTTargetSize(imageSize, self.image.scale, frame.size, RCTScreenScale(), self.contentMode, YES);

if (RCTShouldReloadImageForSizeChange(imageSize, idealSize)) {
if (RCTShouldReloadImageForSizeChange(_targetSize, idealSize)) {
RCTLogInfo(@"[PERF IMAGEVIEW] Reloading image %@ as size %@", _src, NSStringFromCGSize(idealSize));
RCTLogInfo(@"[PERF IMAGEVIEW] Reloading image %@ as size %@", _source.imageURL, NSStringFromCGSize(idealSize));

// 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.
Expand Down
Loading

0 comments on commit b672294

Please sign in to comment.