react-native-fast-image 10.4.0 → 10.6.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 CHANGED
@@ -26,7 +26,7 @@ React Native's `Image` component handles image caching like browsers for the mos
26
26
  - **Responsive images**: give several sizes, and the one that fits the view loads ([`source`](#source)).
27
27
  - **Animated images** (GIF, WebP, APNG and AVIF) that can [loop](#loop) and [pause](#paused).
28
28
  - **Fades** in and between images ([`transition`](#transition)).
29
- - **Placeholders**: a blurred preview from a [ThumbHash](#sourcethumbhash) or a [tiny image](#sourceblurdatauri) while an image loads, or a bundled image ([`placeholder`](#placeholder)).
29
+ - **Placeholders**: a blurred preview from a [ThumbHash](#sourcethumbhash) or a [tiny image](#sourceblurdatauri) while an image loads, a bundled image, or a url, e.g. a thumbnail that's already cached ([`placeholder`](#placeholder)).
30
30
  - [**SVG**](docs/formats.md#svg-images) and [**photo library**](#photo-library-images-ios-and-tvos) images.
31
31
  - [**`FastImageBackground`**](#fastimagebackground) for content over an image.
32
32
  - **Expo and web support**, with a [config plugin](#expo-config-plugin) for Expo.
@@ -107,7 +107,7 @@ It's a single native view, which also takes React Native's View props, such as a
107
107
 
108
108
  #### `source`
109
109
 
110
- **Type:** `number | Source | Source[]`
110
+ **Type:** `number | Source | (number | Source)[]`
111
111
 
112
112
  Source for the remote image to load: a `Source`, a `require()`d image, or an array of the same image at different sizes.
113
113
 
@@ -129,6 +129,7 @@ When `source` changes, the image that's showing stays until the new one has load
129
129
  - When the view's size changes so that another size fits better, it loads that one (with its load events), and keeps showing the current image until then, without a `transition`.
130
130
  - Each size is cached separately. A `cacheKey` applies to its own entry: give each size its own key (or none), or the sizes would replace each other in the cache.
131
131
  - `FastImage.preload`, `getCachePath` and `writeToCache` take one size: pass the one a view will show. An array fails with `{ ok: false, error }`.
132
+ - A `require()`d image can be one of the sizes: its size is the file's.
132
133
  - An array of one is the same as that source.
133
134
 
134
135
  ---
@@ -266,7 +267,7 @@ With several sources, the scale `width` and `height` are multiplied by.
266
267
 
267
268
  #### `placeholder`
268
269
 
269
- **Type:** `string | number | SourcePlaceholder`
270
+ **Type:** `string | number | PlaceholderSource | SourcePlaceholder`
270
271
 
271
272
  A placeholder shown while the image loads, and if it fails to load:
272
273
 
@@ -275,13 +276,15 @@ A placeholder shown while the image loads, and if it fails to load:
275
276
  - The name of an image bundled with the app (an iOS asset catalog image or an Android drawable): `placeholder="gray"`.
276
277
  - A [ThumbHash](#sourcethumbhash), shown blurred: `placeholder={{ thumbhash }}`.
277
278
  - A tiny image as a data uri, shown blurred: `placeholder={{ blurDataUri }}`.
279
+ - A url, loaded and cached like a source: `placeholder="https://example.com/placeholder.png"`, or a source with `headers` or a `cacheKey`: `placeholder={{ uri, headers }}`.
280
+ - A url only shown if it's already cached, e.g. a list's thumbnail while the full-size image loads: `placeholder={{ uri, cache: 'cacheOnly' }}`.
278
281
 
279
- A source's own `thumbhash` or `blurDataUri` is shown instead, if it has one.
282
+ A source's own `thumbhash` or `blurDataUri` is shown instead, if it has one; a url placeholder shows over it once it has loaded.
280
283
 
281
284
  Notes:
282
285
 
283
- - A url isn't loaded: it's ignored, with a warning in development.
284
286
  - On Android, a `require()`d image doesn't show in debug builds, where the dev server serves it. Release builds show it.
287
+ - On the web, a url loads like any image, also with `'cacheOnly'`: a browser can't look only in its cache.
285
288
 
286
289
  ---
287
290
 
@@ -289,7 +292,7 @@ Notes:
289
292
 
290
293
  **Type:** `number`
291
294
 
292
- **Deprecated:** Use `placeholder`, which does the same.
295
+ **Deprecated:** Use `placeholder`, which does the same. With both, `placeholder` shows.
293
296
 
294
297
  ---
295
298
 
@@ -844,6 +847,14 @@ Where the image is in the view: see [`objectPosition`](#objectposition).
844
847
 
845
848
  ---
846
849
 
850
+ ### `PlaceholderSource`
851
+
852
+ **Type:** `Pick<Source, 'uri' | 'headers' | 'cacheKey' | 'cache'>`
853
+
854
+ A url as `placeholder`, loaded like a source (see `placeholder`).
855
+
856
+ ---
857
+
847
858
  ### `PositionLength`
848
859
 
849
860
  **Type:** `` number | `${number}%` ``
@@ -27,7 +27,7 @@ import java.util.Map;
27
27
  import javax.annotation.Nullable;
28
28
 
29
29
  class FastImageViewConverter {
30
- private static final Drawable TRANSPARENT_DRAWABLE = new ColorDrawable(Color.TRANSPARENT);
30
+ static final Drawable TRANSPARENT_DRAWABLE = new ColorDrawable(Color.TRANSPARENT);
31
31
 
32
32
  private static final Map<String, FastImageCacheControl> FAST_IMAGE_CACHE_CONTROL_MAP =
33
33
  new HashMap<String, FastImageCacheControl>() {{
@@ -21,6 +21,7 @@ import com.bumptech.glide.RequestManager;
21
21
  import com.facebook.react.bridge.ColorPropConverter;
22
22
  import com.facebook.react.bridge.Dynamic;
23
23
  import com.facebook.react.bridge.DynamicFromObject;
24
+ import com.facebook.react.bridge.JavaOnlyArray;
24
25
  import com.facebook.react.bridge.ReadableArray;
25
26
  import com.facebook.react.bridge.ReadableMap;
26
27
  import com.facebook.react.bridge.ReadableType;
@@ -307,17 +308,27 @@ class FastImageViewManager extends SimpleViewManager<FastImageViewWithUrl>
307
308
 
308
309
  @Override
309
310
  public void setSource(FastImageViewWithUrl view, Dynamic source) {
310
- view.setSource(source.isNull() ? null : source.asMap());
311
+ // One source, or several (an array).
312
+ view.setSource(source.isNull() ? null
313
+ : source.getType() == ReadableType.Array ? source.asArray()
314
+ : JavaOnlyArray.of(source.asMap()));
311
315
  }
312
316
 
313
- @Override
314
- public void setSources(FastImageViewWithUrl view, Dynamic sources) {
315
- view.setSources(sources.isNull() ? null : sources.asArray());
316
- }
317
-
318
- // The `placeholder` prop: the resolved asset's uri, or a data uri.
317
+ // The `placeholder` prop: a thumbhash or blurDataUri, or a url to load
318
+ // apart from the image, as a source (a map), or the resolved asset's uri
319
+ // or a data uri (a string).
319
320
  @Override
320
321
  public void setPlaceholder(FastImageViewWithUrl view, Dynamic source) {
322
+ if (source.getType() == ReadableType.Map) {
323
+ ReadableMap map = source.asMap();
324
+ boolean hash = map.hasKey("thumbhash") || map.hasKey("blurDataUri");
325
+ view.setPlaceholder(null);
326
+ view.setPlaceholderHash(hash ? FastImageViewWithUrl.sourcePlaceholder(map) : null);
327
+ view.setPlaceholderSource(hash ? null : map);
328
+ return;
329
+ }
330
+ view.setPlaceholderHash(null);
331
+ view.setPlaceholderSource(null);
321
332
  String uri = source.isNull() ? null : source.asString();
322
333
  if (uri != null && FastImageThumbHash.isDataUri(uri)) {
323
334
  Bitmap bitmap = FastImageThumbHash.dataUri(uri);
@@ -344,11 +355,6 @@ class FastImageViewManager extends SimpleViewManager<FastImageViewWithUrl>
344
355
  view.setRecyclingKey(recyclingKey);
345
356
  }
346
357
 
347
- @Override
348
- public void setSourcePlaceholders(FastImageViewWithUrl view, @Nullable ReadableArray placeholders) {
349
- view.setSourcePlaceholders(placeholders);
350
- }
351
-
352
358
  @Override
353
359
  public void setLoopCount(FastImageViewWithUrl view, int loopCount) {
354
360
  view.setLoopCount(loopCount);
@@ -30,6 +30,7 @@ import androidx.annotation.RequiresApi;
30
30
  import androidx.appcompat.widget.AppCompatImageView;
31
31
 
32
32
  import com.bumptech.glide.GenericTransitionOptions;
33
+ import com.bumptech.glide.Priority;
33
34
  import com.bumptech.glide.RequestBuilder;
34
35
  import com.bumptech.glide.RequestManager;
35
36
  import com.bumptech.glide.load.DataSource;
@@ -39,6 +40,7 @@ import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
39
40
  import com.bumptech.glide.load.resource.gif.GifDrawable;
40
41
  import com.bumptech.glide.request.Request;
41
42
  import com.bumptech.glide.request.RequestOptions;
43
+ import com.bumptech.glide.request.target.CustomTarget;
42
44
  import com.bumptech.glide.request.target.DrawableImageViewTarget;
43
45
  import com.bumptech.glide.request.target.SizeReadyCallback;
44
46
  import com.bumptech.glide.request.transition.NoTransition;
@@ -46,6 +48,7 @@ import com.bumptech.glide.request.transition.Transition;
46
48
  import com.bumptech.glide.request.transition.TransitionFactory;
47
49
  import com.facebook.react.bridge.ReadableArray;
48
50
  import com.facebook.react.bridge.ReadableMap;
51
+ import com.facebook.react.bridge.ReadableType;
49
52
  import com.facebook.react.bridge.WritableMap;
50
53
  import com.facebook.react.bridge.WritableNativeMap;
51
54
  import com.facebook.react.modules.i18nmanager.I18nUtil;
@@ -56,6 +59,10 @@ import com.facebook.react.uimanager.PointerEvents;
56
59
  import com.facebook.react.uimanager.ReactPointerEventsView;
57
60
  import com.facebook.react.uimanager.style.LogicalEdge;
58
61
 
62
+ import java.util.ArrayList;
63
+ import java.util.HashMap;
64
+ import java.util.List;
65
+ import java.util.Objects;
59
66
  import javax.annotation.Nonnull;
60
67
 
61
68
  class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEventsView, ReactHitSlopView {
@@ -211,16 +218,72 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
211
218
  return false;
212
219
  }
213
220
 
214
- public void setSource(@Nullable ReadableMap source) {
215
- mNeedsReload = true;
216
- mPropSource = source;
217
- if (mSources == null) mSource = source;
221
+ // The `source` prop: one source, or several (sizes of one image), each
222
+ // with its thumbhash or blurDataUri if it has one. Only a change besides
223
+ // those loads again: a new thumbhash for the same image (e.g. one that
224
+ // arrives later) shows at once (see updatePlaceholder).
225
+ public void setSource(@Nullable ReadableArray sources) {
226
+ List<Object> key = sourceKey(sources);
227
+ if (!mHasSource || !Objects.equals(key, mSourceKey)) {
228
+ mHasSource = true;
229
+ mSourceKey = key;
230
+ mNeedsReload = true;
231
+ mSources = sources != null && sources.size() > 1 ? sources : null;
232
+ mSourceIndex = -1;
233
+ if (mSources == null) mSource = sources != null && sources.size() == 1 ? map(sources, 0) : null;
234
+ }
235
+ mSourcePlaceholders = new ArrayList<>();
236
+ for (int i = 0; sources != null && i < sources.size(); i++) {
237
+ mSourcePlaceholders.add(sourcePlaceholder(map(sources, i)));
238
+ }
239
+ updatePlaceholder();
240
+ }
241
+
242
+ // The sources without their thumbhashes and blurDataUris, to compare.
243
+ private boolean mHasSource = false;
244
+ @Nullable
245
+ private List<Object> mSourceKey = null;
246
+
247
+ @Nullable
248
+ private static List<Object> sourceKey(@Nullable ReadableArray sources) {
249
+ if (sources == null) return null;
250
+ List<Object> key = new ArrayList<>();
251
+ for (int i = 0; i < sources.size(); i++) {
252
+ ReadableMap source = map(sources, i);
253
+ key.add(source == null ? null : withoutPlaceholders(source));
254
+ }
255
+ return key;
256
+ }
257
+
258
+ // A source without its thumbhash and blurDataUri.
259
+ private static HashMap<String, Object> withoutPlaceholders(ReadableMap source) {
260
+ HashMap<String, Object> map = source.toHashMap();
261
+ map.remove("thumbhash");
262
+ map.remove("blurDataUri");
263
+ return map;
264
+ }
265
+
266
+ @Nullable
267
+ private static ReadableMap map(ReadableArray array, int index) {
268
+ return array.getType(index) == ReadableType.Map ? array.getMap(index) : null;
218
269
  }
219
270
 
220
- // The `source` prop. With several `sources`, mSource is the one picked for
221
- // the view's size instead.
271
+ // A source's (or `placeholder`'s) ThumbHash, or its blurDataUri if it's a
272
+ // data uri (JS warns about another), or null.
222
273
  @Nullable
223
- private ReadableMap mPropSource = null;
274
+ static String sourcePlaceholder(@Nullable ReadableMap source) {
275
+ if (source == null) return null;
276
+ String thumbhash = string(source, "thumbhash");
277
+ if (thumbhash != null && !thumbhash.isEmpty()) return thumbhash;
278
+ String blurDataUri = string(source, "blurDataUri");
279
+ return blurDataUri != null && FastImageThumbHash.isDataUri(blurDataUri) ? blurDataUri : null;
280
+ }
281
+
282
+ @Nullable
283
+ private static String string(ReadableMap map, String key) {
284
+ return map.hasKey(key) && map.getType(key) == ReadableType.String ? map.getString(key) : null;
285
+ }
286
+
224
287
  // Several sources (2 or more) of the same image at different sizes, or
225
288
  // null: the view loads the one whose size is closest to its own.
226
289
  @Nullable
@@ -238,13 +301,6 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
238
301
  @Nullable
239
302
  private FastImageViewManager mManager;
240
303
 
241
- public void setSources(@Nullable ReadableArray sources) {
242
- mNeedsReload = true;
243
- mSources = sources != null && sources.size() > 1 ? sources : null;
244
- mSourceIndex = -1;
245
- if (mSources == null) mSource = mPropSource;
246
- }
247
-
248
304
  // The box the image is drawn in: the view inside its padding, which is the
249
305
  // border widths (see updateBorderPadding).
250
306
  private int contentWidth() {
@@ -308,17 +364,20 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
308
364
  return true;
309
365
  }
310
366
 
367
+ // Shows at once, as a new source placeholder does (see updatePlaceholder),
368
+ // without loading the image again.
311
369
  public void setPlaceholder(@Nullable Drawable placeholder) {
312
- mNeedsReload = true;
313
370
  mPlaceholder = placeholder;
371
+ updatePlaceholder();
314
372
  }
315
373
 
316
- // The `sourcePlaceholders` prop, by the source's index ("" for none; one
317
- // for a single source): the picked source's ThumbHash or blurDataUri (a
318
- // data uri) shows instead of the placeholder prop. JS sends them apart from
319
- // the sources, so a new one for the same image doesn't reload it.
374
+ // The sources' ThumbHashes or blurDataUris, by the source's index (null
375
+ // for none): the picked source's shows instead of the placeholder prop.
376
+ private List<String> mSourcePlaceholders = new ArrayList<>();
377
+ // `placeholder` as a thumbhash or blurDataUri: shows for the sources
378
+ // without their own.
320
379
  @Nullable
321
- private ReadableArray mSourcePlaceholders = null;
380
+ private String mPlaceholderHash = null;
322
381
  // What shows while the image loads and if it fails: the picked source's
323
382
  // ThumbHash or blurDataUri decoded (FastImageThumbHash), or the placeholder
324
383
  // prop. FastImage's own drawable either way, which Glide doesn't recycle.
@@ -333,18 +392,129 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
333
392
  @Nullable
334
393
  private String mInvalidPlaceholder = null;
335
394
 
336
- public void setSourcePlaceholders(@Nullable ReadableArray placeholders) {
337
- mSourcePlaceholders = placeholders;
395
+ public void setPlaceholderHash(@Nullable String hash) {
396
+ if (Objects.equals(hash, mPlaceholderHash)) return;
397
+ mPlaceholderHash = hash;
398
+ updatePlaceholder();
399
+ }
400
+
401
+ // `placeholder` as a url (a source): loaded apart from the image, at the
402
+ // most urgent priority, as it only helps if it comes before the image (or
403
+ // only from Glide's caches, with cache cacheOnly).
404
+ // Once it has, a copy shows instead of the other placeholders: FastImage's
405
+ // own drawable, which Glide doesn't recycle, so a fade can still draw it
406
+ // after its request is cleared. A failure shows nothing more.
407
+ @Nullable
408
+ private ReadableMap mPlaceholderSource = null;
409
+ @Nullable
410
+ private Drawable mUrlPlaceholder = null;
411
+ @Nullable
412
+ private CustomTarget<Drawable> mUrlPlaceholderTarget = null;
413
+
414
+ // Its load waits for the view's size (see startUrlPlaceholderLoad).
415
+ private boolean mUrlPlaceholderWaitsForSize = false;
416
+
417
+ public void setPlaceholderSource(@Nullable ReadableMap source) {
418
+ if (source == null && mPlaceholderSource == null) return;
419
+ mPlaceholderSource = source;
420
+ clearUrlPlaceholderTarget();
421
+ mUrlPlaceholderWaitsForSize = false;
422
+ mUrlPlaceholder = null;
338
423
  updatePlaceholder();
424
+ scheduleUrlPlaceholderLoad();
425
+ }
426
+
427
+ // Starts placeholderSource's load once the current callbacks have run: an
428
+ // image from the memory cache has shown by then, and needs none.
429
+ private void scheduleUrlPlaceholderLoad() {
430
+ post(new Runnable() {
431
+ @Override
432
+ public void run() {
433
+ startUrlPlaceholderLoad();
434
+ }
435
+ });
436
+ }
437
+
438
+ // Loads placeholderSource's image while the image loads (or after it
439
+ // failed), at the view's size, once it has one: not if the image showed,
440
+ // or it's already loaded or loading.
441
+ private void startUrlPlaceholderLoad() {
442
+ if (requestManager == null || mDropped || mUrlPlaceholder != null || mUrlPlaceholderTarget != null
443
+ || !FastImageViewConverter.hasUri(mPlaceholderSource)) return;
444
+ if (mLoadEnded && mShownRequest != null) return;
445
+ if (getWidth() <= 0 || getHeight() <= 0) {
446
+ mUrlPlaceholderWaitsForSize = true;
447
+ return;
448
+ }
449
+ FastImageSource imageSource = FastImageViewConverter.getImageSource(getContext(), mPlaceholderSource);
450
+ if (imageSource == null) return;
451
+ CustomTarget<Drawable> target = new CustomTarget<Drawable>(getWidth(), getHeight()) {
452
+ @Override
453
+ public void onResourceReady(@NonNull Drawable resource, @Nullable Transition<? super Drawable> transition) {
454
+ if (this != mUrlPlaceholderTarget) return;
455
+ mUrlPlaceholder = ownCopy(resource);
456
+ updatePlaceholder();
457
+ // With the copy, Glide's can go back to its pools (a load
458
+ // can't be cleared from its own callback).
459
+ final CustomTarget<Drawable> done = this;
460
+ post(new Runnable() {
461
+ @Override
462
+ public void run() {
463
+ if (requestManager != null) requestManager.clear(done);
464
+ }
465
+ });
466
+ }
467
+
468
+ @Override
469
+ public void onLoadCleared(@Nullable Drawable placeholder) {
470
+ }
471
+ };
472
+ mUrlPlaceholderTarget = target;
473
+ requestManager
474
+ .load(imageSource.getSourceForLoad())
475
+ .apply(FastImageViewConverter.getOptions(getContext(), imageSource, mPlaceholderSource)
476
+ .priority(Priority.IMMEDIATE)
477
+ // Copied on a software canvas (see ownCopy).
478
+ .disallowHardwareConfig())
479
+ .into(target);
339
480
  }
340
481
 
341
- // The picked source's ThumbHash or blurDataUri, or null.
482
+ private void clearUrlPlaceholderTarget() {
483
+ if (mUrlPlaceholderTarget != null && requestManager != null) requestManager.clear(mUrlPlaceholderTarget);
484
+ mUrlPlaceholderTarget = null;
485
+ }
486
+
487
+ // A bitmap of the drawable (an animated one's first frame), scaled up
488
+ // smoothly as the other placeholders are, or null for one with no size.
489
+ @Nullable
490
+ private Drawable ownCopy(Drawable drawable) {
491
+ int width = drawable.getIntrinsicWidth();
492
+ int height = drawable.getIntrinsicHeight();
493
+ if (width <= 0 || height <= 0) return null;
494
+ try {
495
+ Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
496
+ Canvas canvas = new Canvas(bitmap);
497
+ Drawable copy = drawable.getConstantState() != null ? drawable.getConstantState().newDrawable().mutate() : drawable;
498
+ copy.setBounds(0, 0, width, height);
499
+ copy.draw(canvas);
500
+ BitmapDrawable own = new BitmapDrawable(getResources(), bitmap);
501
+ own.setFilterBitmap(true);
502
+ return own;
503
+ } catch (OutOfMemoryError | IllegalStateException e) {
504
+ // As startFade: out of memory, or a hardware bitmap after all.
505
+ return null;
506
+ }
507
+ }
508
+
509
+ // The picked source's ThumbHash or blurDataUri, or `placeholder`'s, or
510
+ // null.
342
511
  @Nullable
343
512
  private String pickedSourcePlaceholder() {
344
513
  int index = mSources == null ? 0 : mSourceIndex;
345
- String key = mSourcePlaceholders != null && index >= 0 && index < mSourcePlaceholders.size()
346
- && !mSourcePlaceholders.isNull(index) ? mSourcePlaceholders.getString(index) : null;
347
- return key == null || key.isEmpty() ? null : key;
514
+ // None until one is picked.
515
+ if (index < 0) return null;
516
+ String key = index < mSourcePlaceholders.size() ? mSourcePlaceholders.get(index) : null;
517
+ return key != null ? key : mPlaceholderHash;
348
518
  }
349
519
 
350
520
  // Sets mShownPlaceholder for the picked source. A view showing the previous
@@ -372,10 +542,16 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
372
542
  mInvalidPlaceholder = key;
373
543
  }
374
544
  }
545
+ // placeholderSource's image, once it has loaded, shows instead.
546
+ if (mUrlPlaceholder != null) placeholder = mUrlPlaceholder;
375
547
  if (placeholder == mShownPlaceholder) return;
376
548
  boolean showing = mShowsPlaceholder;
377
549
  mShownPlaceholder = placeholder;
378
- if (showing) setImageDrawable(placeholder);
550
+ // Also one that arrives while the view shows nothing (no placeholder
551
+ // while the image loads, or after it failed): never over an image.
552
+ Drawable shown = getDrawable();
553
+ boolean showsNothing = shown == null || shown == FastImageViewConverter.TRANSPARENT_DRAWABLE;
554
+ if (showing || (placeholder != null && showsNothing)) setImageDrawable(placeholder);
379
555
  }
380
556
 
381
557
  // The events JS has a handler for (the handledEvents prop): the view only
@@ -449,6 +625,7 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
449
625
  void drop() {
450
626
  mDropped = true;
451
627
  clearView(requestManager);
628
+ clearUrlPlaceholderTarget();
452
629
  endProgress();
453
630
  }
454
631
 
@@ -1183,10 +1360,11 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
1183
1360
  setImageMatrix(matrix);
1184
1361
  return;
1185
1362
  }
1186
- // A source's placeholder (a ThumbHash or a blurDataUri) stands for an
1187
- // image whose size isn't known yet: with scale-down and none (which
1188
- // show an image at its own size), it's shown as with contain.
1189
- boolean hashFits = drawable == mShownPlaceholder && mPlaceholderKey != null;
1363
+ // A source's placeholder (a ThumbHash or a blurDataUri), or
1364
+ // placeholderSource's image, stands for an image whose size isn't
1365
+ // known yet: with scale-down and none (which show an image at its own
1366
+ // size), it's shown as with contain.
1367
+ boolean hashFits = drawable == mShownPlaceholder && (mPlaceholderKey != null || drawable == mUrlPlaceholder);
1190
1368
  if (mLayoutScaleType == ScaleType.CENTER_CROP || mLayoutScaleType == ScaleType.FIT_CENTER || hashFits) {
1191
1369
  float x = viewWidth / drawableWidth;
1192
1370
  float y = viewHeight / drawableHeight;
@@ -1352,6 +1530,17 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
1352
1530
  // The loading image loaded (FastImageRequestListener).
1353
1531
  void onImageLoaded() {
1354
1532
  mLoadEnded = true;
1533
+ // The image came first: its placeholder isn't needed (not from
1534
+ // within Glide's callback).
1535
+ if (mUrlPlaceholder == null) {
1536
+ mUrlPlaceholderWaitsForSize = false;
1537
+ post(new Runnable() {
1538
+ @Override
1539
+ public void run() {
1540
+ if (mUrlPlaceholder == null && mLoadEnded && mShownRequest != null) clearUrlPlaceholderTarget();
1541
+ }
1542
+ });
1543
+ }
1355
1544
  endProgress();
1356
1545
  // Before the view shows it (the target does once this returns).
1357
1546
  applyObjectFit();
@@ -1380,6 +1569,10 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
1380
1569
  invalidate();
1381
1570
  }
1382
1571
  if (!switchSourceIfResized()) reloadIfResized();
1572
+ if (mUrlPlaceholderWaitsForSize && w > 0 && h > 0) {
1573
+ mUrlPlaceholderWaitsForSize = false;
1574
+ scheduleUrlPlaceholderLoad();
1575
+ }
1383
1576
  }
1384
1577
 
1385
1578
  private void reloadIfResized() {
@@ -1508,6 +1701,7 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
1508
1701
  }
1509
1702
  });
1510
1703
  builder.into(target);
1704
+ if (!quiet) scheduleUrlPlaceholderLoad();
1511
1705
  }
1512
1706
 
1513
1707
  // Loading the image that's showing again (at a new size, or with another
@@ -1524,6 +1718,8 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
1524
1718
  endProgress();
1525
1719
  applyObjectFit();
1526
1720
  mShownRequest = null;
1721
+ // Shows instead of the other placeholders once it loads.
1722
+ scheduleUrlPlaceholderLoad();
1527
1723
  final int load = mLoadCount;
1528
1724
  // Not from within Glide's callback. Without a thumbnail Glide shows the
1529
1725
  // placeholder the request started with: a source placeholder that changed since
@@ -1605,6 +1801,7 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
1605
1801
 
1606
1802
  // Clear the image.
1607
1803
  setImageDrawable(null);
1804
+ scheduleUrlPlaceholderLoad();
1608
1805
  return;
1609
1806
  }
1610
1807
 
@@ -1620,7 +1817,8 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
1620
1817
  // resolved: fire onError and onLoadEnd and show the placeholder prop,
1621
1818
  // as on iOS (#1028, #945).
1622
1819
  if (mSource != null && (imageSource == null || imageSource.getUri().toString().length() == 0)) {
1623
- String error = "Invalid source: " + mSource;
1820
+ // Without its thumbhash or blurDataUri (a data uri can be long).
1821
+ String error = "Invalid source: " + withoutPlaceholders(mSource);
1624
1822
  WritableMap event = new WritableNativeMap();
1625
1823
  event.putString("error", error);
1626
1824
  FastImageEvents.send(this, REACT_ON_ERROR_EVENT, event);
@@ -1632,6 +1830,7 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
1632
1830
 
1633
1831
  endProgress();
1634
1832
  setImageDrawable(mShownPlaceholder);
1833
+ scheduleUrlPlaceholderLoad();
1635
1834
  return;
1636
1835
  }
1637
1836
 
package/dist/index.cjs.js CHANGED
@@ -261,8 +261,14 @@ var FastImageViewNativeComponent_default = (0, react_native.codegenNativeCompone
261
261
  var NativeFastImageModule_default = react_native.TurboModuleRegistry.get("FastImageModule");
262
262
  //#endregion
263
263
  //#region src/placeholders.ts
264
+ const SCHEME = /^[a-z][a-z\d+.-]*:/i;
264
265
  function isImageName(value) {
265
- return !/^[a-z][a-z\d+.-]*:/i.test(value) && !value.startsWith("/");
266
+ return !SCHEME.test(value) && !value.startsWith("/");
267
+ }
268
+ function placeholderSource(value) {
269
+ const uri = typeof value === "string" ? value : value && typeof value === "object" ? value.uri : void 0;
270
+ if (typeof uri !== "string" || !/^https?:/i.test(uri)) return void 0;
271
+ return typeof value === "string" ? { uri } : value;
266
272
  }
267
273
  function hasSourcePlaceholder(value) {
268
274
  return !!value && typeof value === "object" && ("thumbhash" in value || "blurDataUri" in value);
@@ -270,20 +276,10 @@ function hasSourcePlaceholder(value) {
270
276
  function dataUri(value) {
271
277
  if (typeof value !== "string" || value === "") return void 0;
272
278
  if (/^data:/i.test(value)) return value;
273
- if (typeof __DEV__ !== "undefined" && __DEV__) warnOnce("placeholder-url", `react-native-fast-image: \`placeholder\` and \`blurDataUri\` don't load urls: "${value.slice(0, 60)}" is ignored. Use a data uri (data:image/…), or for \`placeholder\`, a require()d image or the name of an image bundled with the app.`, "warn");
279
+ if (typeof __DEV__ !== "undefined" && __DEV__) warnOnce("placeholder-url", `react-native-fast-image: \`blurDataUri\` takes a data uri (data:image/…): "${value.slice(0, 60)}" is ignored. For a placeholder loaded from a url, use \`placeholder\`.`, "warn");
274
280
  }
275
281
  //#endregion
276
282
  //#region src/index.tsx
277
- function withoutPlaceholders(source) {
278
- if (!hasSourcePlaceholder(source)) return source;
279
- const { thumbhash: _thumbhash, blurDataUri: _blurDataUri, ...rest } = source;
280
- return rest;
281
- }
282
- function sourcePlaceholder(source) {
283
- if (!source || typeof source !== "object") return "";
284
- const { thumbhash, blurDataUri } = source;
285
- return thumbhash || dataUri(blurDataUri) || "";
286
- }
287
283
  const resolvePlaceholder = (placeholder) => {
288
284
  if (typeof placeholder === "string") {
289
285
  if (isImageName(placeholder)) return react_native.Platform.OS === "android" ? placeholder : { uri: placeholder };
@@ -348,25 +344,19 @@ function builtInTransition() {
348
344
  }
349
345
  let defaultTransition;
350
346
  function FastImageBase({ source, placeholder, defaultSource, tintColor, onLoadStart, onProgress, onLoad, onError, onLoadEnd, style, children, objectFit, objectPosition, loop, transition, loading: _loading, sizes: _sizes, forwardedRef, ...viewProps }) {
351
- var _props$recyclingKey, _defaultTransition;
347
+ var _placeholderSource, _props$recyclingKey, _defaultTransition;
352
348
  const { fallback, resizeMode: _resizeMode, ...props } = viewProps;
353
349
  if (__DEV__ && fallback !== void 0) warnOnce("fallback", "react-native-fast-image: `fallback` was removed in 10.0, and does nothing: images always load natively. Remove it, or use React Native's Image for those images.", "warn");
354
350
  if (__DEV__) warnIfChildren(children);
355
351
  const resolvedTintColor = tintColor != null ? tintColor : fromStyle(style, "tintColor");
356
352
  const fit = resolveObjectFit(objectFit, style);
357
353
  const position = objectPositionOrCentered(objectPosition);
358
- const shared = hasSourcePlaceholder(placeholder) ? sourcePlaceholder(placeholder) : "";
359
- const { resolvedSource, sources, sourcePlaceholders } = (0, react.useMemo)(() => {
360
- const sources = Array.isArray(source) ? source.length === 1 ? void 0 : source : void 0;
361
- const single = Array.isArray(source) ? source[0] : source;
362
- const placeholders = (sources !== null && sources !== void 0 ? sources : [single]).map((entry) => sourcePlaceholder(entry) || shared);
363
- return {
364
- resolvedSource: sources ? void 0 : react_native.Image.resolveAssetSource(withoutPlaceholders(single)),
365
- sources: (sources === null || sources === void 0 ? void 0 : sources.some(hasSourcePlaceholder)) ? sources.map(withoutPlaceholders) : sources,
366
- sourcePlaceholders: placeholders.some(Boolean) ? placeholders : void 0
367
- };
368
- }, [source, shared]);
369
- const resolvedPlaceholder = resolvePlaceholder(hasSourcePlaceholder(placeholder) ? defaultSource : placeholder !== null && placeholder !== void 0 ? placeholder : defaultSource);
354
+ const resolvedSource = (0, react.useMemo)(() => {
355
+ for (const entry of [source].flat()) dataUri(entry === null || entry === void 0 ? void 0 : entry.blurDataUri);
356
+ const single = Array.isArray(source) && source.length === 1 ? source[0] : source;
357
+ return Array.isArray(single) ? single.map((entry) => react_native.Image.resolveAssetSource(entry)) : react_native.Image.resolveAssetSource(single);
358
+ }, [source]);
359
+ const resolvedPlaceholder = hasSourcePlaceholder(placeholder) ? placeholder.thumbhash || dataUri(placeholder.blurDataUri) ? placeholder : resolvePlaceholder(defaultSource) : (_placeholderSource = placeholderSource(placeholder)) !== null && _placeholderSource !== void 0 ? _placeholderSource : resolvePlaceholder(placeholder !== null && placeholder !== void 0 ? placeholder : defaultSource);
370
360
  return /* @__PURE__ */ react.default.createElement(FastImageViewNativeComponent_default, {
371
361
  ...props,
372
362
  ref: forwardedRef,
@@ -375,8 +365,6 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, onLoadSt
375
365
  loopCount: loopCount(loop),
376
366
  ...resolveTransition(transition, (_defaultTransition = defaultTransition) !== null && _defaultTransition !== void 0 ? _defaultTransition : builtInTransition()),
377
367
  source: resolvedSource,
378
- sources,
379
- sourcePlaceholders,
380
368
  placeholder: resolvedPlaceholder,
381
369
  onFastImageLoadStart: onLoadStart,
382
370
  onFastImageProgress: onProgress,
@@ -110,6 +110,13 @@ export type PreloadResult = PreloadSuccess | PreloadFailure
110
110
  export type PreloadFn = (
111
111
  sources: Array<FastImageSource>,
112
112
  ) => Promise<Array<PreloadResult>>
113
+ export type PlaceholderSource = {
114
+ uri?: string,
115
+ headers?: Object,
116
+ cacheKey?: string,
117
+ cache?: CacheControls,
118
+ }
119
+
113
120
  export type SourcePlaceholder = {
114
121
  thumbhash?: string,
115
122
  blurDataUri?: string,
@@ -138,8 +145,8 @@ export type FastImageProps = $ReadOnly<{|
138
145
  onLoadStart?: ?() => void,
139
146
  onProgress?: ?(event: OnProgressEvent) => void,
140
147
 
141
- source?: ?(FastImageSource | Array<FastImageSource> | number),
142
- placeholder?: ?(number | string | SourcePlaceholder),
148
+ source?: ?(FastImageSource | Array<FastImageSource | number> | number),
149
+ placeholder?: ?(number | string | SourcePlaceholder | PlaceholderSource),
143
150
  // Deprecated: use placeholder, which is the same.
144
151
  defaultSource?: ?number,
145
152