react-native-fast-image 8.7.0 → 8.8.1
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 -2
- 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/FastImageViewWithUrl.java +151 -2
- package/dist/index.cjs.js +17 -6
- package/dist/index.cjs.js.flow +13 -5
- package/dist/index.d.ts +19 -5
- package/dist/index.js +17 -6
- package/dist/index.js.flow +13 -5
- package/ios/FastImage/FFFastImageView.h +3 -0
- package/ios/FastImage/FFFastImageView.m +32 -0
- package/ios/FastImage/FFFastImageViewManager.m +1 -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).
|
|
@@ -230,11 +243,17 @@ FastImage.preload([
|
|
|
230
243
|
])
|
|
231
244
|
```
|
|
232
245
|
|
|
233
|
-
It resolves when all the images have loaded or failed, with a result for each source, in order: `{ uri, ok, width, height, error }
|
|
246
|
+
It resolves when all the images have loaded or failed, with a result for each source, in order: `{ uri, ok: true, width, height }` if it loaded, or `{ uri, ok: false, error }` if it didn't (checking `ok` narrows the type in TypeScript). `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
247
|
|
|
235
248
|
```js
|
|
236
249
|
const results = await FastImage.preload(sources)
|
|
237
|
-
const
|
|
250
|
+
for (const result of results) {
|
|
251
|
+
if (result.ok) {
|
|
252
|
+
console.log(result.uri, result.width, result.height)
|
|
253
|
+
} else {
|
|
254
|
+
console.warn(result.uri, result.error)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
238
257
|
```
|
|
239
258
|
|
|
240
259
|
### `FastImage.clearMemoryCache: () => Promise<void>`
|
|
@@ -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);
|
|
@@ -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
|
@@ -125,18 +125,29 @@ const FastImageComponent = (0, react.forwardRef)((props, ref) => /* @__PURE__ */
|
|
|
125
125
|
...props
|
|
126
126
|
}));
|
|
127
127
|
FastImageComponent.displayName = "FastImage";
|
|
128
|
+
const noResult = {
|
|
129
|
+
ok: false,
|
|
130
|
+
error: "No result"
|
|
131
|
+
};
|
|
128
132
|
const FastImage = FastImageComponent;
|
|
129
133
|
FastImage.resizeMode = resizeMode;
|
|
130
134
|
FastImage.cacheControl = cacheControl;
|
|
131
135
|
FastImage.priority = priority;
|
|
132
136
|
FastImage.preload = (sources) => Promise.resolve(react_native.NativeModules.FastImageView.preload(sources.map((s) => s || {}))).then((results) => sources.map((source, i) => {
|
|
133
137
|
var _results$i;
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
138
|
+
const uri = source ? source.uri : void 0;
|
|
139
|
+
const result = (_results$i = results === null || results === void 0 ? void 0 : results[i]) !== null && _results$i !== void 0 ? _results$i : noResult;
|
|
140
|
+
if (!result.ok) return {
|
|
141
|
+
...result,
|
|
142
|
+
uri
|
|
143
|
+
};
|
|
144
|
+
return typeof uri === "string" ? {
|
|
145
|
+
...result,
|
|
146
|
+
uri
|
|
147
|
+
} : {
|
|
148
|
+
ok: false,
|
|
149
|
+
error: "Invalid source: no uri",
|
|
150
|
+
uri
|
|
140
151
|
};
|
|
141
152
|
}));
|
|
142
153
|
FastImage.clearMemoryCache = () => react_native.NativeModules.FastImageView.clearMemoryCache();
|
package/dist/index.cjs.js.flow
CHANGED
|
@@ -43,14 +43,21 @@ 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 PreloadSuccess = {|
|
|
47
|
+
uri: string,
|
|
48
|
+
ok: true,
|
|
49
|
+
width: number,
|
|
50
|
+
height: number,
|
|
51
|
+
|}
|
|
52
|
+
|
|
53
|
+
export type PreloadFailure = {|
|
|
47
54
|
uri?: string,
|
|
48
|
-
ok:
|
|
49
|
-
|
|
50
|
-
height?: number,
|
|
51
|
-
error?: string,
|
|
55
|
+
ok: false,
|
|
56
|
+
error: string,
|
|
52
57
|
|}
|
|
53
58
|
|
|
59
|
+
export type PreloadResult = PreloadSuccess | PreloadFailure
|
|
60
|
+
|
|
54
61
|
export type PreloadFn = (
|
|
55
62
|
sources: Array<FastImageSource>,
|
|
56
63
|
) => Promise<Array<PreloadResult>>
|
|
@@ -75,6 +82,7 @@ export type FastImageProps = $ReadOnly<{|
|
|
|
75
82
|
tintColor?: number | string,
|
|
76
83
|
resizeMode?: ?ResizeModes,
|
|
77
84
|
fallback?: ?boolean,
|
|
85
|
+
recyclingKey?: ?string,
|
|
78
86
|
testID?: ?string,
|
|
79
87
|
|}>
|
|
80
88
|
|
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,13 +102,18 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
|
|
|
93
102
|
*/
|
|
94
103
|
children?: React.ReactNode;
|
|
95
104
|
}
|
|
96
|
-
export interface
|
|
105
|
+
export interface PreloadSuccess {
|
|
106
|
+
uri: string;
|
|
107
|
+
ok: true;
|
|
108
|
+
width: number;
|
|
109
|
+
height: number;
|
|
110
|
+
}
|
|
111
|
+
export interface PreloadFailure {
|
|
97
112
|
uri?: string;
|
|
98
|
-
ok:
|
|
99
|
-
|
|
100
|
-
height?: number;
|
|
101
|
-
error?: string;
|
|
113
|
+
ok: false;
|
|
114
|
+
error: string;
|
|
102
115
|
}
|
|
116
|
+
export type PreloadResult = PreloadSuccess | PreloadFailure;
|
|
103
117
|
export interface FastImageStaticProperties {
|
|
104
118
|
resizeMode: typeof resizeMode;
|
|
105
119
|
priority: typeof priority;
|
package/dist/index.js
CHANGED
|
@@ -102,18 +102,29 @@ const FastImageComponent = forwardRef((props, ref) => /* @__PURE__ */ React.crea
|
|
|
102
102
|
...props
|
|
103
103
|
}));
|
|
104
104
|
FastImageComponent.displayName = "FastImage";
|
|
105
|
+
const noResult = {
|
|
106
|
+
ok: false,
|
|
107
|
+
error: "No result"
|
|
108
|
+
};
|
|
105
109
|
const FastImage = FastImageComponent;
|
|
106
110
|
FastImage.resizeMode = resizeMode;
|
|
107
111
|
FastImage.cacheControl = cacheControl;
|
|
108
112
|
FastImage.priority = priority;
|
|
109
113
|
FastImage.preload = (sources) => Promise.resolve(NativeModules.FastImageView.preload(sources.map((s) => s || {}))).then((results) => sources.map((source, i) => {
|
|
110
114
|
var _results$i;
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
115
|
+
const uri = source ? source.uri : void 0;
|
|
116
|
+
const result = (_results$i = results === null || results === void 0 ? void 0 : results[i]) !== null && _results$i !== void 0 ? _results$i : noResult;
|
|
117
|
+
if (!result.ok) return {
|
|
118
|
+
...result,
|
|
119
|
+
uri
|
|
120
|
+
};
|
|
121
|
+
return typeof uri === "string" ? {
|
|
122
|
+
...result,
|
|
123
|
+
uri
|
|
124
|
+
} : {
|
|
125
|
+
ok: false,
|
|
126
|
+
error: "Invalid source: no uri",
|
|
127
|
+
uri
|
|
117
128
|
};
|
|
118
129
|
}));
|
|
119
130
|
FastImage.clearMemoryCache = () => NativeModules.FastImageView.clearMemoryCache();
|
package/dist/index.js.flow
CHANGED
|
@@ -43,14 +43,21 @@ 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 PreloadSuccess = {|
|
|
47
|
+
uri: string,
|
|
48
|
+
ok: true,
|
|
49
|
+
width: number,
|
|
50
|
+
height: number,
|
|
51
|
+
|}
|
|
52
|
+
|
|
53
|
+
export type PreloadFailure = {|
|
|
47
54
|
uri?: string,
|
|
48
|
-
ok:
|
|
49
|
-
|
|
50
|
-
height?: number,
|
|
51
|
-
error?: string,
|
|
55
|
+
ok: false,
|
|
56
|
+
error: string,
|
|
52
57
|
|}
|
|
53
58
|
|
|
59
|
+
export type PreloadResult = PreloadSuccess | PreloadFailure
|
|
60
|
+
|
|
54
61
|
export type PreloadFn = (
|
|
55
62
|
sources: Array<FastImageSource>,
|
|
56
63
|
) => Promise<Array<PreloadResult>>
|
|
@@ -75,6 +82,7 @@ export type FastImageProps = $ReadOnly<{|
|
|
|
75
82
|
tintColor?: number | string,
|
|
76
83
|
resizeMode?: ?ResizeModes,
|
|
77
84
|
fallback?: ?boolean,
|
|
85
|
+
recyclingKey?: ?string,
|
|
78
86
|
testID?: ?string,
|
|
79
87
|
|}>
|
|
80
88
|
|
|
@@ -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(@{});
|
|
@@ -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)
|