diff --git a/docs/components/Image.md b/docs/components/Image.md
index 47d65fc5..02ab318d 100644
--- a/docs/components/Image.md
+++ b/docs/components/Image.md
@@ -75,6 +75,23 @@ Example usage:
```
+## Methods
+
+static **getSize**(uri: string, success: (width, height) => {}, failure: function)
+
+Retrieve the width and height (in pixels) of an image prior to displaying it.
+This method can fail if the image cannot be found, or fails to download.
+
+(In order to retrieve the image dimensions, the image may first need to be
+loaded or downloaded, after which it will be cached. This means that in
+principle you could use this method to preload images, however it is not
+optimized for that purpose, and may in future be implemented in a way that does
+not fully load/download the image data.)
+
+static **prefetch**(url: string): Promise
+
+Prefetches a remote image for later use by downloading it.
+
## Examples
```js
diff --git a/examples/components/Image/ImageExample.js b/examples/components/Image/ImageExample.js
index 73b06a67..843b7689 100644
--- a/examples/components/Image/ImageExample.js
+++ b/examples/components/Image/ImageExample.js
@@ -28,10 +28,9 @@ import { ActivityIndicator, Image, Platform, StyleSheet, Text, View } from 'reac
var base64Icon = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEsAAABLCAQAAACSR7JhAAADtUlEQVR4Ac3YA2Bj6QLH0XPT1Fzbtm29tW3btm3bfLZtv7e2ObZnms7d8Uw098tuetPzrxv8wiISrtVudrG2JXQZ4VOv+qUfmqCGGl1mqLhoA52oZlb0mrjsnhKpgeUNEs91Z0pd1kvihA3ULGVHiQO2narKSHKkEMulm9VgUyE60s1aWoMQUbpZOWE+kaqs4eLEjdIlZTcFZB0ndc1+lhB1lZrIuk5P2aib1NBpZaL+JaOGIt0ls47SKzLC7CqrlGF6RZ09HGoNy1lYl2aRSWL5GuzqWU1KafRdoRp0iOQEiDzgZPnG6DbldcomadViflnl/cL93tOoVbsOLVM2jylvdWjXolWX1hmfZbGR/wjypDjFLSZIRov09BgYmtUqPQPlQrPapecLgTIy0jMgPKtTeob2zWtrGH3xvjUkPCtNg/tm1rjwrMa+mdUkPd3hWbH0jArPGiU9ufCsNNWFZ40wpwn+62/66R2RUtoso1OB34tnLOcy7YB1fUdc9e0q3yru8PGM773vXsuZ5YIZX+5xmHwHGVvlrGPN6ZSiP1smOsMMde40wKv2VmwPPVXNut4sVpUreZiLBHi0qln/VQeI/LTMYXpsJtFiclUN+5HVZazim+Ky+7sAvxWnvjXrJFneVtLWLyPJu9K3cXLWeOlbMTlrIelbMDlrLenrjEQOtIF+fuI9xRp9ZBFp6+b6WT8RrxEpdK64BuvHgDk+vUy+b5hYk6zfyfs051gRoNO1usU12WWRWL73/MMEy9pMi9qIrR4ZpV16Rrvduxazmy1FSvuFXRkqTnE7m2kdb5U8xGjLw/spRr1uTov4uOgQE+0N/DvFrG/Jt7i/FzwxbA9kDanhf2w+t4V97G8lrT7wc08aA2QNUkuTfW/KimT01wdlfK4yEw030VfT0RtZbzjeMprNq8m8tnSTASrTLti64oBNdpmMQm0eEwvfPwRbUBywG5TzjPCsdwk3IeAXjQblLCoXnDVeoAz6SfJNk5TTzytCNZk/POtTSV40NwOFWzw86wNJRpubpXsn60NJFlHeqlYRbslqZm2jnEZ3qcSKgm0kTli3zZVS7y/iivZTweYXJ26Y+RTbV1zh3hYkgyFGSTKPfRVbRqWWVReaxYeSLarYv1Qqsmh1s95S7G+eEWK0f3jYKTbV6bOwepjfhtafsvUsqrQvrGC8YhmnO9cSCk3yuY984F1vesdHYhWJ5FvASlacshUsajFt2mUM9pqzvKGcyNJW0arTKN1GGGzQlH0tXwLDgQTurS8eIQAAAABJRU5ErkJggg==';
//var ImageCapInsetsExample = require('./ImageCapInsetsExample');
-//const IMAGE_PREFETCH_URL = 'http://facebook.github.io/origami/public/images/blog-hero.jpg?r=1&t=' + Date.now();
-//var prefetchTask = Image.prefetch(IMAGE_PREFETCH_URL);
+const IMAGE_PREFETCH_URL = 'http://origami.design/public/images/bird-logo.png?r=1&t=' + Date.now();
+var prefetchTask = Image.prefetch(IMAGE_PREFETCH_URL);
-/*
var NetworkImageCallbackExample = React.createClass({
getInitialState: function() {
return {
@@ -88,7 +87,6 @@ var NetworkImageCallbackExample = React.createClass({
});
}
});
-*/
var NetworkImageExample = React.createClass({
getInitialState: function() {
@@ -118,7 +116,6 @@ var NetworkImageExample = React.createClass({
}
});
-/*
var ImageSizeExample = React.createClass({
getInitialState: function() {
return {
@@ -133,24 +130,25 @@ var ImageSizeExample = React.createClass({
},
render: function() {
return (
-
-
+
Actual dimensions:{'\n'}
- Width: {this.state.width}, Height: {this.state.height}
+ width: {this.state.width}, height: {this.state.height}
+
);
},
});
-*/
+
/*
var MultipleSourcesExample = React.createClass({
getInitialState: function() {
@@ -239,17 +237,17 @@ const examples = [
);
},
},
- /*
{
title: 'Image Loading Events',
render: function() {
return (
-
+
);
},
},
- */
{
title: 'Error Handler',
render: function() {
@@ -263,7 +261,7 @@ const examples = [
title: 'Image Download Progress',
render: function() {
return (
-
+
);
},
platform: 'ios',
@@ -567,14 +565,12 @@ const examples = [
platform: 'ios',
},
*/
- /*
{
title: 'Image Size',
render: function() {
- return ;
+ return ;
},
},
- */
/*
{
title: 'MultipleSourcesExample',
diff --git a/src/components/Image/index.js b/src/components/Image/index.js
index 38c4d52b..cdcd24a3 100644
--- a/src/components/Image/index.js
+++ b/src/components/Image/index.js
@@ -1,6 +1,7 @@
/* global window */
import applyNativeMethods from '../../modules/applyNativeMethods';
import ImageResizeMode from './ImageResizeMode';
+import ImageLoader from '../../modules/ImageLoader';
import ImageStylePropTypes from './ImageStylePropTypes';
import requestAnimationFrame from 'fbjs/lib/requestAnimationFrame';
import StyleSheet from '../../apis/StyleSheet';
@@ -57,11 +58,19 @@ class Image extends Component {
style: emptyObject
};
+ static getSize(uri, success, failure) {
+ ImageLoader.getSize(uri, success, failure);
+ }
+
+ static prefetch(uri) {
+ return ImageLoader.prefetch(uri);
+ }
+
static resizeMode = ImageResizeMode;
constructor(props, context) {
super(props, context);
- this.state = { isLoaded: false };
+ this.state = { shouldDisplaySource: false };
const uri = resolveAssetSource(props.source);
this._imageState = uri ? STATUS_PENDING : STATUS_IDLE;
this._isMounted = false;
@@ -75,7 +84,7 @@ class Image extends Component {
}
componentDidUpdate() {
- if (this._imageState === STATUS_PENDING && !this.image) {
+ if (this._imageState === STATUS_PENDING) {
this._createImageLoader();
}
}
@@ -93,7 +102,7 @@ class Image extends Component {
}
render() {
- const { isLoaded } = this.state;
+ const { shouldDisplaySource } = this.state;
const {
accessibilityLabel,
accessible,
@@ -103,13 +112,17 @@ class Image extends Component {
source,
testID,
/* eslint-disable */
+ onError,
+ onLoad,
+ onLoadEnd,
+ onLoadStart,
resizeMode,
/* eslint-enable */
...other
} = this.props;
- const displayImage = resolveAssetSource(!isLoaded ? defaultSource : source);
- const imageSizeStyle = resolveAssetDimensions(!isLoaded ? defaultSource : source);
+ const displayImage = resolveAssetSource(shouldDisplaySource ? source : defaultSource);
+ const imageSizeStyle = resolveAssetDimensions(shouldDisplaySource ? source : defaultSource);
const backgroundImage = displayImage ? `url("${displayImage}")` : null;
const originalStyle = StyleSheet.flatten(this.props.style);
const finalResizeMode = resizeMode || originalStyle.resizeMode || ImageResizeMode.cover;
@@ -139,28 +152,21 @@ class Image extends Component {
}
_createImageLoader() {
- const uri = resolveAssetSource(this.props.source);
-
this._destroyImageLoader();
- this.image = new window.Image();
- this.image.onerror = this._onError;
- this.image.onload = this._onLoad;
- this.image.src = uri;
+ const uri = resolveAssetSource(this.props.source);
+ this._imageRequestId = ImageLoader.load(uri, this._onLoad, this._onError);
this._onLoadStart();
}
_destroyImageLoader() {
- if (this.image) {
- this.image.onerror = null;
- this.image.onload = null;
- this.image = null;
+ if (this._imageRequestId) {
+ ImageLoader.abort(this._imageRequestId);
+ this._imageRequestId = null;
}
}
_onError = () => {
const { onError, source } = this.props;
- this._destroyImageLoader();
- this._onLoadEnd();
this._updateImageState(STATUS_ERRORED);
if (onError) {
onError({
@@ -169,13 +175,13 @@ class Image extends Component {
}
});
}
+ this._onLoadEnd();
}
_onLoad = (e) => {
const { onLoad } = this.props;
const event = { nativeEvent: e };
- this._destroyImageLoader();
this._updateImageState(STATUS_LOADED);
if (onLoad) { onLoad(event); }
this._onLoadEnd();
@@ -194,11 +200,12 @@ class Image extends Component {
_updateImageState(status) {
this._imageState = status;
- const isLoaded = this._imageState === STATUS_LOADED;
- if (isLoaded !== this.state.isLoaded) {
+ const shouldDisplaySource = this._imageState === STATUS_LOADED || this._imageState === STATUS_LOADING;
+ // only triggers a re-render when the image is loading (to support PJEG), loaded, or failed
+ if (shouldDisplaySource !== this.state.shouldDisplaySource) {
requestAnimationFrame(() => {
if (this._isMounted) {
- this.setState({ isLoaded });
+ this.setState({ shouldDisplaySource });
}
});
}
diff --git a/src/modules/ImageLoader/index.js b/src/modules/ImageLoader/index.js
new file mode 100644
index 00000000..a92c0a10
--- /dev/null
+++ b/src/modules/ImageLoader/index.js
@@ -0,0 +1,48 @@
+let id = 0;
+const requests = {};
+
+const ImageLoader = {
+ abort(requestId: number) {
+ let image = requests[`${requestId}`];
+ if (image) {
+ image.onerror = image.onload = image = null;
+ delete requests[`${requestId}`];
+ }
+ },
+ getSize(uri, success, failure) {
+ let complete = false;
+ const interval = setInterval(callback, 16);
+ const requestId = ImageLoader.load(uri, callback, callback);
+
+ function callback() {
+ const image = requests[`${requestId}`];
+ if (image) {
+ const { naturalHeight, naturalWidth } = image;
+ if (naturalHeight && naturalWidth) {
+ success(naturalWidth, naturalHeight);
+ complete = true;
+ }
+ }
+ if (complete) {
+ ImageLoader.abort(requestId);
+ clearInterval(interval);
+ }
+ }
+ },
+ load(uri, onLoad, onError): number {
+ id += 1;
+ const image = new window.Image();
+ image.onerror = onError;
+ image.onload = onLoad;
+ image.src = uri;
+ requests[`${id}`] = image;
+ return id;
+ },
+ prefetch(uri): Promise {
+ return new Promise((resolve, reject) => {
+ ImageLoader.load(uri, resolve, reject);
+ });
+ }
+};
+
+export default ImageLoader;