Skip to content

Commit e3f1679

Browse files
author
Alessandro Aries
committed
1 parent 9ab80fc commit e3f1679

12 files changed

Lines changed: 330 additions & 11 deletions

File tree

README.md

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -212,21 +212,25 @@ If supplied, changes the color of all the non-transparent pixels to the given co
212212

213213
## Static Methods
214214

215-
### `FastImage.preload: (source[]) => void`
215+
### `FastImage.preload: (source[], onProgress?, onComplete?) => void`
216216

217217
Preload images to display later. e.g.
218218

219219
```js
220-
FastImage.preload([
220+
FastImage.preload(
221+
[
221222
{
222-
uri: 'https://facebook.github.io/react/img/logo_og.png',
223-
headers: { Authorization: 'someAuthToken' },
223+
uri: 'https://facebook.github.io/react/img/logo_og.png',
224+
headers: { Authorization: 'someAuthToken' },
224225
},
225226
{
226-
uri: 'https://facebook.github.io/react/img/logo_og.png',
227-
headers: { Authorization: 'someAuthToken' },
227+
uri: 'https://facebook.github.io/react/img/logo_og.png',
228+
headers: { Authorization: 'someAuthToken' },
228229
},
229-
])
230+
],
231+
(finished, total) => console.log(`Preloaded ${finished}/${total} images`),
232+
(finished, skipped) => console.log(`Completed. Failed to load ${skipped}/${finished} images`),
233+
)
230234
```
231235

232236
### `FastImage.clearMemoryCache: () => Promise<void>`
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package com.dylanvann.fastimage;
2+
3+
import android.support.annotation.Nullable;
4+
import android.util.Log;
5+
6+
import com.bumptech.glide.load.DataSource;
7+
import com.bumptech.glide.load.engine.GlideException;
8+
import com.bumptech.glide.request.RequestListener;
9+
import com.bumptech.glide.request.target.Target;
10+
import com.facebook.react.bridge.Arguments;
11+
import com.facebook.react.bridge.ReactApplicationContext;
12+
import com.facebook.react.bridge.WritableMap;
13+
import com.facebook.react.modules.core.DeviceEventManagerModule;
14+
15+
import java.io.File;
16+
17+
class FastImagePreloaderListener implements RequestListener<File> {
18+
private static final String LOG = "[FFFastImage]";
19+
private static final String EVENT_PROGRESS = "fffastimage-progress";
20+
private static final String EVENT_COMPLETE = "fffastimage-complete";
21+
22+
private final ReactApplicationContext reactContext;
23+
private final int id;
24+
private final int total;
25+
private int succeeded = 0;
26+
private int failed = 0;
27+
28+
public FastImagePreloaderListener(ReactApplicationContext reactContext, int id, int totalImages) {
29+
this.id = id;
30+
this.reactContext = reactContext;
31+
this.total = totalImages;
32+
}
33+
34+
@Override
35+
public boolean onLoadFailed(@Nullable GlideException e, Object o, Target<File> target, boolean b) {
36+
// o is whatever was passed to .load() = GlideURL, String, etc.
37+
Log.d(LOG, "Preload failed: " + o.toString());
38+
this.failed++;
39+
this.dispatchProgress();
40+
return false;
41+
}
42+
43+
@Override
44+
public boolean onResourceReady(File file, Object o, Target<File> target, DataSource dataSource, boolean b) {
45+
// o is whatever was passed to .load() = GlideURL, String, etc.
46+
Log.d(LOG, "Preload succeeded: " + o.toString());
47+
this.succeeded++;
48+
this.dispatchProgress();
49+
return false;
50+
}
51+
52+
private void maybeDispatchComplete() {
53+
if (this.failed + this.succeeded >= this.total) {
54+
WritableMap params = Arguments.createMap();
55+
params.putInt("id", this.id);
56+
params.putInt("finished", this.succeeded + this.failed);
57+
params.putInt("skipped", this.failed);
58+
reactContext
59+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
60+
.emit(EVENT_COMPLETE, params);
61+
}
62+
}
63+
64+
private void dispatchProgress() {
65+
WritableMap params = Arguments.createMap();
66+
params.putInt("id", this.id);
67+
params.putInt("finished", this.succeeded + this.failed);
68+
params.putInt("total", this.total);
69+
reactContext
70+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
71+
.emit(EVENT_PROGRESS, params);
72+
this.maybeDispatchComplete();
73+
}
74+
}

