Skip to content

Commit

Permalink
Automatically scale iOS assets based on the target dimensions specifi…
Browse files Browse the repository at this point in the history
…ed in the style property of the <Image... /> tag.

Currently, documentation under "Image > Best Camera Roll Image" is
misleading:

“iOS saves multiple sizes for the same image in your Camera Roll, it is
very important to pick the one that's as close as possible for
performance reasons. You wouldn't want to use the full quality
3264x2448 image as source when displaying a 200x200 thumbnail. If
there's an exact match, React Native will pick it, otherwise it's going
to use the first one that's at least 50% bigger in order to avoid blur
when resizing from a close size. All of this is done by default so you
don't have to worry about writing the tedious (and error prone) code to
do it yourself.”

https://facebook.github.io/react-native/docs/image.html

This does not occur. Instead, React Native loads the full resolution
image no matter the desired size. This means an 8MP photo will require
32MB of memory to display even if displayed in a 100x100 thumbnail.
Loading a series of large images will spike memory and likely crash
your app.

This commit resolves the discrepancy and brings the codebase inline
with current documentation.

Example: Consider a 3264x2448 image loaded on an iPhone 6 with the
following tag:

   <Image src={{ uri: 'assets-library://... }} style={{ width: 320,
height: 240 }}/>

The image will automatically be scaled to 640x320 (320x240 target
dimensions * a Retina scale of 2.0). This uses considerably less memory
than rendering the full resolution asset. It also happens automatically.

To force the loading of full resolution, the assetUseMaximumSize option
can be passed in:

	<Image src={{ uri: 'assets-library://...', options: {
assetUseMaximumSize: true } }} style={{ width: 320, height: 240 }}/>

Additionally, RCTImageLoader has been updated to handle automatic
scaling for images from both the Assets Library and Photos Framework.

Added an example to UIExplorer. Tap an image in the <CameraRollView>.

Issues touched:
	facebook#1567
	facebook#1845
	facebook#1579
	facebook#930

Note:

Pull Request facebook#1704 (facebook#1704)
takes an alternate approach by allowing an assetThumbnail prop
(boolean) to be passed in to the Image tag's source hash. IE:

<Image src={{ uri: ..., assetThumbnail: true }} />

If true, the asset's thumbnail representation will be used. The
resolution of this thumbnail is non-configurable and does not scale
well.
  • Loading branch information
aroth committed Jul 13, 2015
1 parent 60b5645 commit 967c105
Show file tree
Hide file tree
Showing 9 changed files with 315 additions and 20 deletions.
132 changes: 132 additions & 0 deletions Examples/UIExplorer/AssetThumbnailExample.ios.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/**
* The examples provided by Facebook are for non-commercial testing and
* evaluation purposes only.
*
* Facebook reserves all rights not expressly granted.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
* FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
* @flow
*/
'use strict';

var React = require('react-native');
var {
Image,
StyleSheet,
Text,
View,
ScrollView
} = React;

var AssetThumbnailExampleView = React.createClass({

getInitialState() {
return {
asset: this.props.asset
};
},

render() {
var asset = this.state.asset;
return (
<ScrollView>
<View style={ styles.row }>
<Image
source={{ uri: asset.node.image.uri }}
style={ styles.image1 }
/>
</View>

<View style={ styles.row }>
<Image
source={{ uri: asset.node.image.uri }}
style={ styles.image2 }
/>
</View>

<View style={ styles.row }>
<Image
source={{ uri: asset.node.image.uri }}
style={ styles.image3 }
/>
</View>



</ScrollView>
);
},


});

var styles = StyleSheet.create({
row: {
padding: 10,
flex: 1,
flexDirection: 'row',
},

details: {
margin: 5
},

textColumn: {
flex: 1,
flexDirection: 'column'
},

image1: {
borderWidth: 1,
borderColor: 'black',
width: 240,
height: 320,
margin: 5,
},

image2: {
borderWidth: 1,
borderColor: 'black',
width: 320,
height: 240
},

image3: {
borderWidth: 1,
borderColor: 'black',
width: 100,
height: 100
},

image4: {
borderWidth: 1,
borderColor: 'black',
width: 200,
height: 200
},

image5: {
borderWidth: 1,
borderColor: 'black',
width: 355,
height: 100
},

image6: {
borderWidth: 1,
borderColor: 'black',
width: 355,
height: 355
},

});

exports.title = '<AssetThumbnailExample>';
exports.description = 'Example component that displays the thumbnail capabilities of the <Image /> tag';
module.exports = AssetThumbnailExampleView;
35 changes: 24 additions & 11 deletions Examples/UIExplorer/CameraRollExample.ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@ var {
SliderIOS,
StyleSheet,
SwitchIOS,
TouchableOpacity,
Text,
View,
} = React;

var CameraRollView = require('./CameraRollView.ios');
var AssetThumbnailExampleView = require('./AssetThumbnailExample.ios');

var CAMERA_ROLL_VIEW = 'camera_roll_view';

Expand Down Expand Up @@ -61,25 +63,36 @@ var CameraRollExample = React.createClass({
</View>
);
},

