From ead60db34afe5dc5f76845031e5480e5701f0287 Mon Sep 17 00:00:00 2001 From: doomsower Date: Wed, 29 Aug 2018 08:54:37 +0300 Subject: [PATCH 01/28] Add onProgress and onComplete callbacks for `preload` both on Android and iOS --- README.md | 26 +++--- .../fastimage/FastImagePreloaderListener.java | 74 +++++++++++++++++ ...ule.java => FastImagePreloaderModule.java} | 18 +++- .../fastimage/FastImageViewPackage.java | 2 +- ios/FastImage.xcodeproj/project.pbxproj | 12 +++ ios/FastImage/FFFastImagePreloader.h | 9 ++ ios/FastImage/FFFastImagePreloader.m | 16 ++++ ios/FastImage/FFFastImagePreloaderManager.h | 7 ++ ios/FastImage/FFFastImagePreloaderManager.m | 78 ++++++++++++++++++ ios/FastImage/FFFastImageViewManager.m | 16 ---- .../src/PreloadExample.js | 82 ++++++++++++++----- src/index.d.ts | 14 +++- src/index.js | 14 ++-- src/index.test.js | 2 + src/preloaderManager.js | 48 +++++++++++ 15 files changed, 357 insertions(+), 61 deletions(-) create mode 100644 android/src/main/java/com/dylanvann/fastimage/FastImagePreloaderListener.java rename android/src/main/java/com/dylanvann/fastimage/{FastImageViewModule.java => FastImagePreloaderModule.java} (69%) create mode 100644 ios/FastImage/FFFastImagePreloader.h create mode 100644 ios/FastImage/FFFastImagePreloader.m create mode 100644 ios/FastImage/FFFastImagePreloaderManager.h create mode 100644 ios/FastImage/FFFastImagePreloaderManager.m create mode 100644 src/preloaderManager.js diff --git a/README.md b/README.md index 36f08d981..bf9055480 100644 --- a/README.md +++ b/README.md @@ -191,21 +191,25 @@ In this case the image will still be styled and laid out the same way as `FastIm ## Static Methods -### `FastImage.preload: (source[]) => void` +### `FastImage.preload: (source[], onProgress?, onComplete?) => void` Preload images to display later. e.g. ```js -FastImage.preload([ - { - uri: 'https://facebook.github.io/react/img/logo_og.png', - headers: { Authorization: 'someAuthToken' }, - }, - { - uri: 'https://facebook.github.io/react/img/logo_og.png', - headers: { Authorization: 'someAuthToken' }, - }, -]) +FastImage.preload( + [ + { + uri: 'https://facebook.github.io/react/img/logo_og.png', + headers: { Authorization: 'someAuthToken' }, + }, + { + uri: 'https://facebook.github.io/react/img/logo_og.png', + headers: { Authorization: 'someAuthToken' }, + }, + ], + (finished, total) => console.log(`Preloaded ${finished}/${total} images`), + (finished, skipped) => console.log(`Completed. Failed to load ${skipped}/${finished} images`), +) ``` ## Troubleshooting diff --git a/android/src/main/java/com/dylanvann/fastimage/FastImagePreloaderListener.java b/android/src/main/java/com/dylanvann/fastimage/FastImagePreloaderListener.java new file mode 100644 index 000000000..e372cf1c8 --- /dev/null +++ b/android/src/main/java/com/dylanvann/fastimage/FastImagePreloaderListener.java @@ -0,0 +1,74 @@ +package com.dylanvann.fastimage; + +import android.support.annotation.Nullable; +import android.util.Log; + +import com.bumptech.glide.load.DataSource; +import com.bumptech.glide.load.engine.GlideException; +import com.bumptech.glide.request.RequestListener; +import com.bumptech.glide.request.target.Target; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.modules.core.DeviceEventManagerModule; + +import java.io.File; + +class FastImagePreloaderListener implements RequestListener { + private static final String LOG = "[FFFastImage]"; + private static final String EVENT_PROGRESS = "fffastimage-progress"; + private static final String EVENT_COMPLETE = "fffastimage-complete"; + + private final ReactApplicationContext reactContext; + private final int id; + private final int total; + private int succeeded = 0; + private int failed = 0; + + public FastImagePreloaderListener(ReactApplicationContext reactContext, int id, int totalImages) { + this.id = id; + this.reactContext = reactContext; + this.total = totalImages; + } + + @Override + public boolean onLoadFailed(@Nullable GlideException e, Object o, Target target, boolean b) { + // o is whatever was passed to .load() = GlideURL, String, etc. + Log.d(LOG, "Preload failed: " + o.toString()); + this.failed++; + this.dispatchProgress(); + return false; + } + + @Override + public boolean onResourceReady(File file, Object o, Target target, DataSource dataSource, boolean b) { + // o is whatever was passed to .load() = GlideURL, String, etc. + Log.d(LOG, "Preload succeeded: " + o.toString()); + this.succeeded++; + this.dispatchProgress(); + return false; + } + + private void maybeDispatchComplete() { + if (this.failed + this.succeeded >= this.total) { + WritableMap params = Arguments.createMap(); + params.putInt("id", this.id); + params.putInt("finished", this.succeeded + this.failed); + params.putInt("skipped", this.failed); + reactContext + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit(EVENT_COMPLETE, params); + } + } + + private void dispatchProgress() { + WritableMap params = Arguments.createMap(); + params.putInt("id", this.id); + params.putInt("finished", this.succeeded + this.failed); + params.putInt("total", this.total); + reactContext + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit(EVENT_PROGRESS, params); + this.maybeDispatchComplete(); + } +} \ No newline at end of file diff --git a/android/src/main/java/com/dylanvann/fastimage/FastImageViewModule.java b/android/src/main/java/com/dylanvann/fastimage/FastImagePreloaderModule.java similarity index 69% rename from android/src/main/java/com/dylanvann/fastimage/FastImageViewModule.java rename to android/src/main/java/com/dylanvann/fastimage/FastImagePreloaderModule.java index f92229ebc..062ead374 100644 --- a/android/src/main/java/com/dylanvann/fastimage/FastImageViewModule.java +++ b/android/src/main/java/com/dylanvann/fastimage/FastImagePreloaderModule.java @@ -4,17 +4,19 @@ import com.bumptech.glide.Glide; import com.bumptech.glide.load.model.GlideUrl; +import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; -class FastImageViewModule extends ReactContextBaseJavaModule { +class FastImagePreloaderModule extends ReactContextBaseJavaModule { - private static final String REACT_CLASS = "FastImageView"; + private static final String REACT_CLASS = "FastImagePreloaderManager"; + private int preloaders = 0; - FastImageViewModule(ReactApplicationContext reactContext) { + FastImagePreloaderModule(ReactApplicationContext reactContext) { super(reactContext); } @@ -24,23 +26,31 @@ public String getName() { } @ReactMethod - public void preload(final ReadableArray sources) { + public void createPreloader(Promise promise) { + promise.resolve(preloaders++); + } + + @ReactMethod + public void preload(final int preloaderId, final ReadableArray sources) { final Activity activity = getCurrentActivity(); if (activity == null) return; activity.runOnUiThread(new Runnable() { @Override public void run() { + FastImagePreloaderListener preloader = new FastImagePreloaderListener(getReactApplicationContext(), preloaderId, sources.size()); for (int i = 0; i < sources.size(); i++) { final ReadableMap source = sources.getMap(i); final GlideUrl glideUrl = FastImageViewConverter.getGlideUrl(source); final String stringUrl = glideUrl.toString(); Glide .with(activity.getApplicationContext()) + .downloadOnly() // This will make this work for remote and local images. e.g. // - file:/// // - content:// // - data:image/png;base64 .load(stringUrl.startsWith("http") ? glideUrl : stringUrl) + .listener(preloader) .apply(FastImageViewConverter.getOptions(source)) .preload(); } diff --git a/android/src/main/java/com/dylanvann/fastimage/FastImageViewPackage.java b/android/src/main/java/com/dylanvann/fastimage/FastImageViewPackage.java index 032f46ca3..48aa3b143 100644 --- a/android/src/main/java/com/dylanvann/fastimage/FastImageViewPackage.java +++ b/android/src/main/java/com/dylanvann/fastimage/FastImageViewPackage.java @@ -11,7 +11,7 @@ public class FastImageViewPackage implements ReactPackage { @Override public List createNativeModules(ReactApplicationContext reactContext) { - return Collections.singletonList(new FastImageViewModule(reactContext)); + return Collections.singletonList(new FastImagePreloaderModule(reactContext)); } @Override diff --git a/ios/FastImage.xcodeproj/project.pbxproj b/ios/FastImage.xcodeproj/project.pbxproj index d1f7d89c6..34e3e1e95 100644 --- a/ios/FastImage.xcodeproj/project.pbxproj +++ b/ios/FastImage.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 75470991212F3C590040708C /* FFFastImagePreloaderManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 75470990212F3C590040708C /* FFFastImagePreloaderManager.m */; }; + 75470994212F409B0040708C /* FFFastImagePreloader.m in Sources */ = {isa = PBXBuildFile; fileRef = 75470993212F409B0040708C /* FFFastImagePreloader.m */; }; FCC6D1391EB3912D0065F944 /* libSDWebImage iOS static.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FCC6D0D01EB38D2F0065F944 /* libSDWebImage iOS static.a */; }; FCFB253F1EA5562700F59778 /* FFFastImageSource.m in Sources */ = {isa = PBXBuildFile; fileRef = FCFB25381EA5562700F59778 /* FFFastImageSource.m */; }; FCFB25401EA5562700F59778 /* FFFastImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = FCFB253A1EA5562700F59778 /* FFFastImageView.m */; }; @@ -79,6 +81,10 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 7547098A212F3BE70040708C /* FFFastImagePreloaderManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FFFastImagePreloaderManager.h; sourceTree = ""; }; + 75470990212F3C590040708C /* FFFastImagePreloaderManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FFFastImagePreloaderManager.m; sourceTree = ""; }; + 75470992212F3F9A0040708C /* FFFastImagePreloader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FFFastImagePreloader.h; sourceTree = ""; }; + 75470993212F409B0040708C /* FFFastImagePreloader.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FFFastImagePreloader.m; sourceTree = ""; }; A287971D1DE0C0A60081BDFA /* libFastImage.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libFastImage.a; sourceTree = BUILT_PRODUCTS_DIR; }; FCC6D0C61EB38D2F0065F944 /* SDWebImage.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = SDWebImage.xcodeproj; path = Vendor/SDWebImage/SDWebImage.xcodeproj; sourceTree = ""; }; FCFB25371EA5562700F59778 /* FFFastImageSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FFFastImageSource.h; sourceTree = ""; }; @@ -152,6 +158,10 @@ FCFB253C1EA5562700F59778 /* FFFastImageViewManager.m */, FCFB253D1EA5562700F59778 /* RCTConvert+FFFastImage.h */, FCFB253E1EA5562700F59778 /* RCTConvert+FFFastImage.m */, + 7547098A212F3BE70040708C /* FFFastImagePreloaderManager.h */, + 75470990212F3C590040708C /* FFFastImagePreloaderManager.m */, + 75470992212F3F9A0040708C /* FFFastImagePreloader.h */, + 75470993212F409B0040708C /* FFFastImagePreloader.m */, ); path = FastImage; sourceTree = ""; @@ -265,10 +275,12 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 75470994212F409B0040708C /* FFFastImagePreloader.m in Sources */, FCFB25411EA5562700F59778 /* FFFastImageViewManager.m in Sources */, FCFB25421EA5562700F59778 /* RCTConvert+FFFastImage.m in Sources */, FCFB25401EA5562700F59778 /* FFFastImageView.m in Sources */, FCFB253F1EA5562700F59778 /* FFFastImageSource.m in Sources */, + 75470991212F3C590040708C /* FFFastImagePreloaderManager.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ios/FastImage/FFFastImagePreloader.h b/ios/FastImage/FFFastImagePreloader.h new file mode 100644 index 000000000..29fbb8f80 --- /dev/null +++ b/ios/FastImage/FFFastImagePreloader.h @@ -0,0 +1,9 @@ +#import "FFFastImageSource.h" +#import +#import + +@interface FFFastImagePreloader : SDWebImagePrefetcher + +@property (nonatomic, readonly) NSNumber* id; + +@end diff --git a/ios/FastImage/FFFastImagePreloader.m b/ios/FastImage/FFFastImagePreloader.m new file mode 100644 index 000000000..085541b1f --- /dev/null +++ b/ios/FastImage/FFFastImagePreloader.m @@ -0,0 +1,16 @@ +#import "FFFastImagePreloader.h" +#import "FFFastImageSource.h" + +static int instanceCounter = 0; + +@implementation FFFastImagePreloader + +-(instancetype) init { + if (self = [super init]) { + instanceCounter ++; + _id = [NSNumber numberWithInt:instanceCounter]; + } + return self; +} + +@end diff --git a/ios/FastImage/FFFastImagePreloaderManager.h b/ios/FastImage/FFFastImagePreloaderManager.h new file mode 100644 index 000000000..830450cff --- /dev/null +++ b/ios/FastImage/FFFastImagePreloaderManager.h @@ -0,0 +1,7 @@ +#import +#import +#import + +@interface FFFastImagePreloaderManager : RCTEventEmitter + +@end diff --git a/ios/FastImage/FFFastImagePreloaderManager.m b/ios/FastImage/FFFastImagePreloaderManager.m new file mode 100644 index 000000000..e9ba5397a --- /dev/null +++ b/ios/FastImage/FFFastImagePreloaderManager.m @@ -0,0 +1,78 @@ +#import "FFFastImagePreloaderManager.h" +#import "FFFastImagePreloader.h" +#import "FFFastImageSource.h" + +@implementation FFFastImagePreloaderManager +{ + bool _hasListeners; + NSMutableDictionary* _preloaders; +} + +RCT_EXPORT_MODULE(FastImagePreloaderManager); + +- (dispatch_queue_t)methodQueue +{ + return dispatch_queue_create("com.dylanvann.fastimage.FastImagePreloaderManager", DISPATCH_QUEUE_SERIAL); +} + ++ (BOOL)requiresMainQueueSetup +{ + return YES; +} + +-(instancetype) init { + if (self = [super init]) { + _preloaders = [[NSMutableDictionary alloc] init]; + } + return self; +} + +- (NSArray *)supportedEvents +{ + return @[@"fffastimage-progress", @"fffastimage-complete"]; +} + +- (void) imagePrefetcher:(nonnull SDWebImagePrefetcher *)imagePrefetcher + didFinishWithTotalCount:(NSUInteger)totalCount + skippedCount:(NSUInteger)skippedCount +{ + NSNumber* id = ((FFFastImagePreloader*) imagePrefetcher).id; + [_preloaders removeObjectForKey:id]; + [self sendEventWithName:@"fffastimage-complete" + body:@{ @"id": id, @"finished": [NSNumber numberWithLong:totalCount], @"skipped": [NSNumber numberWithLong:skippedCount]} + ]; +} + +- (void) imagePrefetcher:(nonnull SDWebImagePrefetcher *)imagePrefetcher + didPrefetchURL:(nullable NSURL *)imageURL + finishedCount:(NSUInteger)finishedCount + totalCount:(NSUInteger)totalCount +{ + NSNumber* id = ((FFFastImagePreloader*) imagePrefetcher).id; + [self sendEventWithName:@"fffastimage-progress" + body:@{ @"id": id, @"finished": [NSNumber numberWithLong:finishedCount], @"total": [NSNumber numberWithLong:totalCount] } + ]; +} + +RCT_EXPORT_METHOD(createPreloader:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + FFFastImagePreloader* preloader = [[FFFastImagePreloader alloc] init]; + preloader.delegate = self; + _preloaders[preloader.id] = preloader; + resolve(preloader.id); +} + +RCT_EXPORT_METHOD(preload:(nonnull NSNumber*)preloaderId sources:(nonnull NSArray *)sources) { + NSMutableArray *urls = [NSMutableArray arrayWithCapacity:sources.count]; + + [sources enumerateObjectsUsingBlock:^(FFFastImageSource * _Nonnull source, NSUInteger idx, BOOL * _Nonnull stop) { + [source.headers enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString* header, BOOL *stop) { + [[SDWebImageDownloader sharedDownloader] setValue:header forHTTPHeaderField:key]; + }]; + [urls setObject:source.url atIndexedSubscript:idx]; + }]; + + FFFastImagePreloader* preloader = _preloaders[preloaderId]; + [preloader prefetchURLs:urls]; +} + +@end diff --git a/ios/FastImage/FFFastImageViewManager.m b/ios/FastImage/FFFastImageViewManager.m index 5cc861866..0f9375276 100644 --- a/ios/FastImage/FFFastImageViewManager.m +++ b/ios/FastImage/FFFastImageViewManager.m @@ -1,8 +1,6 @@ #import "FFFastImageViewManager.h" #import "FFFastImageView.h" -#import - @implementation FFFastImageViewManager RCT_EXPORT_MODULE(FastImageView) @@ -19,19 +17,5 @@ - (FFFastImageView*)view { RCT_EXPORT_VIEW_PROPERTY(onFastImageLoad, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onFastImageLoadEnd, RCTDirectEventBlock) -RCT_EXPORT_METHOD(preload:(nonnull NSArray *)sources) -{ - NSMutableArray *urls = [NSMutableArray arrayWithCapacity:sources.count]; - - [sources enumerateObjectsUsingBlock:^(FFFastImageSource * _Nonnull source, NSUInteger idx, BOOL * _Nonnull stop) { - [source.headers enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString* header, BOOL *stop) { - [[SDWebImageDownloader sharedDownloader] setValue:header forHTTPHeaderField:key]; - }]; - [urls setObject:source.url atIndexedSubscript:idx]; - }]; - - [[SDWebImagePrefetcher sharedImagePrefetcher] prefetchURLs:urls]; -} - @end diff --git a/react-native-fast-image-example/src/PreloadExample.js b/react-native-fast-image-example/src/PreloadExample.js index dfb0c78c5..49c4a3b7f 100644 --- a/react-native-fast-image-example/src/PreloadExample.js +++ b/react-native-fast-image-example/src/PreloadExample.js @@ -1,5 +1,5 @@ import React, { Component } from 'react' -import { StyleSheet, View } from 'react-native' +import { StyleSheet, Text, View } from 'react-native' import SectionFlex from './SectionFlex' import FastImage from 'react-native-fast-image' import Section from './Section' @@ -8,55 +8,95 @@ import uuid from 'uuid/v4' import Button from './Button' import { createImageProgress } from 'react-native-image-progress' -const IMAGE_URL = - 'https://cdn-images-1.medium.com/max/1600/1*-CY5bU4OqiJRox7G00sftw.gif' +const IMAGE_URLS = [ + 'https://cdn-images-1.medium.com/max/1600/1*-CY5bU4OqiJRox7G00sftw.gif', + 'https://media.giphy.com/media/GEsoqZDGVoisw/giphy.gif', + 'https://image.that.always.fails.com', +]; const Image = createImageProgress(FastImage) class PreloadExample extends Component { state = { show: false, - url: IMAGE_URL, + urls: [...IMAGE_URLS], + progress: [0, 0], + result: [0, 0], } bustCache = () => { - const key = uuid() - const bust = `?bust=${key}` - // Preload images. This can be called anywhere. - const url = IMAGE_URL + bust this.setState({ - url, + urls: IMAGE_URLS.map((url) => `${url}?bust=${uuid()}`), show: false, + progress: [0, 0], + result: [0, 0], }) } + onProgress = (loaded, total) => { + this.setState({ progress: [loaded, total] }) + } + + onComplete = (loaded, skipped) => { + this.setState({ result: [skipped, loaded] }) + } + preload = () => { - FastImage.preload([{ uri: this.state.url }]) + FastImage.preload( + this.state.urls.map((uri) => ({ uri })), + this.onProgress, + this.onComplete, + ) } showImage = () => { this.setState({ show: true }) } + renderImage = (uri) => { + return this.state.show ? ( + + ) : ( + + ); + } + + renderImages = () => { + const { urls, show } = this.state; + return ( + + { + urls.map(this.renderImage) + } + + ) + } + render() { return (
- +
- {this.state.show ? ( - - ) : ( - - )} + {this.renderImages()} + + {`processed: ${this.state.progress[0]} out of ${this.state.progress[1]}`} + + { + !!this.state.result[1] && + + {`completed: skipped ${this.state.result[0]} out of ${this.state.result[1]}`} + + }