diff --git a/Examples/UIExplorer/ImageExample.js b/Examples/UIExplorer/ImageExample.js index 4fb0722d854b98..ffcea830dbb625 100644 --- a/Examples/UIExplorer/ImageExample.js +++ b/Examples/UIExplorer/ImageExample.js @@ -35,11 +35,14 @@ var { 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); var NetworkImageCallbackExample = React.createClass({ getInitialState: function() { return { events: [], + startLoadPrefetched: false, mountTime: new Date(), }; }, @@ -58,9 +61,26 @@ var NetworkImageCallbackExample = React.createClass({ style={[styles.base, {overflow: 'visible'}]} onLoadStart={() => this._loadEventFired(`✔ onLoadStart (+${new Date() - mountTime}ms)`)} onLoad={() => this._loadEventFired(`✔ onLoad (+${new Date() - mountTime}ms)`)} - onLoadEnd={() => this._loadEventFired(`✔ onLoadEnd (+${new Date() - mountTime}ms)`)} + onLoadEnd={() => { + this._loadEventFired(`✔ onLoadEnd (+${new Date() - mountTime}ms)`); + this.setState({startLoadPrefetched: true}, () => { + prefetchTask.then(() => { + this._loadEventFired(`✔ Prefetch OK (+${new Date() - mountTime}ms)`); + }, error => { + this._loadEventFired(`✘ Prefetch failed (+${new Date() - mountTime}ms)`); + }); + }); + }} /> - + {this.state.startLoadPrefetched ? + this._loadEventFired(`✔ (prefetched) onLoadStart (+${new Date() - mountTime}ms)`)} + onLoad={() => this._loadEventFired(`✔ (prefetched) onLoad (+${new Date() - mountTime}ms)`)} + onLoadEnd={() => this._loadEventFired(`✔ (prefetched) onLoadEnd (+${new Date() - mountTime}ms)`)} + /> + : null} {this.state.events.join('\n')} @@ -173,7 +193,8 @@ exports.examples = [ title: 'Image Loading Events', render: function() { return ( - + ); }, }, diff --git a/Libraries/Image/Image.android.js b/Libraries/Image/Image.android.js index d2e1a177cc6007..3ec42358cc65bf 100644 --- a/Libraries/Image/Image.android.js +++ b/Libraries/Image/Image.android.js @@ -23,11 +23,14 @@ var StyleSheetPropType = require('StyleSheetPropType'); var View = require('View'); var flattenStyle = require('flattenStyle'); -var invariant = require('fbjs/lib/invariant'); var merge = require('merge'); var requireNativeComponent = require('requireNativeComponent'); var resolveAssetSource = require('resolveAssetSource'); +var { + ImageLoader, +} = NativeModules; + /** * - A react component for displaying different types of images, * including network images, static resources, temporary local images, and @@ -110,6 +113,13 @@ var Image = React.createClass({ statics: { resizeMode: ImageResizeMode, + /** + * Prefetches a remote image for later use by downloading it to the disk + * cache + */ + prefetch(url: string) { + return ImageLoader.prefetchImage(url); + }, }, mixins: [NativeMethodsMixin], diff --git a/Libraries/Image/Image.ios.js b/Libraries/Image/Image.ios.js index 940725b91b3c19..7c496656869bf2 100644 --- a/Libraries/Image/Image.ios.js +++ b/Libraries/Image/Image.ios.js @@ -15,23 +15,22 @@ var EdgeInsetsPropType = require('EdgeInsetsPropType'); var ImageResizeMode = require('ImageResizeMode'); var ImageStylePropTypes = require('ImageStylePropTypes'); var NativeMethodsMixin = require('NativeMethodsMixin'); +var NativeModules = require('NativeModules'); var PropTypes = require('ReactPropTypes'); var React = require('React'); var ReactNativeViewAttributes = require('ReactNativeViewAttributes'); -var View = require('View'); var StyleSheet = require('StyleSheet'); var StyleSheetPropType = require('StyleSheetPropType'); var flattenStyle = require('flattenStyle'); -var invariant = require('fbjs/lib/invariant'); var requireNativeComponent = require('requireNativeComponent'); var resolveAssetSource = require('resolveAssetSource'); -var warning = require('fbjs/lib/warning'); var { + ImageLoader, ImageViewManager, NetworkImageViewManager, -} = require('NativeModules'); +} = NativeModules; /** * A React component for displaying different types of images, @@ -181,7 +180,14 @@ var Image = React.createClass({ ImageViewManager.getSize(uri, success, failure || function() { console.warn('Failed to get size for image: ' + uri); }); - } + }, + /** + * Prefetches a remote image for later use by downloading it to the disk + * cache + */ + prefetch(url: string) { + return ImageLoader.prefetchImage(url); + }, }, mixins: [NativeMethodsMixin], diff --git a/Libraries/Image/RCTImageLoader.m b/Libraries/Image/RCTImageLoader.m index ac93eff1b95ade..fe83fdbb5db1d4 100644 --- a/Libraries/Image/RCTImageLoader.m +++ b/Libraries/Image/RCTImageLoader.m @@ -20,6 +20,9 @@ #import "RCTNetworking.h" #import "RCTUtils.h" +static NSString *const RCTErrorInvalidURI = @"E_INVALID_URI"; +static NSString *const RCTErrorPrefetchFailure = @"E_PREFETCH_FAILURE"; + @implementation UIImage (React) - (CAKeyframeAnimation *)reactKeyframeAnimation @@ -634,6 +637,27 @@ - (RCTImageLoaderCancellationBlock)getImageSize:(NSString *)imageTag }]; } +#pragma mark - Bridged methods + +RCT_EXPORT_METHOD(prefetchImage:(NSString *)uri + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) +{ + if (!uri.length) { + reject(RCTErrorInvalidURI, @"Cannot prefetch an image for an empty URI", nil); + return; + } + + [_bridge.imageLoader loadImageWithTag:uri callback:^(NSError *error, UIImage *image) { + if (error) { + reject(RCTErrorPrefetchFailure, nil, error); + return; + } + + resolve(@YES); + }]; +} + #pragma mark - RCTURLRequestHandler - (BOOL)canHandleRequest:(NSURLRequest *)request diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/image/BUCK b/ReactAndroid/src/main/java/com/facebook/react/modules/image/BUCK new file mode 100644 index 00000000000000..4dfac315845a8f --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/image/BUCK @@ -0,0 +1,23 @@ +include_defs('//ReactAndroid/DEFS') + +android_library( + name = 'image', + srcs = glob(['*.java']), + deps = [ + react_native_target('java/com/facebook/react/bridge:bridge'), + react_native_target('java/com/facebook/react/common:common'), + react_native_dep('libraries/fresco/fresco-react-native:fbcore'), + react_native_dep('libraries/fresco/fresco-react-native:fresco-react-native'), + react_native_dep('libraries/fresco/fresco-react-native:fresco-drawee'), + react_native_dep('libraries/fresco/fresco-react-native:imagepipeline'), + react_native_dep('third-party/java/infer-annotations:infer-annotations'), + react_native_dep('third-party/java/jsr-305:jsr-305'), +], + visibility = [ + 'PUBLIC', + ], +) + +project_config( + src_target = ':image', +) diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/image/ImageLoaderModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/image/ImageLoaderModule.java new file mode 100644 index 00000000000000..9bcc746c4b7b0b --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/image/ImageLoaderModule.java @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.modules.image; + +import android.net.Uri; + +import com.facebook.common.executors.CallerThreadExecutor; +import com.facebook.datasource.BaseDataSubscriber; +import com.facebook.datasource.DataSource; +import com.facebook.datasource.DataSubscriber; +import com.facebook.drawee.backends.pipeline.Fresco; +import com.facebook.imagepipeline.request.ImageRequest; +import com.facebook.imagepipeline.request.ImageRequestBuilder; +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; + +public class ImageLoaderModule extends ReactContextBaseJavaModule { + + private static final String ERROR_INVALID_URI = "E_INVALID_URI"; + private static final String ERROR_PREFETCH_FAILURE = "E_PREFETCH_FAILURE"; + + public ImageLoaderModule(ReactApplicationContext reactContext) { + super(reactContext); + } + + @Override + public String getName() { + return "ImageLoader"; + } + + /** + * Prefetches the given image to the Fresco image disk cache. + * + * @param uriString the URI of the remote image to prefetch + * @param promise the promise that is fulfilled when the image is successfully prefetched + * or rejected when there is an error + */ + @ReactMethod + public void prefetchImage(String uriString, final Promise promise) { + if (uriString == null || uriString.isEmpty()) { + promise.reject(ERROR_INVALID_URI, "Cannot prefetch an image for an empty URI"); + return; + } + + Uri uri = Uri.parse(uriString); + ImageRequest request = ImageRequestBuilder.newBuilderWithSource(uri).build(); + + DataSource prefetchSource = Fresco.getImagePipeline().prefetchToDiskCache(request, this); + DataSubscriber prefetchSubscriber = new BaseDataSubscriber() { + @Override + protected void onNewResultImpl(DataSource dataSource) { + if (!dataSource.isFinished()) { + return; + } + try { + promise.resolve(true); + } finally { + dataSource.close(); + } + } + + @Override + protected void onFailureImpl(DataSource dataSource) { + try { + promise.reject(ERROR_PREFETCH_FAILURE, dataSource.getFailureCause()); + } finally { + dataSource.close(); + } + } + }; + prefetchSource.subscribe(prefetchSubscriber, CallerThreadExecutor.getInstance()); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK b/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK index c5d4d0c1e1670f..c93f89f6e7bb6e 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK @@ -36,6 +36,7 @@ android_library( react_native_target('java/com/facebook/react/modules/debug:debug'), react_native_target('java/com/facebook/react/modules/dialog:dialog'), react_native_target('java/com/facebook/react/modules/fresco:fresco'), + react_native_target('java/com/facebook/react/modules/image:image'), react_native_target('java/com/facebook/react/modules/intent:intent'), react_native_target('java/com/facebook/react/modules/location:location'), react_native_target('java/com/facebook/react/modules/netinfo:netinfo'), diff --git a/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java b/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java index 143fc271f05b1c..2687402c660d40 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java +++ b/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java @@ -25,6 +25,7 @@ import com.facebook.react.modules.datepicker.DatePickerDialogModule; import com.facebook.react.modules.dialog.DialogModule; import com.facebook.react.modules.fresco.FrescoModule; +import com.facebook.react.modules.image.ImageLoaderModule; import com.facebook.react.modules.intent.IntentModule; import com.facebook.react.modules.location.LocationModule; import com.facebook.react.modules.netinfo.NetInfoModule; @@ -76,6 +77,7 @@ public List createNativeModules(ReactApplicationContext reactConte new DialogModule(reactContext), new FrescoModule(reactContext), new ImageEditingManager(reactContext), + new ImageLoaderModule(reactContext), new ImageStoreManager(reactContext), new IntentModule(reactContext), new LocationModule(reactContext),