loadAsset(asset){
this.props.navigator.push({
title: "Thumbnails",
component: AssetThumbnailExampleView,
backButtonTitle: 'Back',
passProps: { asset: asset },
});
},

_renderImage(asset) {
var imageSize = this.state.bigImages ? 150 : 75;
var imageStyle = [styles.image, {width: imageSize, height: imageSize}];
var location = asset.node.location.longitude ?
JSON.stringify(asset.node.location) : 'Unknown location';
return (
<View key={asset} style={styles.row}>
<Image
source={asset.node.image}
style={imageStyle}
/>
<View style={styles.info}>
<Text style={styles.url}>{asset.node.image.uri}</Text>
<Text>{location}</Text>
<Text>{asset.node.group_name}</Text>
<Text>{new Date(asset.node.timestamp).toString()}</Text>
<TouchableOpacity onPress={ this.loadAsset.bind( this, asset ) }>
<View key={asset} style={styles.row}>
<Image
source={{ uri: asset.node.image.uri }}
style={imageStyle}
/>
<View style={styles.info}>
<Text style={styles.url}>{asset.node.image.uri}</Text>
<Text>{location}</Text>
<Text>{asset.node.group_name}</Text>
<Text>{new Date(asset.node.timestamp).toString()}</Text>
</View>
</View>
</View>
</TouchableOpacity>
);
},

Expand Down
1 change: 1 addition & 0 deletions Examples/UIExplorer/createExamplePage.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ var createExamplePage = function(title: ?string, exampleModule: ExampleModule)
var result = example.render(null);
if (result) {
renderedComponent = result;
result.props.navigator = this.props.navigator;
}
(React: Object).render = originalRender;
(React: Object).renderComponent = originalRenderComponent;
Expand Down
52 changes: 51 additions & 1 deletion Libraries/Image/Image.ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ var warning = require('warning');
* style={styles.logo}
* source={{uri: 'http://facebook.github.io/react/img/logo_og.png'}}
* />
* <Image
* style={styles.logo}
* source={{ uri: 'assets-library://...',
* options: { assetUseMaximumSize: true }
* }}
* />
* </View>
* );
* },
Expand All @@ -61,9 +67,41 @@ var Image = React.createClass({
* `uri` is a string representing the resource identifier for the image, which
* could be an http address, a local file path, or the name of a static image
* resource (which should be wrapped in the `require('image!name')` function).
*
* `options` supports the following optional properties:
*
* Photos Framework and Asset Library:
* -----------------------------------
* assetUseMaximumSize (boolean): Boolean value indicating whether to use the full
* resolution asset. Defaults to false.
*
* If false, the image will be automatically sized based
* on the target dimensions specified in the style property of the
* <Image... /> tag (width, height).
*
* If true, the full resolution asset will be used.
* This can result in substantial memory usage and potential crashes,
* especially when rendering many images in sequence. Consider that
* an 8MP photo taken with an iPhone6 will require 32MB of memory to
* display in full resolution (3264x2448).
*
*
* Photos Framework only (assets with uri matching ph://...):
* ----------------------------------------------------------
* contentMode (string): Content mode used when requesting images using the Photos Framework.
* `fit` (PHImageContentModeAspectFit) default
* `fill` (PHImageContentModeAspectFill)
*
* renderMode (string): Render mode used when reqeusting images using the Photos Framework.
* `fast` (PHImageRequestOptionsResizeModeFast) default
* `exact` (PHImageRequestOptionsResizeModeFast)
* `none` (PHImageRequestOptionsResizeModeNone)
*
*
*/
source: PropTypes.shape({
uri: PropTypes.string,
assetOptions: PropTypes.object,
}),
/**
* A static image to display while downloading the final image off the
Expand Down Expand Up @@ -154,7 +192,19 @@ var Image = React.createClass({
tintColor: style.tintColor,
});
if (isStored) {
nativeProps.imageTag = source.uri;
var options = {
// iOS specific asset options
assetResizeMode: 'fast',
assetContentMode: 'fill',
assetTargetSize: { width: style.width, height: style.height },
assetUseMaximumSize: false
};

Object.assign( options, this.props.source.options );

nativeProps.imageTag = { uri: source.uri,
options: options };

} else {
nativeProps.src = source.uri;
}
Expand Down
2 changes: 1 addition & 1 deletion Libraries/Image/RCTCameraRollManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ @implementation RCTCameraRollManager
successCallback:(RCTResponseSenderBlock)successCallback
errorCallback:(RCTResponseSenderBlock)errorCallback)
{
[RCTImageLoader loadImageWithTag:imageTag callback:^(NSError *loadError, UIImage *loadedImage) {
[RCTImageLoader loadImageWithTag:imageTag options:@{} callback:^(NSError *loadError, UIImage *loadedImage) {
if (loadError) {
errorCallback(@[[loadError localizedDescription]]);
return;
Expand Down
1 change: 1 addition & 0 deletions Libraries/Image/RCTImageLoader.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
* Will always call callback on main thread.
*/
+ (void)loadImageWithTag:(NSString *)tag
options:(NSDictionary *)options
callback:(void (^)(NSError *error, id /* UIImage or CAAnimation */ image))callback;

@end
Loading

0 comments on commit 967c105

Please sign in to comment.