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 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 }`. `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:
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 failed = results.filter((result) => !result.ok)
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
- builder.into(this);
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
- 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
- }
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();
@@ -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 PreloadResult = {|
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: boolean,
49
- width?: number,
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 PreloadResult {
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: boolean;
99
- width?: number;
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
- 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
- }
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();
@@ -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 PreloadResult = {|
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: boolean,
49
- width?: number,
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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-fast-image",
3
- "version": "8.7.0",
3
+ "version": "8.8.1",
4
4
  "description": "🚩 FastImage, performant React Native image component.",
5
5
  "keywords": [
6
6
  "cache",