react-native-fast-image 8.6.43 → 8.8.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 +21 -1
- package/android/src/main/java/com/dylanvann/fastimage/FastImageRequestListener.java +12 -1
- package/android/src/main/java/com/dylanvann/fastimage/FastImageViewManager.java +5 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageViewModule.java +134 -24
- package/android/src/main/java/com/dylanvann/fastimage/FastImageViewWithUrl.java +151 -2
- package/dist/index.cjs.js +10 -1
- package/dist/index.cjs.js.flow +12 -1
- package/dist/index.d.ts +17 -1
- package/dist/index.js +10 -1
- package/dist/index.js.flow +12 -1
- package/ios/FastImage/FFFastImageSource.h +2 -0
- package/ios/FastImage/FFFastImageView.h +3 -0
- package/ios/FastImage/FFFastImageView.m +32 -0
- package/ios/FastImage/FFFastImageViewManager.m +110 -26
- package/ios/FastImage/RCTConvert+FFFastImage.m +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -112,6 +112,8 @@ If you use Proguard you will need to add these lines to `android/app/proguard-ru
|
|
|
112
112
|
|
|
113
113
|
Source for the remote image to load.
|
|
114
114
|
|
|
115
|
+
When `source` changes, the image that's showing stays until the new one has loaded, as with `<img>` in browsers and React Native's `Image` on iOS. In views that get reused for other content, such as rows in FlashList or recyclerlistview, set `recyclingKey` so a reused row doesn't show the previous row's image.
|
|
116
|
+
|
|
115
117
|
---
|
|
116
118
|
|
|
117
119
|
### `source.uri?: string`
|
|
@@ -147,10 +149,21 @@ Indicates the load order priority of an image. Images with `FastImage.priority.h
|
|
|
147
149
|
### `defaultSource?: number`
|
|
148
150
|
|
|
149
151
|
- An asset loaded with `require(...)`.
|
|
152
|
+
- Shown while the first image loads, and if an image fails to load. When `source` changes, the previous image shows while the new one loads instead (see `source` and `recyclingKey`).
|
|
150
153
|
- Note that like the built-in `Image` implementation, on Android `defaultSource` does not work in debug mode. This is due to the fact that assets are sent from the dev server, but RN's functions only know how to load it from `res`.
|
|
151
154
|
|
|
152
155
|
---
|
|
153
156
|
|
|
157
|
+
### `recyclingKey?: string`
|
|
158
|
+
|
|
159
|
+
For views that get reused for other content, such as rows in FlashList or recyclerlistview. Set it to something that identifies the content, e.g. the item's id. When it changes, the image is cleared right away (to `defaultSource`, or blank) instead of staying until the new one has loaded. Unlike changing `key`, this keeps the view, which is what list recycling is for.
|
|
160
|
+
|
|
161
|
+
```jsx
|
|
162
|
+
<FastImage recyclingKey={item.id} source={{ uri: item.imageUrl }} />
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
154
167
|
### `resizeMode?: enum`
|
|
155
168
|
|
|
156
169
|
- `FastImage.resizeMode.contain` - Scale the image uniformly (maintain the image's aspect ratio) so that both dimensions (width and height) of the image will be equal to or less than the corresponding dimension of the view (minus padding).
|
|
@@ -213,7 +226,7 @@ If supplied, changes the color of all the non-transparent pixels to the given co
|
|
|
213
226
|
|
|
214
227
|
## Static Methods
|
|
215
228
|
|
|
216
|
-
### `FastImage.preload: (source[]) =>
|
|
229
|
+
### `FastImage.preload: (source[]) => Promise<result[]>`
|
|
217
230
|
|
|
218
231
|
Preload images to display later. e.g.
|
|
219
232
|
|
|
@@ -230,6 +243,13 @@ FastImage.preload([
|
|
|
230
243
|
])
|
|
231
244
|
```
|
|
232
245
|
|
|
246
|
+
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:
|
|
247
|
+
|
|
248
|
+
```js
|
|
249
|
+
const results = await FastImage.preload(sources)
|
|
250
|
+
const failed = results.filter((result) => !result.ok)
|
|
251
|
+
```
|
|
252
|
+
|
|
233
253
|
### `FastImage.clearMemoryCache: () => Promise<void>`
|
|
234
254
|
|
|
235
255
|
Clear all images from memory cache.
|
|
@@ -19,19 +19,28 @@ public class FastImageRequestListener implements RequestListener<Drawable> {
|
|
|
19
19
|
static final String REACT_ON_LOAD_END_EVENT = "onFastImageLoadEnd";
|
|
20
20
|
private final String key;
|
|
21
21
|
private final FastImageSource source;
|
|
22
|
+
// Whether the request shows the previous image as a thumbnail meanwhile.
|
|
23
|
+
private final boolean thumbnail;
|
|
24
|
+
// False when loading the image that's showing again at a new size: the
|
|
25
|
+
// view shows it at its old size if that fails.
|
|
26
|
+
private final boolean events;
|
|
22
27
|
|
|
23
|
-
FastImageRequestListener(String key, FastImageSource source) {
|
|
28
|
+
FastImageRequestListener(String key, FastImageSource source, boolean thumbnail, boolean events) {
|
|
24
29
|
this.key = key;
|
|
25
30
|
this.source = source;
|
|
31
|
+
this.thumbnail = thumbnail;
|
|
32
|
+
this.events = events;
|
|
26
33
|
}
|
|
27
34
|
|
|
28
35
|
@Override
|
|
29
36
|
public boolean onLoadFailed(@androidx.annotation.Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
|
|
37
|
+
if (!events) return false;
|
|
30
38
|
FastImageOkHttpProgressGlideModule.forget(key);
|
|
31
39
|
if (!(target instanceof ImageViewTarget)) {
|
|
32
40
|
return false;
|
|
33
41
|
}
|
|
34
42
|
FastImageViewWithUrl view = (FastImageViewWithUrl) ((ImageViewTarget) target).getView();
|
|
43
|
+
view.onImageFailed(thumbnail);
|
|
35
44
|
FastImageEvents.send(view, REACT_ON_ERROR_EVENT);
|
|
36
45
|
FastImageEvents.send(view, REACT_ON_LOAD_END_EVENT);
|
|
37
46
|
return false;
|
|
@@ -43,11 +52,13 @@ public class FastImageRequestListener implements RequestListener<Drawable> {
|
|
|
43
52
|
return false;
|
|
44
53
|
}
|
|
45
54
|
final FastImageViewWithUrl view = (FastImageViewWithUrl) ((ImageViewTarget) target).getView();
|
|
55
|
+
view.onImageLoaded();
|
|
46
56
|
if (resource instanceof GifDrawable) {
|
|
47
57
|
// Play the GIF as many times as the file says, as iOS does. Glide
|
|
48
58
|
// loops every GIF forever by default (#651).
|
|
49
59
|
((GifDrawable) resource).setLoopCount(GifDrawable.LOOP_INTRINSIC);
|
|
50
60
|
}
|
|
61
|
+
if (!events) return false;
|
|
51
62
|
boolean local = !(model instanceof GlideUrl);
|
|
52
63
|
int[] size = FastImageSourceSize.get(resource, model, local,
|
|
53
64
|
dataSource == DataSource.RESOURCE_DISK_CACHE);
|
|
@@ -84,6 +84,11 @@ class FastImageViewManager extends SimpleViewManager<FastImageViewWithUrl> imple
|
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
@ReactProp(name = "recyclingKey")
|
|
88
|
+
public void setRecyclingKey(FastImageViewWithUrl view, @Nullable String recyclingKey) {
|
|
89
|
+
view.setRecyclingKey(recyclingKey);
|
|
90
|
+
}
|
|
91
|
+
|
|
87
92
|
@ReactProp(name = "resizeMode")
|
|
88
93
|
public void setResizeMode(FastImageViewWithUrl view, String resizeMode) {
|
|
89
94
|
final FastImageViewWithUrl.ScaleType scaleType = FastImageViewConverter.getScaleType(resizeMode);
|
|
@@ -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();
|
|
@@ -15,6 +15,8 @@ import com.bumptech.glide.RequestBuilder;
|
|
|
15
15
|
import com.bumptech.glide.RequestManager;
|
|
16
16
|
import com.bumptech.glide.load.model.GlideUrl;
|
|
17
17
|
import com.bumptech.glide.request.Request;
|
|
18
|
+
import com.bumptech.glide.request.target.DrawableImageViewTarget;
|
|
19
|
+
import com.bumptech.glide.request.target.SizeReadyCallback;
|
|
18
20
|
import com.facebook.react.bridge.ReadableMap;
|
|
19
21
|
import com.facebook.react.bridge.WritableMap;
|
|
20
22
|
import com.facebook.react.bridge.WritableNativeMap;
|
|
@@ -67,6 +69,143 @@ class FastImageViewWithUrl extends AppCompatImageView {
|
|
|
67
69
|
mNeedsReload = true;
|
|
68
70
|
}
|
|
69
71
|
|
|
72
|
+
// The request for the image the view shows once it has loaded, and the one
|
|
73
|
+
// loading. A new source starts with the shown one as a thumbnail, from the
|
|
74
|
+
// cache only, so the image stays until the new one has loaded instead of
|
|
75
|
+
// flashing blank (#747). This is Glide's safe way to do that: the previous
|
|
76
|
+
// bitmap can't be kept in the view itself, since Glide reuses it once its
|
|
77
|
+
// request is cleared.
|
|
78
|
+
@Nullable
|
|
79
|
+
private RequestBuilder<Drawable> mShownRequest;
|
|
80
|
+
@Nullable
|
|
81
|
+
private RequestBuilder<Drawable> mLoadingRequest;
|
|
82
|
+
// Counts loads, to tell whether a posted update is for the current one.
|
|
83
|
+
private int mLoadCount = 0;
|
|
84
|
+
// The size Glide loaded the shown image at, and is loading the loading
|
|
85
|
+
// one at (0 until it has one).
|
|
86
|
+
private int mShownWidth = 0;
|
|
87
|
+
private int mShownHeight = 0;
|
|
88
|
+
private int mLoadingWidth = 0;
|
|
89
|
+
private int mLoadingHeight = 0;
|
|
90
|
+
private boolean mResizeReloadPosted = false;
|
|
91
|
+
|
|
92
|
+
@Nullable
|
|
93
|
+
private String mRecyclingKey;
|
|
94
|
+
|
|
95
|
+
// When it changes, the next image doesn't replace the current one: the
|
|
96
|
+
// view clears first (for views reused for other content, like list rows).
|
|
97
|
+
void setRecyclingKey(@Nullable String recyclingKey) {
|
|
98
|
+
if (recyclingKey == null ? mRecyclingKey == null : recyclingKey.equals(mRecyclingKey)) return;
|
|
99
|
+
boolean changed = mRecyclingKey != null;
|
|
100
|
+
mRecyclingKey = recyclingKey;
|
|
101
|
+
if (changed) {
|
|
102
|
+
// No thumbnail of the current image, so Glide clears the view to
|
|
103
|
+
// defaultSource (or nothing) while the next one loads. Reload even
|
|
104
|
+
// if the source is the same.
|
|
105
|
+
mShownRequest = null;
|
|
106
|
+
mNeedsReload = true;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// The loading image loaded (FastImageRequestListener).
|
|
111
|
+
void onImageLoaded() {
|
|
112
|
+
mShownRequest = mLoadingRequest;
|
|
113
|
+
mShownWidth = mLoadingWidth;
|
|
114
|
+
mShownHeight = mLoadingHeight;
|
|
115
|
+
reloadIfResized();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Glide crops or scales the image to the view's size when it loads, so if
|
|
119
|
+
// the view's size changes after that, load it again at the new size.
|
|
120
|
+
// Otherwise a view that gets taller shows a zoomed-in slice of it (#983).
|
|
121
|
+
@Override
|
|
122
|
+
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
|
123
|
+
super.onSizeChanged(w, h, oldw, oldh);
|
|
124
|
+
reloadIfResized();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private void reloadIfResized() {
|
|
128
|
+
if (mResizeReloadPosted || !isResized()) return;
|
|
129
|
+
mResizeReloadPosted = true;
|
|
130
|
+
// After layout (once per frame), and not from within Glide's callback.
|
|
131
|
+
post(new Runnable() {
|
|
132
|
+
@Override
|
|
133
|
+
public void run() {
|
|
134
|
+
mResizeReloadPosted = false;
|
|
135
|
+
if (isResized()) reloadForSize();
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Whether the image loaded, at a size the view doesn't have now.
|
|
141
|
+
private boolean isResized() {
|
|
142
|
+
int width = getWidth() - getPaddingLeft() - getPaddingRight();
|
|
143
|
+
int height = getHeight() - getPaddingTop() - getPaddingBottom();
|
|
144
|
+
return requestManager != null && !mNeedsReload
|
|
145
|
+
&& mShownRequest != null && mShownRequest == mLoadingRequest
|
|
146
|
+
&& mShownWidth > 0 && mShownHeight > 0 && width > 0 && height > 0
|
|
147
|
+
&& (width != mShownWidth || height != mShownHeight);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// The same image at the view's new size. Meanwhile, and if that fails, the
|
|
151
|
+
// view shows the image at its old size, from the cache. It's the image
|
|
152
|
+
// that's already showing, so this sends no events.
|
|
153
|
+
@SuppressLint("CheckResult")
|
|
154
|
+
private void reloadForSize() {
|
|
155
|
+
RequestBuilder<Drawable> shown = mShownRequest;
|
|
156
|
+
RequestBuilder<Drawable> current = fromCache(shown);
|
|
157
|
+
mLoadCount++;
|
|
158
|
+
clearView(requestManager);
|
|
159
|
+
into(shown, shown.clone()
|
|
160
|
+
.thumbnail(current)
|
|
161
|
+
.error(current.clone())
|
|
162
|
+
.listener(new FastImageRequestListener(null, null, true, false)));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// The shown image, from the cache only (never loaded again), at the size
|
|
166
|
+
// it was loaded at: the key it's in Glide's memory cache under.
|
|
167
|
+
@SuppressLint("CheckResult")
|
|
168
|
+
private RequestBuilder<Drawable> fromCache(RequestBuilder<Drawable> shown) {
|
|
169
|
+
RequestBuilder<Drawable> request = shown.clone().onlyRetrieveFromCache(true);
|
|
170
|
+
if (mShownWidth > 0 && mShownHeight > 0) request = request.override(mShownWidth, mShownHeight);
|
|
171
|
+
return request;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Starts loading the request (built by builder), recording the size Glide
|
|
175
|
+
// loads it at. The size callback is added first, so it has the size by
|
|
176
|
+
// the time the image loads, even from the memory cache.
|
|
177
|
+
private void into(RequestBuilder<Drawable> request, RequestBuilder<Drawable> builder) {
|
|
178
|
+
final int load = mLoadCount;
|
|
179
|
+
mLoadingRequest = request;
|
|
180
|
+
mLoadingWidth = 0;
|
|
181
|
+
mLoadingHeight = 0;
|
|
182
|
+
DrawableImageViewTarget target = new DrawableImageViewTarget(this);
|
|
183
|
+
target.getSize(new SizeReadyCallback() {
|
|
184
|
+
@Override
|
|
185
|
+
public void onSizeReady(int width, int height) {
|
|
186
|
+
if (load != mLoadCount) return;
|
|
187
|
+
mLoadingWidth = width;
|
|
188
|
+
mLoadingHeight = height;
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
builder.into(target);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// The loading image failed (FastImageRequestListener). Glide shows
|
|
195
|
+
// defaultSource then, but not over a thumbnail: show it here, as iOS does.
|
|
196
|
+
void onImageFailed(boolean hadThumbnail) {
|
|
197
|
+
mShownRequest = null;
|
|
198
|
+
if (!hadThumbnail) return;
|
|
199
|
+
final int load = mLoadCount;
|
|
200
|
+
// Not from within Glide's callback.
|
|
201
|
+
post(new Runnable() {
|
|
202
|
+
@Override
|
|
203
|
+
public void run() {
|
|
204
|
+
if (load == mLoadCount) setImageDrawable(mDefaultSource);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
70
209
|
@SuppressLint("CheckResult")
|
|
71
210
|
public void onAfterUpdate(
|
|
72
211
|
@Nonnull FastImageViewManager manager,
|
|
@@ -77,6 +216,10 @@ class FastImageViewWithUrl extends AppCompatImageView {
|
|
|
77
216
|
// Only reload for changes that affect the request (source,
|
|
78
217
|
// defaultSource, resizeMode), not for every prop update.
|
|
79
218
|
mNeedsReload = false;
|
|
219
|
+
mLoadCount++;
|
|
220
|
+
RequestBuilder<Drawable> shownRequest = mShownRequest;
|
|
221
|
+
mShownRequest = null;
|
|
222
|
+
mLoadingRequest = null;
|
|
80
223
|
|
|
81
224
|
// Nothing to show.
|
|
82
225
|
if (mSource == null && mDefaultSource == null) {
|
|
@@ -162,11 +305,17 @@ class FastImageViewWithUrl extends AppCompatImageView {
|
|
|
162
305
|
// What into() would apply for the scale type, with
|
|
163
306
|
// the size capture.
|
|
164
307
|
.apply(FastImageSourceSize.scaleTypeOptions(getScaleType(), capture));
|
|
308
|
+
RequestBuilder<Drawable> request = builder.clone();
|
|
309
|
+
|
|
310
|
+
boolean thumbnail = shownRequest != null && model != null;
|
|
311
|
+
if (thumbnail) {
|
|
312
|
+
builder = builder.thumbnail(fromCache(shownRequest));
|
|
313
|
+
}
|
|
165
314
|
|
|
166
315
|
if (key != null)
|
|
167
|
-
builder.listener(new FastImageRequestListener(key, imageSource));
|
|
316
|
+
builder.listener(new FastImageRequestListener(key, imageSource, thumbnail, true));
|
|
168
317
|
|
|
169
|
-
|
|
318
|
+
into(request, builder);
|
|
170
319
|
}
|
|
171
320
|
}
|
|
172
321
|
|
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,
|
|
@@ -65,6 +75,7 @@ export type FastImageProps = $ReadOnly<{|
|
|
|
65
75
|
tintColor?: number | string,
|
|
66
76
|
resizeMode?: ?ResizeModes,
|
|
67
77
|
fallback?: ?boolean,
|
|
78
|
+
recyclingKey?: ?string,
|
|
68
79
|
testID?: ?string,
|
|
69
80
|
|}>
|
|
70
81
|
|
package/dist/index.d.ts
CHANGED
|
@@ -60,6 +60,15 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
|
|
|
60
60
|
defaultSource?: ImageRequireSource;
|
|
61
61
|
resizeMode?: ResizeMode;
|
|
62
62
|
fallback?: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* When `source` changes, the image showing stays until the new one has
|
|
65
|
+
* loaded. For views that get reused for other content, such as rows in
|
|
66
|
+
* FlashList or recyclerlistview, set this to something that identifies the
|
|
67
|
+
* content (e.g. the item's id): when it changes, the image is cleared
|
|
68
|
+
* right away instead, so a reused row doesn't show the previous row's
|
|
69
|
+
* image. Unlike changing `key`, the view is kept.
|
|
70
|
+
*/
|
|
71
|
+
recyclingKey?: string | null;
|
|
63
72
|
onLoadStart?(): void;
|
|
64
73
|
onProgress?(event: OnProgressEvent): void;
|
|
65
74
|
onLoad?(event: OnLoadEvent): void;
|
|
@@ -93,11 +102,18 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
|
|
|
93
102
|
*/
|
|
94
103
|
children?: React.ReactNode;
|
|
95
104
|
}
|
|
105
|
+
export interface PreloadResult {
|
|
106
|
+
uri?: string;
|
|
107
|
+
ok: boolean;
|
|
108
|
+
width?: number;
|
|
109
|
+
height?: number;
|
|
110
|
+
error?: string;
|
|
111
|
+
}
|
|
96
112
|
export interface FastImageStaticProperties {
|
|
97
113
|
resizeMode: typeof resizeMode;
|
|
98
114
|
priority: typeof priority;
|
|
99
115
|
cacheControl: typeof cacheControl;
|
|
100
|
-
preload: (sources: Source[]) =>
|
|
116
|
+
preload: (sources: Source[]) => Promise<PreloadResult[]>;
|
|
101
117
|
clearMemoryCache: () => Promise<void>;
|
|
102
118
|
clearDiskCache: () => Promise<void>;
|
|
103
119
|
}
|
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,
|
|
@@ -65,6 +75,7 @@ export type FastImageProps = $ReadOnly<{|
|
|
|
65
75
|
tintColor?: number | string,
|
|
66
76
|
resizeMode?: ?ResizeModes,
|
|
67
77
|
fallback?: ?boolean,
|
|
78
|
+
recyclingKey?: ?string,
|
|
68
79
|
testID?: ?string,
|
|
69
80
|
|}>
|
|
70
81
|
|
|
@@ -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
|
|
@@ -19,6 +19,9 @@
|
|
|
19
19
|
@property (nonatomic, strong) FFFastImageSource *source;
|
|
20
20
|
@property (nonatomic, strong) UIImage *defaultSource;
|
|
21
21
|
@property (nonatomic, strong) UIColor *imageColor;
|
|
22
|
+
// When it changes, the next image doesn't replace the current one: the view
|
|
23
|
+
// clears first (for views reused for other content, like list rows).
|
|
24
|
+
@property (nonatomic, copy) NSString *recyclingKey;
|
|
22
25
|
|
|
23
26
|
@end
|
|
24
27
|
|
|
@@ -20,6 +20,9 @@
|
|
|
20
20
|
@property(nonatomic, assign) BOOL retriedAfterBackground;
|
|
21
21
|
// Waits for the app to be active again, to restart a load.
|
|
22
22
|
@property(nonatomic, strong) id activeObserver;
|
|
23
|
+
// Whether the view shows an image that loaded (not defaultSource or nothing).
|
|
24
|
+
// A new source then keeps it until the new image has loaded (see reloadImage).
|
|
25
|
+
@property(nonatomic, assign) BOOL showsLoadedImage;
|
|
23
26
|
|
|
24
27
|
@end
|
|
25
28
|
|
|
@@ -186,6 +189,21 @@ static CFTimeInterval FFFEnteredBackgroundAt = 0;
|
|
|
186
189
|
}
|
|
187
190
|
}
|
|
188
191
|
|
|
192
|
+
- (void) setRecyclingKey: (NSString*)recyclingKey {
|
|
193
|
+
if (_recyclingKey == recyclingKey || [_recyclingKey isEqualToString: recyclingKey]) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
BOOL changed = _recyclingKey != nil;
|
|
197
|
+
_recyclingKey = [recyclingKey copy];
|
|
198
|
+
if (changed) {
|
|
199
|
+
// The view shows other content now: don't keep the current image
|
|
200
|
+
// while the next one loads (reloadImage clears it), even if the
|
|
201
|
+
// source is the same.
|
|
202
|
+
self.showsLoadedImage = NO;
|
|
203
|
+
_needsReload = YES;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
189
207
|
- (void) setDefaultSource: (UIImage*)defaultSource {
|
|
190
208
|
if (_defaultSource != defaultSource) {
|
|
191
209
|
_defaultSource = defaultSource;
|
|
@@ -217,6 +235,7 @@ static CFTimeInterval FFFEnteredBackgroundAt = 0;
|
|
|
217
235
|
if (!image) {
|
|
218
236
|
// Not decodable: fail like a remote image, showing defaultSource.
|
|
219
237
|
[self setImage: _defaultSource];
|
|
238
|
+
self.showsLoadedImage = NO;
|
|
220
239
|
self.hasErrored = YES;
|
|
221
240
|
if (self.onFastImageError) {
|
|
222
241
|
self.onFastImageError(@{});
|
|
@@ -234,6 +253,7 @@ static CFTimeInterval FFFEnteredBackgroundAt = 0;
|
|
|
234
253
|
});
|
|
235
254
|
}
|
|
236
255
|
self.hasCompleted = YES;
|
|
256
|
+
self.showsLoadedImage = YES;
|
|
237
257
|
[self sendOnLoad: image];
|
|
238
258
|
|
|
239
259
|
if (self.onFastImageLoadEnd) {
|
|
@@ -270,6 +290,14 @@ static CFTimeInterval FFFEnteredBackgroundAt = 0;
|
|
|
270
290
|
break;
|
|
271
291
|
}
|
|
272
292
|
|
|
293
|
+
// Keep showing the loaded image until the new one has loaded, instead
|
|
294
|
+
// of clearing it to defaultSource (or nothing) while it loads, which
|
|
295
|
+
// flashed (#747). defaultSource shows if the new image fails. As React
|
|
296
|
+
// Native's Image does; a `key` that changes starts from blank instead.
|
|
297
|
+
if (self.showsLoadedImage) {
|
|
298
|
+
options |= SDWebImageDelayPlaceholder;
|
|
299
|
+
}
|
|
300
|
+
|
|
273
301
|
if (self.onFastImageLoadStart) {
|
|
274
302
|
self.onFastImageLoadStart(@{});
|
|
275
303
|
self.hasSentOnLoadStart = YES;
|
|
@@ -284,6 +312,7 @@ static CFTimeInterval FFFEnteredBackgroundAt = 0;
|
|
|
284
312
|
[self downloadImage: _source options: options context: context];
|
|
285
313
|
} else if (_defaultSource) {
|
|
286
314
|
[self setImage: _defaultSource];
|
|
315
|
+
self.showsLoadedImage = NO;
|
|
287
316
|
}
|
|
288
317
|
}
|
|
289
318
|
|
|
@@ -338,6 +367,8 @@ static CFTimeInterval FFFEnteredBackgroundAt = 0;
|
|
|
338
367
|
return;
|
|
339
368
|
}
|
|
340
369
|
if (error) {
|
|
370
|
+
// SDWebImage shows the placeholder (defaultSource or nothing).
|
|
371
|
+
weakSelf.showsLoadedImage = NO;
|
|
341
372
|
weakSelf.hasErrored = YES;
|
|
342
373
|
if (weakSelf.onFastImageError) {
|
|
343
374
|
weakSelf.onFastImageError(@{});
|
|
@@ -347,6 +378,7 @@ static CFTimeInterval FFFEnteredBackgroundAt = 0;
|
|
|
347
378
|
}
|
|
348
379
|
} else {
|
|
349
380
|
weakSelf.hasCompleted = YES;
|
|
381
|
+
weakSelf.showsLoadedImage = YES;
|
|
350
382
|
[weakSelf sendOnLoad: image];
|
|
351
383
|
if (weakSelf.onFastImageLoadEnd) {
|
|
352
384
|
weakSelf.onFastImageLoadEnd(@{});
|
|
@@ -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
|
|
@@ -17,6 +17,7 @@ RCT_EXPORT_MODULE(FastImageView)
|
|
|
17
17
|
RCT_EXPORT_VIEW_PROPERTY(source, FFFastImageSource)
|
|
18
18
|
RCT_EXPORT_VIEW_PROPERTY(defaultSource, UIImage)
|
|
19
19
|
RCT_EXPORT_VIEW_PROPERTY(resizeMode, RCTResizeMode)
|
|
20
|
+
RCT_EXPORT_VIEW_PROPERTY(recyclingKey, NSString)
|
|
20
21
|
RCT_EXPORT_VIEW_PROPERTY(onFastImageLoadStart, RCTDirectEventBlock)
|
|
21
22
|
RCT_EXPORT_VIEW_PROPERTY(onFastImageProgress, RCTDirectEventBlock)
|
|
22
23
|
RCT_EXPORT_VIEW_PROPERTY(onFastImageError, RCTDirectEventBlock)
|
|
@@ -24,36 +25,119 @@ RCT_EXPORT_VIEW_PROPERTY(onFastImageLoad, RCTDirectEventBlock)
|
|
|
24
25
|
RCT_EXPORT_VIEW_PROPERTY(onFastImageLoadEnd, RCTDirectEventBlock)
|
|
25
26
|
RCT_REMAP_VIEW_PROPERTY(tintColor, imageColor, UIColor)
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
// The error's description, with the HTTP status code when there is one.
|
|
29
|
+
static NSString *FFFErrorMessage(NSError *error)
|
|
28
30
|
{
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
NSNumber *statusCode = error.userInfo[SDWebImageErrorDownloadStatusCodeKey];
|
|
32
|
+
if (statusCode) {
|
|
33
|
+
return [NSString stringWithFormat:@"%@, status code: %@", error.localizedDescription, statusCode];
|
|
34
|
+
}
|
|
35
|
+
return error.localizedDescription ?: @"Failed to load the image";
|
|
36
|
+
}
|
|
31
37
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
// Preloads waiting to start, in the order they were added, and the number
|
|
39
|
+
// loading, across all preload calls. Like SDWebImagePrefetcher, at most
|
|
40
|
+
// maxConcurrentPrefetchCount load at a time, so a long list doesn't queue
|
|
41
|
+
// hundreds of operations ahead of the images the app is showing. Only used on
|
|
42
|
+
// the main queue (SDWebImageManager calls its completion blocks there).
|
|
43
|
+
static NSMutableArray<dispatch_block_t> *FFFPendingPreloads;
|
|
44
|
+
static NSUInteger FFFPreloadsInFlight;
|
|
45
|
+
|
|
46
|
+
static NSUInteger FFFPreloadLimit(void)
|
|
47
|
+
{
|
|
48
|
+
NSUInteger limit = [SDWebImagePrefetcher sharedImagePrefetcher].maxConcurrentPrefetchCount;
|
|
49
|
+
return MAX(limit, (NSUInteger)1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Starts pending preloads while there's room. A completion block calls it
|
|
53
|
+
// again, sometimes from inside start() (a memory cache hit completes
|
|
54
|
+
// synchronously on the main queue). That's fine: the counters are shared and
|
|
55
|
+
// updated before each start(), so the inner call starts what fits, and the
|
|
56
|
+
// outer loop sees the updated counters when it checks again.
|
|
57
|
+
static void FFFStartPendingPreloads(void)
|
|
58
|
+
{
|
|
59
|
+
while (FFFPreloadsInFlight < FFFPreloadLimit() && FFFPendingPreloads.count > 0) {
|
|
60
|
+
dispatch_block_t start = FFFPendingPreloads.firstObject;
|
|
61
|
+
[FFFPendingPreloads removeObjectAtIndex:0];
|
|
62
|
+
FFFPreloadsInFlight++;
|
|
63
|
+
start();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Resolves with a result per source, in order, once all have loaded or failed:
|
|
68
|
+
// { ok, width, height } or { ok: false, error }. Never rejects.
|
|
69
|
+
RCT_EXPORT_METHOD(preload:(nonnull NSArray<FFFastImageSource *> *)sources
|
|
70
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
71
|
+
reject:(__unused RCTPromiseRejectBlock)reject)
|
|
72
|
+
{
|
|
73
|
+
// This runs on the UIManager queue; the queue state is only touched on
|
|
74
|
+
// the main queue.
|
|
75
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
76
|
+
if (!FFFPendingPreloads) {
|
|
77
|
+
FFFPendingPreloads = [NSMutableArray array];
|
|
39
78
|
}
|
|
40
|
-
|
|
41
|
-
|
|
79
|
+
NSMutableArray *results = [NSMutableArray arrayWithCapacity:sources.count];
|
|
80
|
+
// Sources of this call still to finish. It starts at the number of
|
|
81
|
+
// sources, so the promise can't resolve before they've all been added.
|
|
82
|
+
__block NSUInteger remaining = sources.count;
|
|
83
|
+
void (^finishOne)(void) = ^{
|
|
84
|
+
if (--remaining == 0) {
|
|
85
|
+
resolve(results);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
// With the prefetcher's options (low priority), but one source at a
|
|
89
|
+
// time (not SDWebImagePrefetcher), to get each source's result and to
|
|
90
|
+
// send its headers with its own request only.
|
|
91
|
+
SDWebImageOptions prefetcherOptions = [SDWebImagePrefetcher sharedImagePrefetcher].options;
|
|
92
|
+
|
|
93
|
+
[sources enumerateObjectsUsingBlock:^(FFFastImageSource * _Nonnull source, NSUInteger idx, BOOL * _Nonnull stop) {
|
|
94
|
+
if (!source.url) {
|
|
95
|
+
// An empty, missing or null uri (JS sends null sources as {}).
|
|
96
|
+
// It fails without taking a slot.
|
|
97
|
+
[results addObject:@{@"ok": @NO, @"error": @"Invalid source: no uri"}];
|
|
98
|
+
finishOne();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
[results addObject:[NSNull null]];
|
|
102
|
+
// A source's own priority replaces the prefetcher's, as on Android.
|
|
103
|
+
SDWebImageOptions options = prefetcherOptions;
|
|
104
|
+
if (source.hasPriority) {
|
|
105
|
+
options &= ~(SDWebImageLowPriority | SDWebImageHighPriority);
|
|
106
|
+
if (source.priority == FFFPriorityLow) {
|
|
107
|
+
options |= SDWebImageLowPriority;
|
|
108
|
+
} else if (source.priority == FFFPriorityHigh) {
|
|
109
|
+
options |= SDWebImageHighPriority;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
[FFFPendingPreloads addObject:[^{
|
|
113
|
+
// Once per slot: SDWebImage calls this once with finished set
|
|
114
|
+
// (a progressive load calls it more, without).
|
|
115
|
+
__block BOOL done = NO;
|
|
116
|
+
[[SDWebImageManager sharedManager] loadImageWithURL:source.url
|
|
117
|
+
options:options
|
|
118
|
+
context:@{SDWebImageContextDownloadRequestModifier: source.requestModifier}
|
|
119
|
+
progress:nil
|
|
120
|
+
completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
|
|
121
|
+
if (!finished || done) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
done = YES;
|
|
125
|
+
results[idx] = image
|
|
126
|
+
? @{@"ok": @YES, @"width": @(image.size.width), @"height": @(image.size.height)}
|
|
127
|
+
: @{@"ok": @NO, @"error": FFFErrorMessage(error)};
|
|
128
|
+
FFFPreloadsInFlight--;
|
|
129
|
+
finishOne();
|
|
130
|
+
FFFStartPendingPreloads();
|
|
131
|
+
}];
|
|
132
|
+
} copy]];
|
|
133
|
+
}];
|
|
134
|
+
|
|
135
|
+
if (sources.count == 0) {
|
|
136
|
+
resolve(results);
|
|
42
137
|
return;
|
|
43
138
|
}
|
|
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];
|
|
139
|
+
FFFStartPendingPreloads();
|
|
140
|
+
});
|
|
57
141
|
}
|
|
58
142
|
|
|
59
143
|
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
|
}
|