From 509920be4be8bc57f51118f0248279996ae9dcb0 Mon Sep 17 00:00:00 2001 From: Nicolas Gallagher Date: Sat, 24 Dec 2016 17:18:14 +0000 Subject: [PATCH] [add] Image 'prefetch' and 'getSize' statics Fix #160 --- docs/components/Image.md | 17 ++++++++ examples/components/Image/ImageExample.js | 44 +++++++++----------- src/components/Image/index.js | 49 +++++++++++++---------- src/modules/ImageLoader/index.js | 48 ++++++++++++++++++++++ 4 files changed, 113 insertions(+), 45 deletions(-) create mode 100644 src/modules/ImageLoader/index.js 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;