android/src/main/java/com/dylanvann/fastimage/FastImageViewModule.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616

1717
class FastImageViewModule extends ReactContextBaseJavaModule {
1818

19-
private static final String REACT_CLASS = "FastImageView";
19+
private static final String REACT_CLASS = "FastImagePreloaderManager";
20+
private int preloaders = 0;
2021

2122
FastImageViewModule(ReactApplicationContext reactContext) {
2223
super(reactContext);
@@ -29,18 +30,25 @@ public String getName() {
2930
}
3031

3132
@ReactMethod
32-
public void preload(final ReadableArray sources) {
33+
public void createPreloader(Promise promise) {
34+
promise.resolve(preloaders++);
35+
}
36+
37+
@ReactMethod
38+
public void preload(final int preloaderId, final ReadableArray sources) {
3339
final Activity activity = getCurrentActivity();
3440
if (activity == null) return;
3541
activity.runOnUiThread(new Runnable() {
3642
@Override
3743
public void run() {
44+
FastImagePreloaderListener preloader = new FastImagePreloaderListener(getReactApplicationContext(), preloaderId, sources.size());
3845
for (int i = 0; i < sources.size(); i++) {
3946
final ReadableMap source = sources.getMap(i);
4047
final FastImageSource imageSource = FastImageViewConverter.getImageSource(activity, source);
4148

4249
Glide
4350
.with(activity.getApplicationContext())
51+
.downloadOnly()
4452
// This will make this work for remote and local images. e.g.
4553
// - file:///
4654
// - content://
@@ -51,6 +59,7 @@ public void run() {
5159
imageSource.isBase64Resource() ? imageSource.getSource() :
5260
imageSource.isResource() ? imageSource.getUri() : imageSource.getGlideUrl()
5361
)
62+
.listener(preloader)
5463
.apply(FastImageViewConverter.getOptions(activity, imageSource, source))
5564
.preload();
5665
}

ios/FastImage.xcodeproj/project.pbxproj

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@
7979
/* End PBXCopyFilesBuildPhase section */
8080

8181
/* Begin PBXFileReference section */
82+
7547098A212F3BE70040708C /* FFFastImagePreloaderManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FFFastImagePreloaderManager.h; sourceTree = "<group>"; };
83+
75470990212F3C590040708C /* FFFastImagePreloaderManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FFFastImagePreloaderManager.m; sourceTree = "<group>"; };
84+
75470992212F3F9A0040708C /* FFFastImagePreloader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FFFastImagePreloader.h; sourceTree = "<group>"; };
85+
75470993212F409B0040708C /* FFFastImagePreloader.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FFFastImagePreloader.m; sourceTree = "<group>"; };
8286
A287971D1DE0C0A60081BDFA /* libFastImage.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libFastImage.a; sourceTree = BUILT_PRODUCTS_DIR; };
8387
FCFB25371EA5562700F59778 /* FFFastImageSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FFFastImageSource.h; sourceTree = "<group>"; };
8488
FCFB25381EA5562700F59778 /* FFFastImageSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FFFastImageSource.m; sourceTree = "<group>"; };
@@ -149,6 +153,10 @@
149153
FCFB253C1EA5562700F59778 /* FFFastImageViewManager.m */,
150154
FCFB253D1EA5562700F59778 /* RCTConvert+FFFastImage.h */,
151155
FCFB253E1EA5562700F59778 /* RCTConvert+FFFastImage.m */,
156+
7547098A212F3BE70040708C /* FFFastImagePreloaderManager.h */,
157+
75470990212F3C590040708C /* FFFastImagePreloaderManager.m */,
158+
75470992212F3F9A0040708C /* FFFastImagePreloader.h */,
159+
75470993212F409B0040708C /* FFFastImagePreloader.m */,
152160
);
153161
path = FastImage;
154162
sourceTree = "<group>";
@@ -261,10 +269,12 @@
261269
isa = PBXSourcesBuildPhase;
262270
buildActionMask = 2147483647;
263271
files = (
272+
75470994212F409B0040708C /* FFFastImagePreloader.m in Sources */,
264273
FCFB25411EA5562700F59778 /* FFFastImageViewManager.m in Sources */,
265274
FCFB25421EA5562700F59778 /* RCTConvert+FFFastImage.m in Sources */,
266275
FCFB25401EA5562700F59778 /* FFFastImageView.m in Sources */,
267276
FCFB253F1EA5562700F59778 /* FFFastImageSource.m in Sources */,
277+
75470991212F3C590040708C /* FFFastImagePreloaderManager.m in Sources */,
268278
);
269279
runOnlyForDeploymentPostprocessing = 0;
270280
};
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#import "FFFastImageSource.h"
2+
#import <Foundation/Foundation.h>
3+
#import <SDWebImage/SDWebImagePrefetcher.h>
4+
5+
@interface FFFastImagePreloader : SDWebImagePrefetcher
6+
7+
@property (nonatomic, readonly) NSNumber* id;
8+
9+
@end
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#import "FFFastImagePreloader.h"
2+
#import "FFFastImageSource.h"
3+
4+
static int instanceCounter = 0;
5+
6+
@implementation FFFastImagePreloader
7+
8+
-(instancetype) init {
9+
if (self = [super init]) {
10+
instanceCounter ++;
11+
_id = [NSNumber numberWithInt:instanceCounter];
12+
}
13+
return self;
14+
}
15+
16+
@end
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#import <React/RCTBridgeModule.h>
2+
#import <React/RCTEventEmitter.h>
3+
#import <SDWebImage/SDWebImagePrefetcher.h>
4+
5+
@interface FFFastImagePreloaderManager : RCTEventEmitter <RCTBridgeModule, SDWebImagePrefetcherDelegate>
6+
7+
@end
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
#import "FFFastImagePreloaderManager.h"
2+
#import "FFFastImagePreloader.h"
3+
#import "FFFastImageSource.h"
4+
#import "SDWebImageDownloader.h"
5+
6+
@implementation FFFastImagePreloaderManager
7+
{
8+
bool _hasListeners;
9+
NSMutableDictionary* _preloaders;
10+
}
11+
12+
RCT_EXPORT_MODULE(FastImagePreloaderManager);
13+
14+
- (dispatch_queue_t)methodQueue
15+
{
16+
return dispatch_queue_create("com.dylanvann.fastimage.FastImagePreloaderManager", DISPATCH_QUEUE_SERIAL);
17+
}
18+
19+
+ (BOOL)requiresMainQueueSetup
20+
{
21+
return YES;
22+
}
23+
24+
-(instancetype) init {
25+
if (self = [super init]) {
26+
_preloaders = [[NSMutableDictionary alloc] init];
27+
}
28+
return self;
29+
}
30+
31+
- (NSArray<NSString *> *)supportedEvents
32+
{
33+
return @[@"fffastimage-progress", @"fffastimage-complete"];
34+
}
35+
36+
- (void) imagePrefetcher:(nonnull SDWebImagePrefetcher *)imagePrefetcher
37+
didFinishWithTotalCount:(NSUInteger)totalCount
38+
skippedCount:(NSUInteger)skippedCount
39+
{
40+
NSNumber* id = ((FFFastImagePreloader*) imagePrefetcher).id;
41+
[_preloaders removeObjectForKey:id];
42+
[self sendEventWithName:@"fffastimage-complete"
43+
body:@{ @"id": id, @"finished": [NSNumber numberWithLong:totalCount], @"skipped": [NSNumber numberWithLong:skippedCount]}
44+
];
45+
}
46+
47+
- (void) imagePrefetcher:(nonnull SDWebImagePrefetcher *)imagePrefetcher
48+
didPrefetchURL:(nullable NSURL *)imageURL
49+
finishedCount:(NSUInteger)finishedCount
50+
totalCount:(NSUInteger)totalCount
51+
{
52+
NSNumber* id = ((FFFastImagePreloader*) imagePrefetcher).id;
53+
[self sendEventWithName:@"fffastimage-progress"
54+
body:@{ @"id": id, @"finished": [NSNumber numberWithLong:finishedCount], @"total": [NSNumber numberWithLong:totalCount] }
55+
];
56+
}
57+
58+
RCT_EXPORT_METHOD(createPreloader:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
59+
FFFastImagePreloader* preloader = [[FFFastImagePreloader alloc] init];
60+
preloader.delegate = self;
61+
_preloaders[preloader.id] = preloader;
62+
resolve(preloader.id);
63+
}
64+
65+
RCT_EXPORT_METHOD(preload:(nonnull NSNumber*)preloaderId sources:(nonnull NSArray<FFFastImageSource *> *)sources) {
66+
NSMutableArray *urls = [NSMutableArray arrayWithCapacity:sources.count];
67+
68+
[sources enumerateObjectsUsingBlock:^(FFFastImageSource * _Nonnull source, NSUInteger idx, BOOL * _Nonnull stop) {
69+
[source.headers enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString* header, BOOL *stop) {
70+
[[SDWebImageDownloader sharedDownloader] setValue:header forHTTPHeaderField:key];
71+
}];
72+
[urls setObject:source.url atIndexedSubscript:idx];
73+
}];
74+
75+
FFFastImagePreloader* preloader = _preloaders[preloaderId];
76+
[preloader prefetchURLs:urls];
77+
}
78+
79+
@end

src/PreloaderManager.tsx

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { NativeEventEmitter, NativeModules } from 'react-native'
2+
3+
import type { EmitterSubscription } from 'react-native'
4+
5+
import type {
6+
Source,
7+
PreloadProgressHandler,
8+
PreloadCompletionHandler,
9+
} from './index'
10+
11+
const nativeManager = NativeModules.FastImagePreloaderManager
12+
const nativeEmitter = new NativeEventEmitter(nativeManager)
13+
14+
type PreloadCallbacks = {
15+
onProgress?: PreloadProgressHandler
16+
onComplete?: PreloadCompletionHandler
17+
}
18+
19+
type OnProgressParams = {
20+
id: number
21+
finished: number
22+
total: number
23+
}
24+
25+
type OnCompleteParams = {
26+
id: number
27+
finished: number
28+
skipped: number
29+
}
30+
31+
class PreloaderManager {
32+
_instances: Map<number, PreloadCallbacks> = new Map()
33+
_subProgress!: EmitterSubscription
34+
_subComplete!: EmitterSubscription
35+
36+
preload(
37+
sources: Source[],
38+
onProgress?: PreloadProgressHandler,
39+
onComplete?: PreloadCompletionHandler,
40+
) {
41+
nativeManager.createPreloader().then((id: number) => {
42+
if (this._instances.size === 0) {
43+
this._subProgress = nativeEmitter.addListener(
44+
'fffastimage-progress',
45+
this.onProgress.bind(this),
46+
)
47+
this._subComplete = nativeEmitter.addListener(
48+
'fffastimage-complete',
49+
this.onComplete.bind(this),
50+
)
51+
}
52+
53+
this._instances.set(id, { onProgress, onComplete })
54+
55+
nativeManager.preload(id, sources)
56+
})
57+
}
58+
59+
onProgress({ id, finished, total }: OnProgressParams) {
60+
const instance = this._instances.get(id)
61+
62+
if (instance && instance.onProgress)
63+
instance.onProgress(finished, total)
64+
}
65+
66+
onComplete({ id, finished, skipped }: OnCompleteParams) {
67+
const instance = this._instances.get(id)
68+
69+
if (instance && instance.onComplete)
70+
instance.onComplete(finished, skipped)
71+
72+
this._instances.delete(id)
73+
74+
if (
75+
this._instances.size === 0 &&
76+
this._subProgress &&
77+
this._subComplete
78+
) {
79+
this._subProgress.remove()
80+
this._subComplete.remove()
81+
}
82+
}
83+
}
84+
85+
const preloaderManager = new PreloaderManager()
86+
87+
export default preloaderManager

0 commit comments

Comments
 (0)