[add] Image 'prefetch' and 'getSize' statics

Fix #160
This commit is contained in:
Nicolas Gallagher
2016-12-24 17:18:14 +00:00
parent 04e3c23e67
commit 509920be4b
4 changed files with 113 additions and 45 deletions
+17
View File
@@ -75,6 +75,23 @@ Example usage:
<Image resizeMode={Image.resizeMode.contain} /> <Image resizeMode={Image.resizeMode.contain} />
``` ```
## 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 ## Examples
```js ```js
+20 -24
View File
@@ -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 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'); //var ImageCapInsetsExample = require('./ImageCapInsetsExample');
//const IMAGE_PREFETCH_URL = 'http://facebook.github.io/origami/public/images/blog-hero.jpg?r=1&t=' + Date.now(); 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 prefetchTask = Image.prefetch(IMAGE_PREFETCH_URL);
/*
var NetworkImageCallbackExample = React.createClass({ var NetworkImageCallbackExample = React.createClass({
getInitialState: function() { getInitialState: function() {
return { return {
@@ -88,7 +87,6 @@ var NetworkImageCallbackExample = React.createClass({
}); });
} }
}); });
*/
var NetworkImageExample = React.createClass({ var NetworkImageExample = React.createClass({
getInitialState: function() { getInitialState: function() {
@@ -118,7 +116,6 @@ var NetworkImageExample = React.createClass({
} }
}); });
/*
var ImageSizeExample = React.createClass({ var ImageSizeExample = React.createClass({
getInitialState: function() { getInitialState: function() {
return { return {
@@ -133,24 +130,25 @@ var ImageSizeExample = React.createClass({
}, },
render: function() { render: function() {
return ( return (
<View style={{flexDirection: 'row'}}> <View>
<Image
style={{
width: 60,
height: 60,
backgroundColor: 'transparent',
marginRight: 10,
}}
source={this.props.source} />
<Text> <Text>
Actual dimensions:{'\n'} Actual dimensions:{'\n'}
Width: {this.state.width}, Height: {this.state.height} width: {this.state.width}, height: {this.state.height}
</Text> </Text>
<Image
source={this.props.source}
style={{
backgroundColor: '#eee',
height: 227,
marginTop: 10,
width: 323
}}
/>
</View> </View>
); );
}, },
}); });
*/
/* /*
var MultipleSourcesExample = React.createClass({ var MultipleSourcesExample = React.createClass({
getInitialState: function() { getInitialState: function() {
@@ -239,17 +237,17 @@ const examples = [
); );
}, },
}, },
/*
{ {
title: 'Image Loading Events', title: 'Image Loading Events',
render: function() { render: function() {
return ( return (
<NetworkImageCallbackExample source={{uri: 'http://facebook.github.io/origami/public/images/blog-hero.jpg?r=1&t=' + Date.now()}} <NetworkImageCallbackExample
prefetchedSource={{uri: IMAGE_PREFETCH_URL}}/> source={{uri: 'http://origami.design/public/images/bird-logo.png?r=1&t=' + Date.now()}}
prefetchedSource={{uri: IMAGE_PREFETCH_URL}}
/>
); );
}, },
}, },
*/
{ {
title: 'Error Handler', title: 'Error Handler',
render: function() { render: function() {
@@ -263,7 +261,7 @@ const examples = [
title: 'Image Download Progress', title: 'Image Download Progress',
render: function() { render: function() {
return ( return (
<NetworkImageExample source={{uri: 'http://facebook.github.io/origami/public/images/blog-hero.jpg?r=1'}}/> <NetworkImageExample source={{uri: 'http://origami.design/public/images/bird-logo.png?r=1'}}/>
); );
}, },
platform: 'ios', platform: 'ios',
@@ -567,14 +565,12 @@ const examples = [
platform: 'ios', platform: 'ios',
}, },
*/ */
/*
{ {
title: 'Image Size', title: 'Image Size',
render: function() { render: function() {
return <ImageSizeExample source={fullImage} />; return <ImageSizeExample source={{ uri: 'https://upload.wikimedia.org/wikipedia/commons/d/d7/Chestnut-mandibled_Toucan.jpg' }} />;
}, },
}, },
*/
/* /*
{ {
title: 'MultipleSourcesExample', title: 'MultipleSourcesExample',
+28 -21
View File
@@ -1,6 +1,7 @@
/* global window */ /* global window */
import applyNativeMethods from '../../modules/applyNativeMethods'; import applyNativeMethods from '../../modules/applyNativeMethods';
import ImageResizeMode from './ImageResizeMode'; import ImageResizeMode from './ImageResizeMode';
import ImageLoader from '../../modules/ImageLoader';
import ImageStylePropTypes from './ImageStylePropTypes'; import ImageStylePropTypes from './ImageStylePropTypes';
import requestAnimationFrame from 'fbjs/lib/requestAnimationFrame'; import requestAnimationFrame from 'fbjs/lib/requestAnimationFrame';
import StyleSheet from '../../apis/StyleSheet'; import StyleSheet from '../../apis/StyleSheet';
@@ -57,11 +58,19 @@ class Image extends Component {
style: emptyObject style: emptyObject
}; };
static getSize(uri, success, failure) {
ImageLoader.getSize(uri, success, failure);
}
static prefetch(uri) {
return ImageLoader.prefetch(uri);
}
static resizeMode = ImageResizeMode; static resizeMode = ImageResizeMode;
constructor(props, context) { constructor(props, context) {
super(props, context); super(props, context);
this.state = { isLoaded: false }; this.state = { shouldDisplaySource: false };
const uri = resolveAssetSource(props.source); const uri = resolveAssetSource(props.source);
this._imageState = uri ? STATUS_PENDING : STATUS_IDLE; this._imageState = uri ? STATUS_PENDING : STATUS_IDLE;
this._isMounted = false; this._isMounted = false;
@@ -75,7 +84,7 @@ class Image extends Component {
} }
componentDidUpdate() { componentDidUpdate() {
if (this._imageState === STATUS_PENDING && !this.image) { if (this._imageState === STATUS_PENDING) {
this._createImageLoader(); this._createImageLoader();
} }
} }
@@ -93,7 +102,7 @@ class Image extends Component {
} }
render() { render() {
const { isLoaded } = this.state; const { shouldDisplaySource } = this.state;
const { const {
accessibilityLabel, accessibilityLabel,
accessible, accessible,
@@ -103,13 +112,17 @@ class Image extends Component {
source, source,
testID, testID,
/* eslint-disable */ /* eslint-disable */
onError,
onLoad,
onLoadEnd,
onLoadStart,
resizeMode, resizeMode,
/* eslint-enable */ /* eslint-enable */
...other ...other
} = this.props; } = this.props;
const displayImage = resolveAssetSource(!isLoaded ? defaultSource : source); const displayImage = resolveAssetSource(shouldDisplaySource ? source : defaultSource);
const imageSizeStyle = resolveAssetDimensions(!isLoaded ? defaultSource : source); const imageSizeStyle = resolveAssetDimensions(shouldDisplaySource ? source : defaultSource);
const backgroundImage = displayImage ? `url("${displayImage}")` : null; const backgroundImage = displayImage ? `url("${displayImage}")` : null;
const originalStyle = StyleSheet.flatten(this.props.style); const originalStyle = StyleSheet.flatten(this.props.style);
const finalResizeMode = resizeMode || originalStyle.resizeMode || ImageResizeMode.cover; const finalResizeMode = resizeMode || originalStyle.resizeMode || ImageResizeMode.cover;
@@ -139,28 +152,21 @@ class Image extends Component {
} }
_createImageLoader() { _createImageLoader() {
const uri = resolveAssetSource(this.props.source);
this._destroyImageLoader(); this._destroyImageLoader();
this.image = new window.Image(); const uri = resolveAssetSource(this.props.source);
this.image.onerror = this._onError; this._imageRequestId = ImageLoader.load(uri, this._onLoad, this._onError);
this.image.onload = this._onLoad;
this.image.src = uri;
this._onLoadStart(); this._onLoadStart();
} }
_destroyImageLoader() { _destroyImageLoader() {
if (this.image) { if (this._imageRequestId) {
this.image.onerror = null; ImageLoader.abort(this._imageRequestId);
this.image.onload = null; this._imageRequestId = null;
this.image = null;
} }
} }
_onError = () => { _onError = () => {
const { onError, source } = this.props; const { onError, source } = this.props;
this._destroyImageLoader();
this._onLoadEnd();
this._updateImageState(STATUS_ERRORED); this._updateImageState(STATUS_ERRORED);
if (onError) { if (onError) {
onError({ onError({
@@ -169,13 +175,13 @@ class Image extends Component {
} }
}); });
} }
this._onLoadEnd();
} }
_onLoad = (e) => { _onLoad = (e) => {
const { onLoad } = this.props; const { onLoad } = this.props;
const event = { nativeEvent: e }; const event = { nativeEvent: e };
this._destroyImageLoader();
this._updateImageState(STATUS_LOADED); this._updateImageState(STATUS_LOADED);
if (onLoad) { onLoad(event); } if (onLoad) { onLoad(event); }
this._onLoadEnd(); this._onLoadEnd();
@@ -194,11 +200,12 @@ class Image extends Component {
_updateImageState(status) { _updateImageState(status) {
this._imageState = status; this._imageState = status;
const isLoaded = this._imageState === STATUS_LOADED; const shouldDisplaySource = this._imageState === STATUS_LOADED || this._imageState === STATUS_LOADING;
if (isLoaded !== this.state.isLoaded) { // only triggers a re-render when the image is loading (to support PJEG), loaded, or failed
if (shouldDisplaySource !== this.state.shouldDisplaySource) {
requestAnimationFrame(() => { requestAnimationFrame(() => {
if (this._isMounted) { if (this._isMounted) {
this.setState({ isLoaded }); this.setState({ shouldDisplaySource });
} }
}); });
} }
+48
View File
@@ -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;