react-native-fast-image 8.6.42 → 8.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -1
- package/android/src/main/java/com/dylanvann/fastimage/FastImageViewModule.java +134 -24
- package/dist/index.cjs.js +10 -1
- package/dist/index.cjs.js.flow +11 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +10 -1
- package/dist/index.js.flow +11 -1
- package/ios/FastImage/FFFastImageSource.h +2 -0
- package/ios/FastImage/FFFastImageView.m +26 -1
- package/ios/FastImage/FFFastImageViewManager.m +109 -26
- package/ios/FastImage/RCTConvert+FFFastImage.m +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -213,7 +213,7 @@ If supplied, changes the color of all the non-transparent pixels to the given co
|
|
|
213
213
|
|
|
214
214
|
## Static Methods
|
|
215
215
|
|
|
216
|
-
### `FastImage.preload: (source[]) =>
|
|
216
|
+
### `FastImage.preload: (source[]) => Promise<result[]>`
|
|
217
217
|
|
|
218
218
|
Preload images to display later. e.g.
|
|
219
219
|
|
|
@@ -230,6 +230,13 @@ FastImage.preload([
|
|
|
230
230
|
])
|
|
231
231
|
```
|
|
232
232
|
|
|
233
|
+
It resolves when all the images have loaded or failed, with a result for each source, in order: `{ uri, ok, width, height, error }`. `width` and `height` are the image's size (as in `onLoad`), which is also a way to find an image's size without showing it. A few sources load at a time (3, or `SDWebImagePrefetcher`'s `maxConcurrentPrefetchCount` on iOS), so a long list doesn't hold up the images the app is showing, and at low priority unless the source sets `priority`. It never rejects:
|
|
234
|
+
|
|
235
|
+
```js
|
|
236
|
+
const results = await FastImage.preload(sources)
|
|
237
|
+
const failed = results.filter((result) => !result.ok)
|
|
238
|
+
```
|
|
239
|
+
|
|
233
240
|
### `FastImage.clearMemoryCache: () => Promise<void>`
|
|
234
241
|
|
|
235
242
|
Clear all images from memory cache.
|
|
@@ -1,24 +1,34 @@
|
|
|
1
1
|
package com.dylanvann.fastimage;
|
|
2
2
|
|
|
3
3
|
import android.app.Activity;
|
|
4
|
-
import android.
|
|
4
|
+
import android.graphics.drawable.Drawable;
|
|
5
5
|
|
|
6
6
|
import androidx.annotation.NonNull;
|
|
7
|
+
import androidx.annotation.Nullable;
|
|
7
8
|
|
|
8
9
|
import com.bumptech.glide.Glide;
|
|
9
|
-
import com.bumptech.glide.
|
|
10
|
+
import com.bumptech.glide.Priority;
|
|
11
|
+
import com.bumptech.glide.load.DataSource;
|
|
12
|
+
import com.bumptech.glide.load.engine.GlideException;
|
|
13
|
+
import com.bumptech.glide.request.RequestListener;
|
|
14
|
+
import com.bumptech.glide.request.RequestOptions;
|
|
15
|
+
import com.bumptech.glide.request.target.Target;
|
|
16
|
+
import com.facebook.react.bridge.Arguments;
|
|
10
17
|
import com.facebook.react.bridge.Promise;
|
|
11
18
|
import com.facebook.react.bridge.ReactApplicationContext;
|
|
12
19
|
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
|
13
20
|
import com.facebook.react.bridge.ReactMethod;
|
|
14
21
|
import com.facebook.react.bridge.ReadableArray;
|
|
15
22
|
import com.facebook.react.bridge.ReadableMap;
|
|
16
|
-
import com.facebook.react.
|
|
23
|
+
import com.facebook.react.bridge.UiThreadUtil;
|
|
24
|
+
import com.facebook.react.bridge.WritableArray;
|
|
25
|
+
import com.facebook.react.bridge.WritableMap;
|
|
26
|
+
|
|
27
|
+
import java.util.ArrayDeque;
|
|
17
28
|
|
|
18
29
|
class FastImageViewModule extends ReactContextBaseJavaModule {
|
|
19
30
|
|
|
20
31
|
private static final String REACT_CLASS = "FastImageView";
|
|
21
|
-
private static final String TAG = "FastImage";
|
|
22
32
|
|
|
23
33
|
FastImageViewModule(ReactApplicationContext reactContext) {
|
|
24
34
|
super(reactContext);
|
|
@@ -30,41 +40,141 @@ class FastImageViewModule extends ReactContextBaseJavaModule {
|
|
|
30
40
|
return REACT_CLASS;
|
|
31
41
|
}
|
|
32
42
|
|
|
43
|
+
// At most this many preloaded sources load at a time, across all preload
|
|
44
|
+
// calls (the same as SDWebImagePrefetcher's default on iOS), so a long
|
|
45
|
+
// list doesn't queue hundreds of requests ahead of the images the app is
|
|
46
|
+
// showing (Glide's executors run requests in order within a priority).
|
|
47
|
+
private static final int PRELOAD_LIMIT = 3;
|
|
48
|
+
// Preloads waiting to start, in the order they were added, and the number
|
|
49
|
+
// loading. Only used on the UI thread (Glide calls its listeners there).
|
|
50
|
+
private static final ArrayDeque<Runnable> pendingPreloads = new ArrayDeque<>();
|
|
51
|
+
private static int preloadsInFlight = 0;
|
|
52
|
+
|
|
53
|
+
// Starts pending preloads while there's room. A listener calls it again,
|
|
54
|
+
// sometimes from inside run() (Glide reports a memory-cached image
|
|
55
|
+
// synchronously from preload()). That's fine: the counters are shared and
|
|
56
|
+
// updated before each run(), so the inner call starts what fits, and the
|
|
57
|
+
// outer loop sees the updated counters when it checks again.
|
|
58
|
+
private static void startPendingPreloads() {
|
|
59
|
+
while (preloadsInFlight < PRELOAD_LIMIT && !pendingPreloads.isEmpty()) {
|
|
60
|
+
preloadsInFlight++;
|
|
61
|
+
pendingPreloads.poll().run();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Resolves with a result per source, in order, once all have loaded or
|
|
66
|
+
// failed: { ok, width, height } or { ok: false, error }. Never rejects.
|
|
33
67
|
@ReactMethod
|
|
34
|
-
public void preload(final ReadableArray sources) {
|
|
35
|
-
final
|
|
36
|
-
|
|
37
|
-
activity.runOnUiThread(new Runnable() {
|
|
68
|
+
public void preload(final ReadableArray sources, final Promise promise) {
|
|
69
|
+
final ReactApplicationContext context = getReactApplicationContext();
|
|
70
|
+
UiThreadUtil.runOnUiThread(new Runnable() {
|
|
38
71
|
@Override
|
|
39
72
|
public void run() {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
73
|
+
final int count = sources.size();
|
|
74
|
+
final WritableMap[] results = new WritableMap[count];
|
|
75
|
+
// Sources of this call still to finish. It starts at the
|
|
76
|
+
// number of sources, so the promise can't resolve before
|
|
77
|
+
// they've all been added.
|
|
78
|
+
final int[] remaining = {count};
|
|
79
|
+
final Runnable finishOne = new Runnable() {
|
|
80
|
+
@Override
|
|
81
|
+
public void run() {
|
|
82
|
+
if (--remaining[0] > 0) return;
|
|
83
|
+
WritableArray array = Arguments.createArray();
|
|
84
|
+
for (WritableMap result : results) array.pushMap(result);
|
|
85
|
+
promise.resolve(array);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
for (int i = 0; i < count; i++) {
|
|
89
|
+
final int index = i;
|
|
90
|
+
final ReadableMap source = sources.isNull(i) ? null : sources.getMap(i);
|
|
91
|
+
// Glide throws on an empty url. Invalid sources fail
|
|
92
|
+
// without taking a slot.
|
|
44
93
|
if (!FastImageViewConverter.hasUri(source)) {
|
|
45
|
-
|
|
94
|
+
results[i] = failure("Invalid source: no uri");
|
|
95
|
+
finishOne.run();
|
|
46
96
|
continue;
|
|
47
97
|
}
|
|
48
|
-
final FastImageSource imageSource = FastImageViewConverter.getImageSource(
|
|
49
|
-
//
|
|
50
|
-
//
|
|
98
|
+
final FastImageSource imageSource = FastImageViewConverter.getImageSource(context, source);
|
|
99
|
+
// A uri that can't be resolved (e.g. a relative path) resolves
|
|
100
|
+
// to an empty one.
|
|
51
101
|
if (imageSource.getUri().toString().isEmpty()) {
|
|
52
|
-
|
|
102
|
+
results[i] = failure("Invalid source: can't resolve " + source.getString("uri"));
|
|
103
|
+
finishOne.run();
|
|
53
104
|
continue;
|
|
54
105
|
}
|
|
106
|
+
RequestOptions options = FastImageViewConverter.getOptions(context, imageSource, source);
|
|
107
|
+
// Low priority unless the source sets one, as on iOS (the
|
|
108
|
+
// prefetcher's options), so the images the app shows load
|
|
109
|
+
// first.
|
|
110
|
+
final RequestOptions preloadOptions = source.hasKey("priority") && !source.isNull("priority")
|
|
111
|
+
? options
|
|
112
|
+
: options.priority(Priority.LOW);
|
|
113
|
+
pendingPreloads.add(new Runnable() {
|
|
114
|
+
@Override
|
|
115
|
+
public void run() {
|
|
116
|
+
Glide
|
|
117
|
+
.with(context)
|
|
118
|
+
// Load it the way the view does, so local images
|
|
119
|
+
// (file://, content://, asset:/) work too.
|
|
120
|
+
.load(imageSource.getSourceForLoad())
|
|
121
|
+
.apply(preloadOptions)
|
|
122
|
+
.listener(new RequestListener<Drawable>() {
|
|
123
|
+
@Override
|
|
124
|
+
public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
|
|
125
|
+
results[index] = failure(errorMessage(e));
|
|
126
|
+
preloadsInFlight--;
|
|
127
|
+
finishOne.run();
|
|
128
|
+
startPendingPreloads();
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
55
131
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
132
|
+
@Override
|
|
133
|
+
public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
|
|
134
|
+
// Preloaded at its original size, so this is
|
|
135
|
+
// the image's own size.
|
|
136
|
+
WritableMap result = Arguments.createMap();
|
|
137
|
+
result.putBoolean("ok", true);
|
|
138
|
+
result.putInt("width", resource.getIntrinsicWidth());
|
|
139
|
+
result.putInt("height", resource.getIntrinsicHeight());
|
|
140
|
+
results[index] = result;
|
|
141
|
+
preloadsInFlight--;
|
|
142
|
+
finishOne.run();
|
|
143
|
+
startPendingPreloads();
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
.preload();
|
|
148
|
+
}
|
|
149
|
+
});
|
|
63
150
|
}
|
|
151
|
+
if (count == 0) {
|
|
152
|
+
promise.resolve(Arguments.createArray());
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
startPendingPreloads();
|
|
64
156
|
}
|
|
65
157
|
});
|
|
66
158
|
}
|
|
67
159
|
|
|
160
|
+
private static WritableMap failure(String error) {
|
|
161
|
+
WritableMap result = Arguments.createMap();
|
|
162
|
+
result.putBoolean("ok", false);
|
|
163
|
+
result.putString("error", error);
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// The first root cause's message, e.g. "Not Found, status code: 404".
|
|
168
|
+
private static String errorMessage(@Nullable GlideException e) {
|
|
169
|
+
if (e != null) {
|
|
170
|
+
for (Throwable cause : e.getRootCauses()) {
|
|
171
|
+
if (cause.getMessage() != null) return cause.getMessage();
|
|
172
|
+
}
|
|
173
|
+
if (e.getMessage() != null) return e.getMessage();
|
|
174
|
+
}
|
|
175
|
+
return "Failed to load the image";
|
|
176
|
+
}
|
|
177
|
+
|
|
68
178
|
@ReactMethod
|
|
69
179
|
public void clearMemoryCache(final Promise promise) {
|
|
70
180
|
final Activity activity = getCurrentActivity();
|
package/dist/index.cjs.js
CHANGED
|
@@ -129,7 +129,16 @@ const FastImage = FastImageComponent;
|
|
|
129
129
|
FastImage.resizeMode = resizeMode;
|
|
130
130
|
FastImage.cacheControl = cacheControl;
|
|
131
131
|
FastImage.priority = priority;
|
|
132
|
-
FastImage.preload = (sources) => react_native.NativeModules.FastImageView.preload(sources)
|
|
132
|
+
FastImage.preload = (sources) => Promise.resolve(react_native.NativeModules.FastImageView.preload(sources.map((s) => s || {}))).then((results) => sources.map((source, i) => {
|
|
133
|
+
var _results$i;
|
|
134
|
+
return {
|
|
135
|
+
uri: source ? source.uri : void 0,
|
|
136
|
+
...(_results$i = results === null || results === void 0 ? void 0 : results[i]) !== null && _results$i !== void 0 ? _results$i : {
|
|
137
|
+
ok: false,
|
|
138
|
+
error: "No result"
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
}));
|
|
133
142
|
FastImage.clearMemoryCache = () => react_native.NativeModules.FastImageView.clearMemoryCache();
|
|
134
143
|
FastImage.clearDiskCache = () => react_native.NativeModules.FastImageView.clearDiskCache();
|
|
135
144
|
const styles = react_native.StyleSheet.create({
|
package/dist/index.cjs.js.flow
CHANGED
|
@@ -43,7 +43,17 @@ export type ResizeModes = $Values<ResizeMode>
|
|
|
43
43
|
export type Priorities = $Values<Priority>
|
|
44
44
|
export type CacheControls = $Values<CacheControl>
|
|
45
45
|
|
|
46
|
-
export type
|
|
46
|
+
export type PreloadResult = {|
|
|
47
|
+
uri?: string,
|
|
48
|
+
ok: boolean,
|
|
49
|
+
width?: number,
|
|
50
|
+
height?: number,
|
|
51
|
+
error?: string,
|
|
52
|
+
|}
|
|
53
|
+
|
|
54
|
+
export type PreloadFn = (
|
|
55
|
+
sources: Array<FastImageSource>,
|
|
56
|
+
) => Promise<Array<PreloadResult>>
|
|
47
57
|
export type FastImageSource = {
|
|
48
58
|
uri?: string,
|
|
49
59
|
headers?: Object,
|
package/dist/index.d.ts
CHANGED
|
@@ -93,11 +93,18 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
|
|
|
93
93
|
*/
|
|
94
94
|
children?: React.ReactNode;
|
|
95
95
|
}
|
|
96
|
+
export interface PreloadResult {
|
|
97
|
+
uri?: string;
|
|
98
|
+
ok: boolean;
|
|
99
|
+
width?: number;
|
|
100
|
+
height?: number;
|
|
101
|
+
error?: string;
|
|
102
|
+
}
|
|
96
103
|
export interface FastImageStaticProperties {
|
|
97
104
|
resizeMode: typeof resizeMode;
|
|
98
105
|
priority: typeof priority;
|
|
99
106
|
cacheControl: typeof cacheControl;
|
|
100
|
-
preload: (sources: Source[]) =>
|
|
107
|
+
preload: (sources: Source[]) => Promise<PreloadResult[]>;
|
|
101
108
|
clearMemoryCache: () => Promise<void>;
|
|
102
109
|
clearDiskCache: () => Promise<void>;
|
|
103
110
|
}
|
package/dist/index.js
CHANGED
|
@@ -106,7 +106,16 @@ const FastImage = FastImageComponent;
|
|
|
106
106
|
FastImage.resizeMode = resizeMode;
|
|
107
107
|
FastImage.cacheControl = cacheControl;
|
|
108
108
|
FastImage.priority = priority;
|
|
109
|
-
FastImage.preload = (sources) => NativeModules.FastImageView.preload(sources)
|
|
109
|
+
FastImage.preload = (sources) => Promise.resolve(NativeModules.FastImageView.preload(sources.map((s) => s || {}))).then((results) => sources.map((source, i) => {
|
|
110
|
+
var _results$i;
|
|
111
|
+
return {
|
|
112
|
+
uri: source ? source.uri : void 0,
|
|
113
|
+
...(_results$i = results === null || results === void 0 ? void 0 : results[i]) !== null && _results$i !== void 0 ? _results$i : {
|
|
114
|
+
ok: false,
|
|
115
|
+
error: "No result"
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}));
|
|
110
119
|
FastImage.clearMemoryCache = () => NativeModules.FastImageView.clearMemoryCache();
|
|
111
120
|
FastImage.clearDiskCache = () => NativeModules.FastImageView.clearDiskCache();
|
|
112
121
|
const styles = StyleSheet.create({
|
package/dist/index.js.flow
CHANGED
|
@@ -43,7 +43,17 @@ export type ResizeModes = $Values<ResizeMode>
|
|
|
43
43
|
export type Priorities = $Values<Priority>
|
|
44
44
|
export type CacheControls = $Values<CacheControl>
|
|
45
45
|
|
|
46
|
-
export type
|
|
46
|
+
export type PreloadResult = {|
|
|
47
|
+
uri?: string,
|
|
48
|
+
ok: boolean,
|
|
49
|
+
width?: number,
|
|
50
|
+
height?: number,
|
|
51
|
+
error?: string,
|
|
52
|
+
|}
|
|
53
|
+
|
|
54
|
+
export type PreloadFn = (
|
|
55
|
+
sources: Array<FastImageSource>,
|
|
56
|
+
) => Promise<Array<PreloadResult>>
|
|
47
57
|
export type FastImageSource = {
|
|
48
58
|
uri?: string,
|
|
49
59
|
headers?: Object,
|
|
@@ -21,6 +21,8 @@ typedef NS_ENUM(NSInteger, FFFCacheControl) {
|
|
|
21
21
|
@property (nonatomic) NSURL* url;
|
|
22
22
|
// priority for image request
|
|
23
23
|
@property (nonatomic) FFFPriority priority;
|
|
24
|
+
// whether the source set a priority (preload is low priority otherwise)
|
|
25
|
+
@property (nonatomic) BOOL hasPriority;
|
|
24
26
|
// headers for the image request
|
|
25
27
|
@property (nonatomic) NSDictionary *headers;
|
|
26
28
|
// cache control mode
|
|
@@ -50,10 +50,34 @@ static CFTimeInterval FFFEnteredBackgroundAt = 0;
|
|
|
50
50
|
- (void) setResizeMode: (RCTResizeMode)resizeMode {
|
|
51
51
|
if (_resizeMode != resizeMode) {
|
|
52
52
|
_resizeMode = resizeMode;
|
|
53
|
-
self
|
|
53
|
+
[self updateContentMode];
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
// The content mode for resizeMode. `center` scales an image larger than the
|
|
58
|
+
// view down to fit, as React Native's Image and Android do (#866); only a
|
|
59
|
+
// smaller one is shown at its own size. UIViewContentModeCenter alone showed
|
|
60
|
+
// large images at full size, cropped. So it depends on the image and the
|
|
61
|
+
// view's size.
|
|
62
|
+
- (void) updateContentMode {
|
|
63
|
+
UIViewContentMode contentMode = (UIViewContentMode) _resizeMode;
|
|
64
|
+
if (_resizeMode == RCTResizeModeCenter) {
|
|
65
|
+
CGSize imageSize = super.image.size;
|
|
66
|
+
CGSize viewSize = self.bounds.size;
|
|
67
|
+
if (imageSize.width > viewSize.width || imageSize.height > viewSize.height) {
|
|
68
|
+
contentMode = UIViewContentModeScaleAspectFit;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (self.contentMode != contentMode) {
|
|
72
|
+
self.contentMode = contentMode;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
- (void) layoutSubviews {
|
|
77
|
+
[super layoutSubviews];
|
|
78
|
+
[self updateContentMode];
|
|
79
|
+
}
|
|
80
|
+
|
|
57
81
|
- (void) setOnFastImageLoadEnd: (RCTDirectEventBlock)onFastImageLoadEnd {
|
|
58
82
|
_onFastImageLoadEnd = onFastImageLoadEnd;
|
|
59
83
|
if (self.hasCompleted && _onFastImageLoadEnd) {
|
|
@@ -142,6 +166,7 @@ static CFTimeInterval FFFEnteredBackgroundAt = 0;
|
|
|
142
166
|
self.untintedImage = nil;
|
|
143
167
|
super.image = image;
|
|
144
168
|
}
|
|
169
|
+
[self updateContentMode];
|
|
145
170
|
}
|
|
146
171
|
|
|
147
172
|
- (void) sendOnLoad: (UIImage*)image {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#import "FFFastImageViewManager.h"
|
|
2
2
|
#import "FFFastImageView.h"
|
|
3
|
-
#import <React/RCTLog.h>
|
|
4
3
|
|
|
5
4
|
#import <SDWebImage/SDImageCache.h>
|
|
6
5
|
#import <SDWebImage/SDWebImageManager.h>
|
|
6
|
+
#import <SDWebImage/SDWebImageError.h>
|
|
7
7
|
#import <SDWebImage/SDWebImagePrefetcher.h>
|
|
8
8
|
|
|
9
9
|
@implementation FFFastImageViewManager
|
|
@@ -24,36 +24,119 @@ RCT_EXPORT_VIEW_PROPERTY(onFastImageLoad, RCTDirectEventBlock)
|
|
|
24
24
|
RCT_EXPORT_VIEW_PROPERTY(onFastImageLoadEnd, RCTDirectEventBlock)
|
|
25
25
|
RCT_REMAP_VIEW_PROPERTY(tintColor, imageColor, UIColor)
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
// The error's description, with the HTTP status code when there is one.
|
|
28
|
+
static NSString *FFFErrorMessage(NSError *error)
|
|
28
29
|
{
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
NSNumber *statusCode = error.userInfo[SDWebImageErrorDownloadStatusCodeKey];
|
|
31
|
+
if (statusCode) {
|
|
32
|
+
return [NSString stringWithFormat:@"%@, status code: %@", error.localizedDescription, statusCode];
|
|
33
|
+
}
|
|
34
|
+
return error.localizedDescription ?: @"Failed to load the image";
|
|
35
|
+
}
|
|
31
36
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
// Preloads waiting to start, in the order they were added, and the number
|
|
38
|
+
// loading, across all preload calls. Like SDWebImagePrefetcher, at most
|
|
39
|
+
// maxConcurrentPrefetchCount load at a time, so a long list doesn't queue
|
|
40
|
+
// hundreds of operations ahead of the images the app is showing. Only used on
|
|
41
|
+
// the main queue (SDWebImageManager calls its completion blocks there).
|
|
42
|
+
static NSMutableArray<dispatch_block_t> *FFFPendingPreloads;
|
|
43
|
+
static NSUInteger FFFPreloadsInFlight;
|
|
44
|
+
|
|
45
|
+
static NSUInteger FFFPreloadLimit(void)
|
|
46
|
+
{
|
|
47
|
+
NSUInteger limit = [SDWebImagePrefetcher sharedImagePrefetcher].maxConcurrentPrefetchCount;
|
|
48
|
+
return MAX(limit, (NSUInteger)1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Starts pending preloads while there's room. A completion block calls it
|
|
52
|
+
// again, sometimes from inside start() (a memory cache hit completes
|
|
53
|
+
// synchronously on the main queue). That's fine: the counters are shared and
|
|
54
|
+
// updated before each start(), so the inner call starts what fits, and the
|
|
55
|
+
// outer loop sees the updated counters when it checks again.
|
|
56
|
+
static void FFFStartPendingPreloads(void)
|
|
57
|
+
{
|
|
58
|
+
while (FFFPreloadsInFlight < FFFPreloadLimit() && FFFPendingPreloads.count > 0) {
|
|
59
|
+
dispatch_block_t start = FFFPendingPreloads.firstObject;
|
|
60
|
+
[FFFPendingPreloads removeObjectAtIndex:0];
|
|
61
|
+
FFFPreloadsInFlight++;
|
|
62
|
+
start();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Resolves with a result per source, in order, once all have loaded or failed:
|
|
67
|
+
// { ok, width, height } or { ok: false, error }. Never rejects.
|
|
68
|
+
RCT_EXPORT_METHOD(preload:(nonnull NSArray<FFFastImageSource *> *)sources
|
|
69
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
70
|
+
reject:(__unused RCTPromiseRejectBlock)reject)
|
|
71
|
+
{
|
|
72
|
+
// This runs on the UIManager queue; the queue state is only touched on
|
|
73
|
+
// the main queue.
|
|
74
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
75
|
+
if (!FFFPendingPreloads) {
|
|
76
|
+
FFFPendingPreloads = [NSMutableArray array];
|
|
39
77
|
}
|
|
40
|
-
|
|
41
|
-
|
|
78
|
+
NSMutableArray *results = [NSMutableArray arrayWithCapacity:sources.count];
|
|
79
|
+
// Sources of this call still to finish. It starts at the number of
|
|
80
|
+
// sources, so the promise can't resolve before they've all been added.
|
|
81
|
+
__block NSUInteger remaining = sources.count;
|
|
82
|
+
void (^finishOne)(void) = ^{
|
|
83
|
+
if (--remaining == 0) {
|
|
84
|
+
resolve(results);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
// With the prefetcher's options (low priority), but one source at a
|
|
88
|
+
// time (not SDWebImagePrefetcher), to get each source's result and to
|
|
89
|
+
// send its headers with its own request only.
|
|
90
|
+
SDWebImageOptions prefetcherOptions = [SDWebImagePrefetcher sharedImagePrefetcher].options;
|
|
91
|
+
|
|
92
|
+
[sources enumerateObjectsUsingBlock:^(FFFastImageSource * _Nonnull source, NSUInteger idx, BOOL * _Nonnull stop) {
|
|
93
|
+
if (!source.url) {
|
|
94
|
+
// An empty, missing or null uri (JS sends null sources as {}).
|
|
95
|
+
// It fails without taking a slot.
|
|
96
|
+
[results addObject:@{@"ok": @NO, @"error": @"Invalid source: no uri"}];
|
|
97
|
+
finishOne();
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
[results addObject:[NSNull null]];
|
|
101
|
+
// A source's own priority replaces the prefetcher's, as on Android.
|
|
102
|
+
SDWebImageOptions options = prefetcherOptions;
|
|
103
|
+
if (source.hasPriority) {
|
|
104
|
+
options &= ~(SDWebImageLowPriority | SDWebImageHighPriority);
|
|
105
|
+
if (source.priority == FFFPriorityLow) {
|
|
106
|
+
options |= SDWebImageLowPriority;
|
|
107
|
+
} else if (source.priority == FFFPriorityHigh) {
|
|
108
|
+
options |= SDWebImageHighPriority;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
[FFFPendingPreloads addObject:[^{
|
|
112
|
+
// Once per slot: SDWebImage calls this once with finished set
|
|
113
|
+
// (a progressive load calls it more, without).
|
|
114
|
+
__block BOOL done = NO;
|
|
115
|
+
[[SDWebImageManager sharedManager] loadImageWithURL:source.url
|
|
116
|
+
options:options
|
|
117
|
+
context:@{SDWebImageContextDownloadRequestModifier: source.requestModifier}
|
|
118
|
+
progress:nil
|
|
119
|
+
completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
|
|
120
|
+
if (!finished || done) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
done = YES;
|
|
124
|
+
results[idx] = image
|
|
125
|
+
? @{@"ok": @YES, @"width": @(image.size.width), @"height": @(image.size.height)}
|
|
126
|
+
: @{@"ok": @NO, @"error": FFFErrorMessage(error)};
|
|
127
|
+
FFFPreloadsInFlight--;
|
|
128
|
+
finishOne();
|
|
129
|
+
FFFStartPendingPreloads();
|
|
130
|
+
}];
|
|
131
|
+
} copy]];
|
|
132
|
+
}];
|
|
133
|
+
|
|
134
|
+
if (sources.count == 0) {
|
|
135
|
+
resolve(results);
|
|
42
136
|
return;
|
|
43
137
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
// prefetcher can't take headers per request in SDWebImage 5.11 (its
|
|
47
|
-
// context is shared by all prefetches), so load it the way the
|
|
48
|
-
// prefetcher does, with the same options.
|
|
49
|
-
[[SDWebImageManager sharedManager] loadImageWithURL:source.url
|
|
50
|
-
options:prefetcher.options
|
|
51
|
-
context:@{SDWebImageContextDownloadRequestModifier: source.requestModifier}
|
|
52
|
-
progress:nil
|
|
53
|
-
completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {}];
|
|
54
|
-
}];
|
|
55
|
-
|
|
56
|
-
[prefetcher prefetchURLs:urls];
|
|
138
|
+
FFFStartPendingPreloads();
|
|
139
|
+
});
|
|
57
140
|
}
|
|
58
141
|
|
|
59
142
|
RCT_EXPORT_METHOD(clearMemoryCache:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
|
|
@@ -45,6 +45,8 @@ RCT_ENUM_CONVERTER(FFFCacheControl, (@{
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
FFFastImageSource *imageSource = [[FFFastImageSource alloc] initWithURL:uri priority:priority headers:headers cacheControl:cacheControl];
|
|
48
|
+
id priorityJSON = json[@"priority"];
|
|
49
|
+
imageSource.hasPriority = priorityJSON && priorityJSON != (id)kCFNull;
|
|
48
50
|
|
|
49
51
|
return imageSource;
|
|
50
52
|
}
|