react-native-fast-image 10.4.0 → 10.5.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 +14 -4
- package/android/src/main/java/com/fastimage/FastImageViewConverter.java +1 -1
- package/android/src/main/java/com/fastimage/FastImageViewManager.java +8 -1
- package/android/src/main/java/com/fastimage/FastImageViewWithUrl.java +145 -5
- package/dist/index.cjs.js +10 -4
- package/dist/index.cjs.js.flow +8 -1
- package/dist/index.cjs.web.js +23 -4
- package/dist/index.cjs.web.js.flow +8 -1
- package/dist/index.d.ts +11 -3
- package/dist/index.js +10 -4
- package/dist/index.js.flow +8 -1
- package/dist/index.web.js +23 -4
- package/dist/index.web.js.flow +8 -1
- package/dist/placeholders.d.ts +2 -1
- package/ios/FastImage/FFFastImageView.h +4 -0
- package/ios/FastImage/FFFastImageView.m +161 -14
- package/ios/FastImage/FFFastImageViewComponentView.mm +19 -1
- package/package.json +1 -1
- package/src/index.js.flow +8 -1
- package/src/index.tsx +41 -13
- package/src/index.web.tsx +39 -4
- package/src/placeholders.ts +26 -9
- package/src/specs/FastImageViewNativeComponent.ts +4 -2
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,
|
|
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.
|
|
@@ -266,7 +266,7 @@ With several sources, the scale `width` and `height` are multiplied by.
|
|
|
266
266
|
|
|
267
267
|
#### `placeholder`
|
|
268
268
|
|
|
269
|
-
**Type:** `string | number | SourcePlaceholder`
|
|
269
|
+
**Type:** `string | number | PlaceholderSource | SourcePlaceholder`
|
|
270
270
|
|
|
271
271
|
A placeholder shown while the image loads, and if it fails to load:
|
|
272
272
|
|
|
@@ -275,13 +275,15 @@ A placeholder shown while the image loads, and if it fails to load:
|
|
|
275
275
|
- The name of an image bundled with the app (an iOS asset catalog image or an Android drawable): `placeholder="gray"`.
|
|
276
276
|
- A [ThumbHash](#sourcethumbhash), shown blurred: `placeholder={{ thumbhash }}`.
|
|
277
277
|
- A tiny image as a data uri, shown blurred: `placeholder={{ blurDataUri }}`.
|
|
278
|
+
- 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 }}`.
|
|
279
|
+
- 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
280
|
|
|
279
|
-
A source's own `thumbhash` or `blurDataUri` is shown instead, if it has one.
|
|
281
|
+
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
282
|
|
|
281
283
|
Notes:
|
|
282
284
|
|
|
283
|
-
- A url isn't loaded: it's ignored, with a warning in development.
|
|
284
285
|
- On Android, a `require()`d image doesn't show in debug builds, where the dev server serves it. Release builds show it.
|
|
286
|
+
- On the web, a url loads like any image, also with `'cacheOnly'`: a browser can't look only in its cache.
|
|
285
287
|
|
|
286
288
|
---
|
|
287
289
|
|
|
@@ -844,6 +846,14 @@ Where the image is in the view: see [`objectPosition`](#objectposition).
|
|
|
844
846
|
|
|
845
847
|
---
|
|
846
848
|
|
|
849
|
+
### `PlaceholderSource`
|
|
850
|
+
|
|
851
|
+
**Type:** `Pick<Source, 'uri' | 'headers' | 'cacheKey' | 'cache'>`
|
|
852
|
+
|
|
853
|
+
A url as `placeholder`, loaded like a source (see `placeholder`).
|
|
854
|
+
|
|
855
|
+
---
|
|
856
|
+
|
|
847
857
|
### `PositionLength`
|
|
848
858
|
|
|
849
859
|
**Type:** `` number | `${number}%` ``
|
|
@@ -27,7 +27,7 @@ import java.util.Map;
|
|
|
27
27
|
import javax.annotation.Nullable;
|
|
28
28
|
|
|
29
29
|
class FastImageViewConverter {
|
|
30
|
-
|
|
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>() {{
|
|
@@ -315,9 +315,16 @@ class FastImageViewManager extends SimpleViewManager<FastImageViewWithUrl>
|
|
|
315
315
|
view.setSources(sources.isNull() ? null : sources.asArray());
|
|
316
316
|
}
|
|
317
317
|
|
|
318
|
-
// The `placeholder` prop:
|
|
318
|
+
// The `placeholder` prop: a url to load apart from the image, as a source
|
|
319
|
+
// (a map), or the resolved asset's uri 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
|
+
view.setPlaceholder(null);
|
|
324
|
+
view.setPlaceholderSource(source.asMap());
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
view.setPlaceholderSource(null);
|
|
321
328
|
String uri = source.isNull() ? null : source.asString();
|
|
322
329
|
if (uri != null && FastImageThumbHash.isDataUri(uri)) {
|
|
323
330
|
Bitmap bitmap = FastImageThumbHash.dataUri(uri);
|
|
@@ -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;
|
|
@@ -309,6 +311,8 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
|
|
|
309
311
|
}
|
|
310
312
|
|
|
311
313
|
public void setPlaceholder(@Nullable Drawable placeholder) {
|
|
314
|
+
// None again (the placeholder prop is a url) doesn't reload.
|
|
315
|
+
if (placeholder == null && mPlaceholder == null) return;
|
|
312
316
|
mNeedsReload = true;
|
|
313
317
|
mPlaceholder = placeholder;
|
|
314
318
|
}
|
|
@@ -338,6 +342,114 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
|
|
|
338
342
|
updatePlaceholder();
|
|
339
343
|
}
|
|
340
344
|
|
|
345
|
+
// `placeholder` as a url (a source): loaded apart from the image, at the
|
|
346
|
+
// most urgent priority, as it only helps if it comes before the image (or
|
|
347
|
+
// only from Glide's caches, with cache cacheOnly).
|
|
348
|
+
// Once it has, a copy shows instead of the other placeholders: FastImage's
|
|
349
|
+
// own drawable, which Glide doesn't recycle, so a fade can still draw it
|
|
350
|
+
// after its request is cleared. A failure shows nothing more.
|
|
351
|
+
@Nullable
|
|
352
|
+
private ReadableMap mPlaceholderSource = null;
|
|
353
|
+
@Nullable
|
|
354
|
+
private Drawable mUrlPlaceholder = null;
|
|
355
|
+
@Nullable
|
|
356
|
+
private CustomTarget<Drawable> mUrlPlaceholderTarget = null;
|
|
357
|
+
|
|
358
|
+
// Its load waits for the view's size (see startUrlPlaceholderLoad).
|
|
359
|
+
private boolean mUrlPlaceholderWaitsForSize = false;
|
|
360
|
+
|
|
361
|
+
public void setPlaceholderSource(@Nullable ReadableMap source) {
|
|
362
|
+
if (source == null && mPlaceholderSource == null) return;
|
|
363
|
+
mPlaceholderSource = source;
|
|
364
|
+
clearUrlPlaceholderTarget();
|
|
365
|
+
mUrlPlaceholderWaitsForSize = false;
|
|
366
|
+
mUrlPlaceholder = null;
|
|
367
|
+
updatePlaceholder();
|
|
368
|
+
scheduleUrlPlaceholderLoad();
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Starts placeholderSource's load once the current callbacks have run: an
|
|
372
|
+
// image from the memory cache has shown by then, and needs none.
|
|
373
|
+
private void scheduleUrlPlaceholderLoad() {
|
|
374
|
+
post(new Runnable() {
|
|
375
|
+
@Override
|
|
376
|
+
public void run() {
|
|
377
|
+
startUrlPlaceholderLoad();
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Loads placeholderSource's image while the image loads (or after it
|
|
383
|
+
// failed), at the view's size, once it has one: not if the image showed,
|
|
384
|
+
// or it's already loaded or loading.
|
|
385
|
+
private void startUrlPlaceholderLoad() {
|
|
386
|
+
if (requestManager == null || mDropped || mUrlPlaceholder != null || mUrlPlaceholderTarget != null
|
|
387
|
+
|| !FastImageViewConverter.hasUri(mPlaceholderSource)) return;
|
|
388
|
+
if (mLoadEnded && mShownRequest != null) return;
|
|
389
|
+
if (getWidth() <= 0 || getHeight() <= 0) {
|
|
390
|
+
mUrlPlaceholderWaitsForSize = true;
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
FastImageSource imageSource = FastImageViewConverter.getImageSource(getContext(), mPlaceholderSource);
|
|
394
|
+
if (imageSource == null) return;
|
|
395
|
+
CustomTarget<Drawable> target = new CustomTarget<Drawable>(getWidth(), getHeight()) {
|
|
396
|
+
@Override
|
|
397
|
+
public void onResourceReady(@NonNull Drawable resource, @Nullable Transition<? super Drawable> transition) {
|
|
398
|
+
if (this != mUrlPlaceholderTarget) return;
|
|
399
|
+
mUrlPlaceholder = ownCopy(resource);
|
|
400
|
+
updatePlaceholder();
|
|
401
|
+
// With the copy, Glide's can go back to its pools (a load
|
|
402
|
+
// can't be cleared from its own callback).
|
|
403
|
+
final CustomTarget<Drawable> done = this;
|
|
404
|
+
post(new Runnable() {
|
|
405
|
+
@Override
|
|
406
|
+
public void run() {
|
|
407
|
+
if (requestManager != null) requestManager.clear(done);
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
@Override
|
|
413
|
+
public void onLoadCleared(@Nullable Drawable placeholder) {
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
mUrlPlaceholderTarget = target;
|
|
417
|
+
requestManager
|
|
418
|
+
.load(imageSource.getSourceForLoad())
|
|
419
|
+
.apply(FastImageViewConverter.getOptions(getContext(), imageSource, mPlaceholderSource)
|
|
420
|
+
.priority(Priority.IMMEDIATE)
|
|
421
|
+
// Copied on a software canvas (see ownCopy).
|
|
422
|
+
.disallowHardwareConfig())
|
|
423
|
+
.into(target);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
private void clearUrlPlaceholderTarget() {
|
|
427
|
+
if (mUrlPlaceholderTarget != null && requestManager != null) requestManager.clear(mUrlPlaceholderTarget);
|
|
428
|
+
mUrlPlaceholderTarget = null;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// A bitmap of the drawable (an animated one's first frame), scaled up
|
|
432
|
+
// smoothly as the other placeholders are, or null for one with no size.
|
|
433
|
+
@Nullable
|
|
434
|
+
private Drawable ownCopy(Drawable drawable) {
|
|
435
|
+
int width = drawable.getIntrinsicWidth();
|
|
436
|
+
int height = drawable.getIntrinsicHeight();
|
|
437
|
+
if (width <= 0 || height <= 0) return null;
|
|
438
|
+
try {
|
|
439
|
+
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
|
440
|
+
Canvas canvas = new Canvas(bitmap);
|
|
441
|
+
Drawable copy = drawable.getConstantState() != null ? drawable.getConstantState().newDrawable().mutate() : drawable;
|
|
442
|
+
copy.setBounds(0, 0, width, height);
|
|
443
|
+
copy.draw(canvas);
|
|
444
|
+
BitmapDrawable own = new BitmapDrawable(getResources(), bitmap);
|
|
445
|
+
own.setFilterBitmap(true);
|
|
446
|
+
return own;
|
|
447
|
+
} catch (OutOfMemoryError | IllegalStateException e) {
|
|
448
|
+
// As startFade: out of memory, or a hardware bitmap after all.
|
|
449
|
+
return null;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
341
453
|
// The picked source's ThumbHash or blurDataUri, or null.
|
|
342
454
|
@Nullable
|
|
343
455
|
private String pickedSourcePlaceholder() {
|
|
@@ -372,10 +484,16 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
|
|
|
372
484
|
mInvalidPlaceholder = key;
|
|
373
485
|
}
|
|
374
486
|
}
|
|
487
|
+
// placeholderSource's image, once it has loaded, shows instead.
|
|
488
|
+
if (mUrlPlaceholder != null) placeholder = mUrlPlaceholder;
|
|
375
489
|
if (placeholder == mShownPlaceholder) return;
|
|
376
490
|
boolean showing = mShowsPlaceholder;
|
|
377
491
|
mShownPlaceholder = placeholder;
|
|
378
|
-
|
|
492
|
+
// Also one that arrives while the view shows nothing (no placeholder
|
|
493
|
+
// while the image loads, or after it failed): never over an image.
|
|
494
|
+
Drawable shown = getDrawable();
|
|
495
|
+
boolean showsNothing = shown == null || shown == FastImageViewConverter.TRANSPARENT_DRAWABLE;
|
|
496
|
+
if (showing || (placeholder != null && showsNothing)) setImageDrawable(placeholder);
|
|
379
497
|
}
|
|
380
498
|
|
|
381
499
|
// The events JS has a handler for (the handledEvents prop): the view only
|
|
@@ -449,6 +567,7 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
|
|
|
449
567
|
void drop() {
|
|
450
568
|
mDropped = true;
|
|
451
569
|
clearView(requestManager);
|
|
570
|
+
clearUrlPlaceholderTarget();
|
|
452
571
|
endProgress();
|
|
453
572
|
}
|
|
454
573
|
|
|
@@ -1183,10 +1302,11 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
|
|
|
1183
1302
|
setImageMatrix(matrix);
|
|
1184
1303
|
return;
|
|
1185
1304
|
}
|
|
1186
|
-
// A source's placeholder (a ThumbHash or a blurDataUri)
|
|
1187
|
-
// image whose size isn't
|
|
1188
|
-
// show an image at its own
|
|
1189
|
-
|
|
1305
|
+
// A source's placeholder (a ThumbHash or a blurDataUri), or
|
|
1306
|
+
// placeholderSource's image, stands for an image whose size isn't
|
|
1307
|
+
// known yet: with scale-down and none (which show an image at its own
|
|
1308
|
+
// size), it's shown as with contain.
|
|
1309
|
+
boolean hashFits = drawable == mShownPlaceholder && (mPlaceholderKey != null || drawable == mUrlPlaceholder);
|
|
1190
1310
|
if (mLayoutScaleType == ScaleType.CENTER_CROP || mLayoutScaleType == ScaleType.FIT_CENTER || hashFits) {
|
|
1191
1311
|
float x = viewWidth / drawableWidth;
|
|
1192
1312
|
float y = viewHeight / drawableHeight;
|
|
@@ -1352,6 +1472,17 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
|
|
|
1352
1472
|
// The loading image loaded (FastImageRequestListener).
|
|
1353
1473
|
void onImageLoaded() {
|
|
1354
1474
|
mLoadEnded = true;
|
|
1475
|
+
// The image came first: its placeholder isn't needed (not from
|
|
1476
|
+
// within Glide's callback).
|
|
1477
|
+
if (mUrlPlaceholder == null) {
|
|
1478
|
+
mUrlPlaceholderWaitsForSize = false;
|
|
1479
|
+
post(new Runnable() {
|
|
1480
|
+
@Override
|
|
1481
|
+
public void run() {
|
|
1482
|
+
if (mUrlPlaceholder == null && mLoadEnded && mShownRequest != null) clearUrlPlaceholderTarget();
|
|
1483
|
+
}
|
|
1484
|
+
});
|
|
1485
|
+
}
|
|
1355
1486
|
endProgress();
|
|
1356
1487
|
// Before the view shows it (the target does once this returns).
|
|
1357
1488
|
applyObjectFit();
|
|
@@ -1380,6 +1511,10 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
|
|
|
1380
1511
|
invalidate();
|
|
1381
1512
|
}
|
|
1382
1513
|
if (!switchSourceIfResized()) reloadIfResized();
|
|
1514
|
+
if (mUrlPlaceholderWaitsForSize && w > 0 && h > 0) {
|
|
1515
|
+
mUrlPlaceholderWaitsForSize = false;
|
|
1516
|
+
scheduleUrlPlaceholderLoad();
|
|
1517
|
+
}
|
|
1383
1518
|
}
|
|
1384
1519
|
|
|
1385
1520
|
private void reloadIfResized() {
|
|
@@ -1508,6 +1643,7 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
|
|
|
1508
1643
|
}
|
|
1509
1644
|
});
|
|
1510
1645
|
builder.into(target);
|
|
1646
|
+
if (!quiet) scheduleUrlPlaceholderLoad();
|
|
1511
1647
|
}
|
|
1512
1648
|
|
|
1513
1649
|
// Loading the image that's showing again (at a new size, or with another
|
|
@@ -1524,6 +1660,8 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
|
|
|
1524
1660
|
endProgress();
|
|
1525
1661
|
applyObjectFit();
|
|
1526
1662
|
mShownRequest = null;
|
|
1663
|
+
// Shows instead of the other placeholders once it loads.
|
|
1664
|
+
scheduleUrlPlaceholderLoad();
|
|
1527
1665
|
final int load = mLoadCount;
|
|
1528
1666
|
// Not from within Glide's callback. Without a thumbnail Glide shows the
|
|
1529
1667
|
// placeholder the request started with: a source placeholder that changed since
|
|
@@ -1605,6 +1743,7 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
|
|
|
1605
1743
|
|
|
1606
1744
|
// Clear the image.
|
|
1607
1745
|
setImageDrawable(null);
|
|
1746
|
+
scheduleUrlPlaceholderLoad();
|
|
1608
1747
|
return;
|
|
1609
1748
|
}
|
|
1610
1749
|
|
|
@@ -1632,6 +1771,7 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
|
|
|
1632
1771
|
|
|
1633
1772
|
endProgress();
|
|
1634
1773
|
setImageDrawable(mShownPlaceholder);
|
|
1774
|
+
scheduleUrlPlaceholderLoad();
|
|
1635
1775
|
return;
|
|
1636
1776
|
}
|
|
1637
1777
|
|
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
|
|
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,7 +276,7 @@ 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: \`
|
|
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
|
|
@@ -348,7 +354,7 @@ function builtInTransition() {
|
|
|
348
354
|
}
|
|
349
355
|
let defaultTransition;
|
|
350
356
|
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;
|
|
357
|
+
var _placeholderSource, _props$recyclingKey, _defaultTransition;
|
|
352
358
|
const { fallback, resizeMode: _resizeMode, ...props } = viewProps;
|
|
353
359
|
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
360
|
if (__DEV__) warnIfChildren(children);
|
|
@@ -366,7 +372,7 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, onLoadSt
|
|
|
366
372
|
sourcePlaceholders: placeholders.some(Boolean) ? placeholders : void 0
|
|
367
373
|
};
|
|
368
374
|
}, [source, shared]);
|
|
369
|
-
const resolvedPlaceholder = resolvePlaceholder(hasSourcePlaceholder(placeholder) ? defaultSource : placeholder !== null && placeholder !== void 0 ? placeholder : defaultSource);
|
|
375
|
+
const resolvedPlaceholder = (_placeholderSource = placeholderSource(placeholder)) !== null && _placeholderSource !== void 0 ? _placeholderSource : resolvePlaceholder(hasSourcePlaceholder(placeholder) ? defaultSource : placeholder !== null && placeholder !== void 0 ? placeholder : defaultSource);
|
|
370
376
|
return /* @__PURE__ */ react.default.createElement(FastImageViewNativeComponent_default, {
|
|
371
377
|
...props,
|
|
372
378
|
ref: forwardedRef,
|
package/dist/index.cjs.js.flow
CHANGED
|
@@ -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,
|
|
@@ -139,7 +146,7 @@ export type FastImageProps = $ReadOnly<{|
|
|
|
139
146
|
onProgress?: ?(event: OnProgressEvent) => void,
|
|
140
147
|
|
|
141
148
|
source?: ?(FastImageSource | Array<FastImageSource> | number),
|
|
142
|
-
placeholder?: ?(number | string | SourcePlaceholder),
|
|
149
|
+
placeholder?: ?(number | string | SourcePlaceholder | PlaceholderSource),
|
|
143
150
|
// Deprecated: use placeholder, which is the same.
|
|
144
151
|
defaultSource?: ?number,
|
|
145
152
|
|
package/dist/index.cjs.web.js
CHANGED
|
@@ -268,13 +268,18 @@ function cssPosition({ x, y }) {
|
|
|
268
268
|
}
|
|
269
269
|
//#endregion
|
|
270
270
|
//#region src/placeholders.ts
|
|
271
|
+
function placeholderSource(value) {
|
|
272
|
+
const uri = typeof value === "string" ? value : value && typeof value === "object" ? value.uri : void 0;
|
|
273
|
+
if (typeof uri !== "string" || !/^https?:/i.test(uri)) return void 0;
|
|
274
|
+
return typeof value === "string" ? { uri } : value;
|
|
275
|
+
}
|
|
271
276
|
function hasSourcePlaceholder(value) {
|
|
272
277
|
return !!value && typeof value === "object" && ("thumbhash" in value || "blurDataUri" in value);
|
|
273
278
|
}
|
|
274
279
|
function dataUri(value) {
|
|
275
280
|
if (typeof value !== "string" || value === "") return void 0;
|
|
276
281
|
if (/^data:/i.test(value)) return value;
|
|
277
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) warnOnce("placeholder-url", `react-native-fast-image: \`
|
|
282
|
+
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");
|
|
278
283
|
}
|
|
279
284
|
//#endregion
|
|
280
285
|
//#region src/index.web.tsx
|
|
@@ -284,7 +289,7 @@ const notSupported = {
|
|
|
284
289
|
};
|
|
285
290
|
const useClientLayoutEffect = globalThis.document === void 0 ? react.useEffect : react.useLayoutEffect;
|
|
286
291
|
function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadius, onLoadStart, onLoad, onError, onLoadEnd, style, children, objectFit, objectPosition, loading, sizes, dynamicRangeLimit, forwardedRef, onLayout, pointerEvents, recyclingKey: _recyclingKey, loop: _loop, imageRendering: _imageRendering, paused: _paused, transition: _transition, downsample: _downsample, onProgress: _onProgress, ...props }) {
|
|
287
|
-
var _resolveUri;
|
|
292
|
+
var _resolveUri, _placeholderSource;
|
|
288
293
|
if (typeof __DEV__ !== "undefined" && __DEV__) warnIfChildren(children);
|
|
289
294
|
const { accessibilityLabel, "aria-label": ariaLabel, resizeMode: _resizeMode, ...viewProps } = props;
|
|
290
295
|
const label = ariaLabel || accessibilityLabel || void 0;
|
|
@@ -296,7 +301,9 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
|
|
|
296
301
|
const priority = sourcePriority(source);
|
|
297
302
|
const preview = hasSourcePlaceholder(placeholder) ? placeholder : void 0;
|
|
298
303
|
const blurUri = (_resolveUri = resolveUri(dataUri(shared(source, "blurDataUri", preview)))) !== null && _resolveUri !== void 0 ? _resolveUri : thumbhashUri(shared(source, "thumbhash", preview));
|
|
299
|
-
const
|
|
304
|
+
const urlPlaceholder = (_placeholderSource = placeholderSource(placeholder)) === null || _placeholderSource === void 0 ? void 0 : _placeholderSource.uri;
|
|
305
|
+
const [failedPlaceholder, setFailedPlaceholder] = (0, react.useState)();
|
|
306
|
+
const defaultUri = blurUri !== null && blurUri !== void 0 ? blurUri : resolveUri(preview || urlPlaceholder ? defaultSource : placeholder !== null && placeholder !== void 0 ? placeholder : defaultSource);
|
|
300
307
|
const [result, setResult] = (0, react.useState)();
|
|
301
308
|
const ok = result && result.key === key ? result.ok : void 0;
|
|
302
309
|
const svgId = (0, react.useId)().replace(/[^\w-]/g, "");
|
|
@@ -305,7 +312,7 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
|
|
|
305
312
|
const fit = resolveObjectFit(objectFit, style);
|
|
306
313
|
const position = cssPosition(objectPositionOrCentered(objectPosition));
|
|
307
314
|
const filter = [blurRadius ? `blur(${blurRadius}px)` : "", tintColor ? `url(#${tintId})` : ""].filter(Boolean).join(" ") || void 0;
|
|
308
|
-
const showDefault =
|
|
315
|
+
const showDefault = ok !== true;
|
|
309
316
|
useClientLayoutEffect(() => {
|
|
310
317
|
if (key) onLoadStart === null || onLoadStart === void 0 || onLoadStart();
|
|
311
318
|
}, [key]);
|
|
@@ -326,6 +333,18 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
|
|
|
326
333
|
position,
|
|
327
334
|
filter: blurUri ? [`url(#${blurId})`, tintColor ? `url(#${tintId})` : ""].filter(Boolean).join(" ") : filter,
|
|
328
335
|
alt: key ? "" : label !== null && label !== void 0 ? label : ""
|
|
336
|
+
}) : null, urlPlaceholder && showDefault ? /* @__PURE__ */ react.default.createElement(Picture, {
|
|
337
|
+
key: `placeholder ${urlPlaceholder}`,
|
|
338
|
+
src: urlPlaceholder,
|
|
339
|
+
loading,
|
|
340
|
+
priority: "high",
|
|
341
|
+
dynamicRangeLimit,
|
|
342
|
+
fit: fit === "scale-down" || fit === "none" ? "contain" : fit,
|
|
343
|
+
position,
|
|
344
|
+
filter: tintColor ? `url(#${tintId})` : void 0,
|
|
345
|
+
hidden: failedPlaceholder === urlPlaceholder,
|
|
346
|
+
onError: () => setFailedPlaceholder(urlPlaceholder),
|
|
347
|
+
alt: ""
|
|
329
348
|
}) : null, key && src ? /* @__PURE__ */ react.default.createElement(Picture, {
|
|
330
349
|
key: several ? "sizes" : key,
|
|
331
350
|
src,
|
|
@@ -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,
|
|
@@ -139,7 +146,7 @@ export type FastImageProps = $ReadOnly<{|
|
|
|
139
146
|
onProgress?: ?(event: OnProgressEvent) => void,
|
|
140
147
|
|
|
141
148
|
source?: ?(FastImageSource | Array<FastImageSource> | number),
|
|
142
|
-
placeholder?: ?(number | string | SourcePlaceholder),
|
|
149
|
+
placeholder?: ?(number | string | SourcePlaceholder | PlaceholderSource),
|
|
143
150
|
// Deprecated: use placeholder, which is the same.
|
|
144
151
|
defaultSource?: ?number,
|
|
145
152
|
|
package/dist/index.d.ts
CHANGED
|
@@ -154,6 +154,8 @@ export type Source = {
|
|
|
154
154
|
};
|
|
155
155
|
/** A `thumbhash` or `blurDataUri` as `placeholder`. */
|
|
156
156
|
export type SourcePlaceholder = Pick<Source, 'thumbhash' | 'blurDataUri'>;
|
|
157
|
+
/** A url as `placeholder`, loaded like a source (see `placeholder`). */
|
|
158
|
+
export type PlaceholderSource = Pick<Source, 'uri' | 'headers' | 'cacheKey' | 'cache'>;
|
|
157
159
|
/** How the image fades in: see [`transition`](#transition). */
|
|
158
160
|
export type Transition = {
|
|
159
161
|
/**
|
|
@@ -343,17 +345,23 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
|
|
|
343
345
|
* `placeholder={{ thumbhash }}`.
|
|
344
346
|
* - A tiny image as a data uri, shown blurred:
|
|
345
347
|
* `placeholder={{ blurDataUri }}`.
|
|
348
|
+
* - A url, loaded and cached like a source:
|
|
349
|
+
* `placeholder="https://example.com/placeholder.png"`, or a source with
|
|
350
|
+
* `headers` or a `cacheKey`: `placeholder={{ uri, headers }}`.
|
|
351
|
+
* - A url only shown if it's already cached, e.g. a list's thumbnail while
|
|
352
|
+
* the full-size image loads: `placeholder={{ uri, cache: 'cacheOnly' }}`.
|
|
346
353
|
*
|
|
347
354
|
* A source's own `thumbhash` or `blurDataUri` is shown instead, if it has
|
|
348
|
-
* one.
|
|
355
|
+
* one; a url placeholder shows over it once it has loaded.
|
|
349
356
|
*
|
|
350
357
|
* Notes:
|
|
351
358
|
*
|
|
352
|
-
* - A url isn't loaded: it's ignored, with a warning in development.
|
|
353
359
|
* - On Android, a `require()`d image doesn't show in debug builds, where
|
|
354
360
|
* the dev server serves it. Release builds show it.
|
|
361
|
+
* - On the web, a url loads like any image, also with `'cacheOnly'`: a
|
|
362
|
+
* browser can't look only in its cache.
|
|
355
363
|
*/
|
|
356
|
-
placeholder?: ImageRequireSource | string | SourcePlaceholder;
|
|
364
|
+
placeholder?: ImageRequireSource | string | SourcePlaceholder | PlaceholderSource;
|
|
357
365
|
/**
|
|
358
366
|
* @deprecated Use `placeholder`, which does the same.
|
|
359
367
|
*/
|
package/dist/index.js
CHANGED
|
@@ -234,8 +234,14 @@ var FastImageViewNativeComponent_default = codegenNativeComponent("FastImageView
|
|
|
234
234
|
var NativeFastImageModule_default = TurboModuleRegistry.get("FastImageModule");
|
|
235
235
|
//#endregion
|
|
236
236
|
//#region src/placeholders.ts
|
|
237
|
+
const SCHEME = /^[a-z][a-z\d+.-]*:/i;
|
|
237
238
|
function isImageName(value) {
|
|
238
|
-
return
|
|
239
|
+
return !SCHEME.test(value) && !value.startsWith("/");
|
|
240
|
+
}
|
|
241
|
+
function placeholderSource(value) {
|
|
242
|
+
const uri = typeof value === "string" ? value : value && typeof value === "object" ? value.uri : void 0;
|
|
243
|
+
if (typeof uri !== "string" || !/^https?:/i.test(uri)) return void 0;
|
|
244
|
+
return typeof value === "string" ? { uri } : value;
|
|
239
245
|
}
|
|
240
246
|
function hasSourcePlaceholder(value) {
|
|
241
247
|
return !!value && typeof value === "object" && ("thumbhash" in value || "blurDataUri" in value);
|
|
@@ -243,7 +249,7 @@ function hasSourcePlaceholder(value) {
|
|
|
243
249
|
function dataUri(value) {
|
|
244
250
|
if (typeof value !== "string" || value === "") return void 0;
|
|
245
251
|
if (/^data:/i.test(value)) return value;
|
|
246
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) warnOnce("placeholder-url", `react-native-fast-image: \`
|
|
252
|
+
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");
|
|
247
253
|
}
|
|
248
254
|
//#endregion
|
|
249
255
|
//#region src/index.tsx
|
|
@@ -321,7 +327,7 @@ function builtInTransition() {
|
|
|
321
327
|
}
|
|
322
328
|
let defaultTransition;
|
|
323
329
|
function FastImageBase({ source, placeholder, defaultSource, tintColor, onLoadStart, onProgress, onLoad, onError, onLoadEnd, style, children, objectFit, objectPosition, loop, transition, loading: _loading, sizes: _sizes, forwardedRef, ...viewProps }) {
|
|
324
|
-
var _props$recyclingKey, _defaultTransition;
|
|
330
|
+
var _placeholderSource, _props$recyclingKey, _defaultTransition;
|
|
325
331
|
const { fallback, resizeMode: _resizeMode, ...props } = viewProps;
|
|
326
332
|
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");
|
|
327
333
|
if (__DEV__) warnIfChildren(children);
|
|
@@ -339,7 +345,7 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, onLoadSt
|
|
|
339
345
|
sourcePlaceholders: placeholders.some(Boolean) ? placeholders : void 0
|
|
340
346
|
};
|
|
341
347
|
}, [source, shared]);
|
|
342
|
-
const resolvedPlaceholder = resolvePlaceholder(hasSourcePlaceholder(placeholder) ? defaultSource : placeholder !== null && placeholder !== void 0 ? placeholder : defaultSource);
|
|
348
|
+
const resolvedPlaceholder = (_placeholderSource = placeholderSource(placeholder)) !== null && _placeholderSource !== void 0 ? _placeholderSource : resolvePlaceholder(hasSourcePlaceholder(placeholder) ? defaultSource : placeholder !== null && placeholder !== void 0 ? placeholder : defaultSource);
|
|
343
349
|
return /* @__PURE__ */ React.createElement(FastImageViewNativeComponent_default, {
|
|
344
350
|
...props,
|
|
345
351
|
ref: forwardedRef,
|
package/dist/index.js.flow
CHANGED
|
@@ -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,
|
|
@@ -139,7 +146,7 @@ export type FastImageProps = $ReadOnly<{|
|
|
|
139
146
|
onProgress?: ?(event: OnProgressEvent) => void,
|
|
140
147
|
|
|
141
148
|
source?: ?(FastImageSource | Array<FastImageSource> | number),
|
|
142
|
-
placeholder?: ?(number | string | SourcePlaceholder),
|
|
149
|
+
placeholder?: ?(number | string | SourcePlaceholder | PlaceholderSource),
|
|
143
150
|
// Deprecated: use placeholder, which is the same.
|
|
144
151
|
defaultSource?: ?number,
|
|
145
152
|
|
package/dist/index.web.js
CHANGED
|
@@ -241,13 +241,18 @@ function cssPosition({ x, y }) {
|
|
|
241
241
|
}
|
|
242
242
|
//#endregion
|
|
243
243
|
//#region src/placeholders.ts
|
|
244
|
+
function placeholderSource(value) {
|
|
245
|
+
const uri = typeof value === "string" ? value : value && typeof value === "object" ? value.uri : void 0;
|
|
246
|
+
if (typeof uri !== "string" || !/^https?:/i.test(uri)) return void 0;
|
|
247
|
+
return typeof value === "string" ? { uri } : value;
|
|
248
|
+
}
|
|
244
249
|
function hasSourcePlaceholder(value) {
|
|
245
250
|
return !!value && typeof value === "object" && ("thumbhash" in value || "blurDataUri" in value);
|
|
246
251
|
}
|
|
247
252
|
function dataUri(value) {
|
|
248
253
|
if (typeof value !== "string" || value === "") return void 0;
|
|
249
254
|
if (/^data:/i.test(value)) return value;
|
|
250
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) warnOnce("placeholder-url", `react-native-fast-image: \`
|
|
255
|
+
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");
|
|
251
256
|
}
|
|
252
257
|
//#endregion
|
|
253
258
|
//#region src/index.web.tsx
|
|
@@ -257,7 +262,7 @@ const notSupported = {
|
|
|
257
262
|
};
|
|
258
263
|
const useClientLayoutEffect = globalThis.document === void 0 ? useEffect : useLayoutEffect;
|
|
259
264
|
function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadius, onLoadStart, onLoad, onError, onLoadEnd, style, children, objectFit, objectPosition, loading, sizes, dynamicRangeLimit, forwardedRef, onLayout, pointerEvents, recyclingKey: _recyclingKey, loop: _loop, imageRendering: _imageRendering, paused: _paused, transition: _transition, downsample: _downsample, onProgress: _onProgress, ...props }) {
|
|
260
|
-
var _resolveUri;
|
|
265
|
+
var _resolveUri, _placeholderSource;
|
|
261
266
|
if (typeof __DEV__ !== "undefined" && __DEV__) warnIfChildren(children);
|
|
262
267
|
const { accessibilityLabel, "aria-label": ariaLabel, resizeMode: _resizeMode, ...viewProps } = props;
|
|
263
268
|
const label = ariaLabel || accessibilityLabel || void 0;
|
|
@@ -269,7 +274,9 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
|
|
|
269
274
|
const priority = sourcePriority(source);
|
|
270
275
|
const preview = hasSourcePlaceholder(placeholder) ? placeholder : void 0;
|
|
271
276
|
const blurUri = (_resolveUri = resolveUri(dataUri(shared(source, "blurDataUri", preview)))) !== null && _resolveUri !== void 0 ? _resolveUri : thumbhashUri(shared(source, "thumbhash", preview));
|
|
272
|
-
const
|
|
277
|
+
const urlPlaceholder = (_placeholderSource = placeholderSource(placeholder)) === null || _placeholderSource === void 0 ? void 0 : _placeholderSource.uri;
|
|
278
|
+
const [failedPlaceholder, setFailedPlaceholder] = useState();
|
|
279
|
+
const defaultUri = blurUri !== null && blurUri !== void 0 ? blurUri : resolveUri(preview || urlPlaceholder ? defaultSource : placeholder !== null && placeholder !== void 0 ? placeholder : defaultSource);
|
|
273
280
|
const [result, setResult] = useState();
|
|
274
281
|
const ok = result && result.key === key ? result.ok : void 0;
|
|
275
282
|
const svgId = useId().replace(/[^\w-]/g, "");
|
|
@@ -278,7 +285,7 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
|
|
|
278
285
|
const fit = resolveObjectFit(objectFit, style);
|
|
279
286
|
const position = cssPosition(objectPositionOrCentered(objectPosition));
|
|
280
287
|
const filter = [blurRadius ? `blur(${blurRadius}px)` : "", tintColor ? `url(#${tintId})` : ""].filter(Boolean).join(" ") || void 0;
|
|
281
|
-
const showDefault =
|
|
288
|
+
const showDefault = ok !== true;
|
|
282
289
|
useClientLayoutEffect(() => {
|
|
283
290
|
if (key) onLoadStart === null || onLoadStart === void 0 || onLoadStart();
|
|
284
291
|
}, [key]);
|
|
@@ -299,6 +306,18 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
|
|
|
299
306
|
position,
|
|
300
307
|
filter: blurUri ? [`url(#${blurId})`, tintColor ? `url(#${tintId})` : ""].filter(Boolean).join(" ") : filter,
|
|
301
308
|
alt: key ? "" : label !== null && label !== void 0 ? label : ""
|
|
309
|
+
}) : null, urlPlaceholder && showDefault ? /* @__PURE__ */ React.createElement(Picture, {
|
|
310
|
+
key: `placeholder ${urlPlaceholder}`,
|
|
311
|
+
src: urlPlaceholder,
|
|
312
|
+
loading,
|
|
313
|
+
priority: "high",
|
|
314
|
+
dynamicRangeLimit,
|
|
315
|
+
fit: fit === "scale-down" || fit === "none" ? "contain" : fit,
|
|
316
|
+
position,
|
|
317
|
+
filter: tintColor ? `url(#${tintId})` : void 0,
|
|
318
|
+
hidden: failedPlaceholder === urlPlaceholder,
|
|
319
|
+
onError: () => setFailedPlaceholder(urlPlaceholder),
|
|
320
|
+
alt: ""
|
|
302
321
|
}) : null, key && src ? /* @__PURE__ */ React.createElement(Picture, {
|
|
303
322
|
key: several ? "sizes" : key,
|
|
304
323
|
src,
|
package/dist/index.web.js.flow
CHANGED
|
@@ -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,
|
|
@@ -139,7 +146,7 @@ export type FastImageProps = $ReadOnly<{|
|
|
|
139
146
|
onProgress?: ?(event: OnProgressEvent) => void,
|
|
140
147
|
|
|
141
148
|
source?: ?(FastImageSource | Array<FastImageSource> | number),
|
|
142
|
-
placeholder?: ?(number | string | SourcePlaceholder),
|
|
149
|
+
placeholder?: ?(number | string | SourcePlaceholder | PlaceholderSource),
|
|
143
150
|
// Deprecated: use placeholder, which is the same.
|
|
144
151
|
defaultSource?: ?number,
|
|
145
152
|
|
package/dist/placeholders.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { SourcePlaceholder } from './index';
|
|
1
|
+
import type { PlaceholderSource, SourcePlaceholder } from './index';
|
|
2
2
|
export declare function isImageName(value: string): boolean;
|
|
3
|
+
export declare function placeholderSource(value: unknown): PlaceholderSource | undefined;
|
|
3
4
|
export declare function hasSourcePlaceholder(value: unknown): value is SourcePlaceholder;
|
|
4
5
|
export declare function dataUri(value: unknown): string | undefined;
|
|
@@ -34,6 +34,10 @@ typedef NS_ENUM(NSInteger, FFFObjectFit) {
|
|
|
34
34
|
@property (nonatomic, copy) NSArray<FFFastImageSource *> *sources;
|
|
35
35
|
// The `placeholder` prop.
|
|
36
36
|
@property (nonatomic, strong) UIImage *placeholder;
|
|
37
|
+
// `placeholder` as a url (a source): loaded apart from the
|
|
38
|
+
// image, at a high priority (or only from the cache, with cache cacheOnly),
|
|
39
|
+
// it shows over the other placeholders once it has.
|
|
40
|
+
@property (nonatomic, strong) FFFastImageSource *placeholderSource;
|
|
37
41
|
// The `sourcePlaceholders` prop, by the source's index (an empty string for
|
|
38
42
|
// none; one for a single source): the picked source's ThumbHash or
|
|
39
43
|
// blurDataUri (a data uri) shows instead of placeholder. JS sends them apart
|
|
@@ -70,8 +70,18 @@
|
|
|
70
70
|
// shownPlaceholder).
|
|
71
71
|
@property(nonatomic, strong) UIImage* hashImage;
|
|
72
72
|
@property(nonatomic, copy) NSString* hashImageKey;
|
|
73
|
-
//
|
|
74
|
-
//
|
|
73
|
+
// placeholderSource's image once it has loaded, or nil: it shows instead of
|
|
74
|
+
// the other placeholders (see shownPlaceholder). Its load, and which it is.
|
|
75
|
+
@property(nonatomic, strong) UIImage* urlPlaceholderImage;
|
|
76
|
+
@property(nonatomic, strong) id<SDWebImageOperation> urlPlaceholderOperation;
|
|
77
|
+
@property(nonatomic, assign) NSUInteger urlPlaceholderLoad;
|
|
78
|
+
// placeholderSource changed: its load starts with the image's, or after the
|
|
79
|
+
// props if the image is still loading (see startUrlPlaceholderLoad).
|
|
80
|
+
@property(nonatomic, assign) BOOL needsUrlPlaceholderLoad;
|
|
81
|
+
// Whether the view shows a placeholder (the placeholder prop, the source's,
|
|
82
|
+
// or placeholderSource's), and whether it stands for the image (the source's
|
|
83
|
+
// or placeholderSource's): drawn as with contain for scale-down and none, as
|
|
84
|
+
// the image's size isn't known yet, scaled smoothly and not blurred.
|
|
75
85
|
@property(nonatomic, assign) BOOL showsPlaceholder;
|
|
76
86
|
@property(nonatomic, assign) BOOL showsHash;
|
|
77
87
|
// dynamicRangeLimit switched between HDR and SDR: the image is decoded again,
|
|
@@ -444,7 +454,7 @@ static UIImage* FFFBlurredImage(UIImage* image, CGFloat scale, CGFloat radius, B
|
|
|
444
454
|
|
|
445
455
|
- (void) setImage: (UIImage*)image {
|
|
446
456
|
self.imageCount++;
|
|
447
|
-
self.showsHash = image != nil && image == self.hashImage;
|
|
457
|
+
self.showsHash = image != nil && (image == self.hashImage || image == self.urlPlaceholderImage);
|
|
448
458
|
self.showsPlaceholder = self.showsHash || (image != nil && image == _placeholder);
|
|
449
459
|
if ([_imageRendering isEqualToString: @"pixelated"]) {
|
|
450
460
|
// A source's placeholder is a blurred preview, scaled up smoothly
|
|
@@ -540,7 +550,7 @@ static UIImage* FFFBlurredImage(UIImage* image, CGFloat scale, CGFloat radius, B
|
|
|
540
550
|
// Whether the image is blurred: a loaded image, not the placeholder prop (as
|
|
541
551
|
// with React Native's Image, and on Android).
|
|
542
552
|
- (BOOL) blurs: (UIImage*)image {
|
|
543
|
-
return _blurRadius > 0 && image && image != _placeholder && image != self.hashImage;
|
|
553
|
+
return _blurRadius > 0 && image && image != _placeholder && image != self.hashImage && image != self.urlPlaceholderImage;
|
|
544
554
|
}
|
|
545
555
|
|
|
546
556
|
// Blurs the image off the main thread, and shows it (tinted as any image) unless
|
|
@@ -849,10 +859,124 @@ NSString *FFFErrorMessage(NSError *error)
|
|
|
849
859
|
[self updateHashImage];
|
|
850
860
|
}
|
|
851
861
|
|
|
852
|
-
// What shows while the image loads and if it fails:
|
|
853
|
-
//
|
|
862
|
+
// What shows while the image loads and if it fails: placeholderSource's
|
|
863
|
+
// image once it has loaded, the picked source's placeholder (its ThumbHash or
|
|
864
|
+
// blurDataUri), or the placeholder prop.
|
|
854
865
|
- (UIImage*) shownPlaceholder {
|
|
855
|
-
return self.hashImage ?: _placeholder;
|
|
866
|
+
return self.urlPlaceholderImage ?: self.hashImage ?: _placeholder;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// Shows the placeholder that shows now (shownPlaceholder) in place of
|
|
870
|
+
// `previous` if the view showed it, or if it showed nothing (one that arrived
|
|
871
|
+
// while nothing showed): never over a loaded image, or the image kept while
|
|
872
|
+
// a new source loads.
|
|
873
|
+
- (void) showChangedPlaceholder: (UIImage*)previous showing: (BOOL)showing {
|
|
874
|
+
UIImage* placeholder = [self shownPlaceholder];
|
|
875
|
+
if (placeholder == previous) {
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
BOOL showsNothing = super.image == nil && !self.showsLoadedImage && !self.blursOverNothing;
|
|
879
|
+
if (showing || (placeholder && showsNothing)) {
|
|
880
|
+
[self setImage: placeholder];
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// Images that views showed, by their source's cacheKey or url, while they're
|
|
885
|
+
// still in memory (held by a view or SDWebImage's memory cache), so
|
|
886
|
+
// placeholderSource finds one at once whatever size it was decoded at (the
|
|
887
|
+
// memory cache's key includes the size).
|
|
888
|
+
+ (NSMapTable<NSString*, UIImage*>*) shownImages {
|
|
889
|
+
static NSMapTable* images;
|
|
890
|
+
static dispatch_once_t once;
|
|
891
|
+
dispatch_once(&once, ^{
|
|
892
|
+
images = [NSMapTable strongToWeakObjectsMapTable];
|
|
893
|
+
});
|
|
894
|
+
return images;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// The key a remote image (http or https) is recorded under: its cacheKey or
|
|
898
|
+
// url. nil for others (data uris, files), which aren't recorded.
|
|
899
|
+
+ (NSString*) shownImageKey: (FFFastImageSource*)source {
|
|
900
|
+
NSString* scheme = source.url.scheme.lowercaseString;
|
|
901
|
+
if (![scheme isEqualToString: @"http"] && ![scheme isEqualToString: @"https"]) {
|
|
902
|
+
return nil;
|
|
903
|
+
}
|
|
904
|
+
return source.cacheKey.length > 0 ? source.cacheKey : source.url.absoluteString;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
+ (void) rememberShownImage: (UIImage*)image source: (FFFastImageSource*)source {
|
|
908
|
+
NSString* key = [self shownImageKey: source];
|
|
909
|
+
if (image && key) {
|
|
910
|
+
[[self shownImages] setObject: image forKey: key];
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
- (void) setPlaceholderSource: (FFFastImageSource*)placeholderSource {
|
|
915
|
+
_placeholderSource = placeholderSource;
|
|
916
|
+
[self cancelUrlPlaceholderLoad];
|
|
917
|
+
[self showUrlPlaceholderImage: nil];
|
|
918
|
+
self.needsUrlPlaceholderLoad = YES;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
- (void) cancelUrlPlaceholderLoad {
|
|
922
|
+
self.urlPlaceholderLoad++;
|
|
923
|
+
[self.urlPlaceholderOperation cancel];
|
|
924
|
+
self.urlPlaceholderOperation = nil;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// Loads placeholderSource's image with the image's load (after layout, so at
|
|
928
|
+
// the view's size, with the props applied), or after the image failed: not
|
|
929
|
+
// if the image shows at once (from the memory cache, or a data uri), or the
|
|
930
|
+
// placeholder is already loaded or loading.
|
|
931
|
+
- (void) startUrlPlaceholderLoad {
|
|
932
|
+
self.needsUrlPlaceholderLoad = NO;
|
|
933
|
+
if (!_placeholderSource.url || self.urlPlaceholderImage || self.urlPlaceholderOperation) {
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
if (_source && !self.hasErrored && [self loadsDuringLayout]) {
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
[self loadUrlPlaceholder];
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// Loads placeholderSource's image, as a source would load (its cache,
|
|
943
|
+
// headers and cacheKey), at a high priority, as it only helps if it comes
|
|
944
|
+
// before the image. One a view still shows (or that's still in memory) shows
|
|
945
|
+
// at once. A failure shows nothing more.
|
|
946
|
+
- (void) loadUrlPlaceholder {
|
|
947
|
+
[self cancelUrlPlaceholderLoad];
|
|
948
|
+
NSUInteger load = self.urlPlaceholderLoad;
|
|
949
|
+
FFFastImageSource* source = _placeholderSource;
|
|
950
|
+
NSString* key = [FFFastImageView shownImageKey: source];
|
|
951
|
+
UIImage* shown = key ? [[FFFastImageView shownImages] objectForKey: key] : nil;
|
|
952
|
+
if (shown) {
|
|
953
|
+
[self showUrlPlaceholderImage: shown];
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
SDWebImageOptions options = [source cacheOptions] | SDWebImageHighPriority | SDWebImageRetryFailed | SDWebImageHandleCookies;
|
|
957
|
+
SDWebImageContext* context = [self contextForSource: source box: [self decodeBox] cover: NO];
|
|
958
|
+
__weak typeof(self) weakSelf = self;
|
|
959
|
+
self.urlPlaceholderOperation = [[SDWebImageManager sharedManager] loadImageWithURL: source.url options: options context: context progress: nil completed: ^(UIImage* _Nullable image, NSData* _Nullable data, NSError* _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL* _Nullable imageURL) {
|
|
960
|
+
if (!finished || weakSelf.urlPlaceholderLoad != load) {
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
weakSelf.urlPlaceholderOperation = nil;
|
|
964
|
+
if (!image) {
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
[FFFastImageView rememberShownImage: image source: source];
|
|
968
|
+
[weakSelf showUrlPlaceholderImage: image];
|
|
969
|
+
}];
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
- (void) showUrlPlaceholderImage: (UIImage*)image {
|
|
973
|
+
if (self.urlPlaceholderImage == image) {
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
UIImage* previous = [self shownPlaceholder];
|
|
977
|
+
BOOL showing = self.showsPlaceholder;
|
|
978
|
+
self.urlPlaceholderImage = image;
|
|
979
|
+
[self showChangedPlaceholder: previous showing: showing];
|
|
856
980
|
}
|
|
857
981
|
|
|
858
982
|
// The picked source's ThumbHash or blurDataUri, or nil.
|
|
@@ -876,10 +1000,7 @@ NSString *FFFErrorMessage(NSError *error)
|
|
|
876
1000
|
BOOL showing = self.showsPlaceholder;
|
|
877
1001
|
self.hashImageKey = hash;
|
|
878
1002
|
self.hashImage = [FFFThumbHash imageForPlaceholder: hash];
|
|
879
|
-
|
|
880
|
-
if (showing && placeholder != previous) {
|
|
881
|
-
[self setImage: placeholder];
|
|
882
|
-
}
|
|
1003
|
+
[self showChangedPlaceholder: previous showing: showing];
|
|
883
1004
|
}
|
|
884
1005
|
|
|
885
1006
|
- (void) didSetProps {
|
|
@@ -909,6 +1030,14 @@ NSString *FFFErrorMessage(NSError *error)
|
|
|
909
1030
|
} else {
|
|
910
1031
|
[self reloadIfResized];
|
|
911
1032
|
}
|
|
1033
|
+
// A new placeholderSource while the image loads, or after it failed (a
|
|
1034
|
+
// load waiting for the view's size starts it with the image's).
|
|
1035
|
+
if (self.needsUrlPlaceholderLoad && !_needsReload && !self.waitsForSize) {
|
|
1036
|
+
if (!self.hasCompleted || self.hasErrored) {
|
|
1037
|
+
[self startUrlPlaceholderLoad];
|
|
1038
|
+
}
|
|
1039
|
+
self.needsUrlPlaceholderLoad = NO;
|
|
1040
|
+
}
|
|
912
1041
|
}
|
|
913
1042
|
|
|
914
1043
|
// The same image decoded again for a dynamicRangeLimit between HDR and SDR,
|
|
@@ -1003,6 +1132,7 @@ NSString *FFFErrorMessage(NSError *error)
|
|
|
1003
1132
|
- (void) reloadImage {
|
|
1004
1133
|
_needsReload = NO;
|
|
1005
1134
|
self.needsDynamicRangeReload = NO;
|
|
1135
|
+
self.needsUrlPlaceholderLoad = NO;
|
|
1006
1136
|
self.waitsForSize = NO;
|
|
1007
1137
|
self.loadsSwitchedSource = self.switchesSource;
|
|
1008
1138
|
self.switchesSource = NO;
|
|
@@ -1038,6 +1168,7 @@ NSString *FFFErrorMessage(NSError *error)
|
|
|
1038
1168
|
[self setImage: [self shownPlaceholder]];
|
|
1039
1169
|
self.showsLoadedImage = NO;
|
|
1040
1170
|
[self sendOnError: @"Failed to decode the image"];
|
|
1171
|
+
[self startUrlPlaceholderLoad];
|
|
1041
1172
|
if (self.onFastImageLoadEnd) {
|
|
1042
1173
|
self.onFastImageLoadEnd([self loadEndEvent: NO]);
|
|
1043
1174
|
}
|
|
@@ -1080,10 +1211,15 @@ NSString *FFFErrorMessage(NSError *error)
|
|
|
1080
1211
|
self.retriedAfterBackground = NO;
|
|
1081
1212
|
[self stopWaitingForActive];
|
|
1082
1213
|
|
|
1214
|
+
[self startUrlPlaceholderLoad];
|
|
1083
1215
|
[self downloadImage: _source options: options context: [self loadContext] events: YES];
|
|
1084
|
-
} else
|
|
1085
|
-
|
|
1086
|
-
|
|
1216
|
+
} else {
|
|
1217
|
+
if ([self shownPlaceholder]) {
|
|
1218
|
+
[self setImage: [self shownPlaceholder]];
|
|
1219
|
+
self.showsLoadedImage = NO;
|
|
1220
|
+
}
|
|
1221
|
+
// No image: only placeholders show.
|
|
1222
|
+
[self startUrlPlaceholderLoad];
|
|
1087
1223
|
}
|
|
1088
1224
|
}
|
|
1089
1225
|
|
|
@@ -1319,6 +1455,8 @@ NSString *FFFErrorMessage(NSError *error)
|
|
|
1319
1455
|
[weakSelf setImage: current];
|
|
1320
1456
|
}
|
|
1321
1457
|
weakSelf.showsLoadedImage = NO;
|
|
1458
|
+
// Shows instead of the other placeholders once it loads.
|
|
1459
|
+
[weakSelf startUrlPlaceholderLoad];
|
|
1322
1460
|
[weakSelf sendOnError: [source errorMessage: error]];
|
|
1323
1461
|
if (weakSelf.onFastImageLoadEnd) {
|
|
1324
1462
|
weakSelf.onFastImageLoadEnd([weakSelf loadEndEvent: NO]);
|
|
@@ -1326,6 +1464,12 @@ NSString *FFFErrorMessage(NSError *error)
|
|
|
1326
1464
|
} else {
|
|
1327
1465
|
weakSelf.hasCompleted = YES;
|
|
1328
1466
|
weakSelf.showsLoadedImage = YES;
|
|
1467
|
+
// For other views' placeholderSource.
|
|
1468
|
+
[FFFastImageView rememberShownImage: image source: source];
|
|
1469
|
+
// The image came first: its placeholder isn't needed.
|
|
1470
|
+
if (!weakSelf.urlPlaceholderImage) {
|
|
1471
|
+
[weakSelf cancelUrlPlaceholderLoad];
|
|
1472
|
+
}
|
|
1329
1473
|
[weakSelf sendOnLoad: image];
|
|
1330
1474
|
if (weakSelf.onFastImageLoadEnd) {
|
|
1331
1475
|
weakSelf.onFastImageLoadEnd([weakSelf loadEndEvent: YES]);
|
|
@@ -1375,11 +1519,14 @@ NSString *FFFErrorMessage(NSError *error)
|
|
|
1375
1519
|
self.waitsForSize = NO;
|
|
1376
1520
|
[self stopWaitingForActive];
|
|
1377
1521
|
[self sd_cancelLatestImageLoad];
|
|
1522
|
+
[self cancelUrlPlaceholderLoad];
|
|
1378
1523
|
}
|
|
1379
1524
|
|
|
1380
1525
|
- (void) dealloc {
|
|
1381
1526
|
[self stopWaitingForActive];
|
|
1382
1527
|
[self sd_cancelLatestImageLoad];
|
|
1528
|
+
// Fabric frees views it doesn't recycle without stop.
|
|
1529
|
+
[_urlPlaceholderOperation cancel];
|
|
1383
1530
|
}
|
|
1384
1531
|
|
|
1385
1532
|
@end
|
|
@@ -102,6 +102,22 @@ static id _Nullable FFFJSON(const folly::dynamic &value)
|
|
|
102
102
|
return value.isNull() ? nil : FFFObjectFromDynamic(value);
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
// Whether the placeholder prop is a url to load apart from the image (a
|
|
106
|
+
// source with an http or https uri), rather than an image to show at once: a
|
|
107
|
+
// bundled image (served by the packager in development), a file or a data uri.
|
|
108
|
+
static BOOL FFFIsUrlPlaceholder(id _Nullable placeholder)
|
|
109
|
+
{
|
|
110
|
+
if (![placeholder isKindOfClass:[NSDictionary class]] || placeholder[@"__packager_asset"]) {
|
|
111
|
+
return NO;
|
|
112
|
+
}
|
|
113
|
+
id uri = placeholder[@"uri"];
|
|
114
|
+
if (![uri isKindOfClass:[NSString class]]) {
|
|
115
|
+
return NO;
|
|
116
|
+
}
|
|
117
|
+
NSString *scheme = [NSURL URLWithString:uri].scheme.lowercaseString;
|
|
118
|
+
return [scheme isEqualToString:@"http"] || [scheme isEqualToString:@"https"];
|
|
119
|
+
}
|
|
120
|
+
|
|
105
121
|
// The events JS has a handler for: the handledEvents prop's bits (see
|
|
106
122
|
// src/specs/FastImageViewNativeComponent.ts).
|
|
107
123
|
typedef NS_OPTIONS(int32_t, FFFEvent) {
|
|
@@ -234,7 +250,9 @@ typedef NS_OPTIONS(int32_t, FFFEvent) {
|
|
|
234
250
|
}
|
|
235
251
|
if (all || oldViewProps.placeholder != newViewProps.placeholder) {
|
|
236
252
|
id placeholder = FFFJSON(newViewProps.placeholder);
|
|
237
|
-
|
|
253
|
+
BOOL loads = FFFIsUrlPlaceholder(placeholder);
|
|
254
|
+
view.placeholder = placeholder && !loads ? [RCTConvert UIImage:placeholder] : nil;
|
|
255
|
+
view.placeholderSource = loads ? [RCTConvert FFFastImageSource:placeholder] : nil;
|
|
238
256
|
}
|
|
239
257
|
if (all || oldViewProps.sourcePlaceholders != newViewProps.sourcePlaceholders) {
|
|
240
258
|
NSMutableArray<NSString *> *placeholders = [NSMutableArray arrayWithCapacity:newViewProps.sourcePlaceholders.size()];
|
package/package.json
CHANGED
package/src/index.js.flow
CHANGED
|
@@ -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,
|
|
@@ -139,7 +146,7 @@ export type FastImageProps = $ReadOnly<{|
|
|
|
139
146
|
onProgress?: ?(event: OnProgressEvent) => void,
|
|
140
147
|
|
|
141
148
|
source?: ?(FastImageSource | Array<FastImageSource> | number),
|
|
142
|
-
placeholder?: ?(number | string | SourcePlaceholder),
|
|
149
|
+
placeholder?: ?(number | string | SourcePlaceholder | PlaceholderSource),
|
|
143
150
|
// Deprecated: use placeholder, which is the same.
|
|
144
151
|
defaultSource?: ?number,
|
|
145
152
|
|
package/src/index.tsx
CHANGED
|
@@ -16,7 +16,12 @@ import { fromStyle, resolveObjectFit } from './objectFit'
|
|
|
16
16
|
import { objectPositionOrCentered, type ObjectPosition } from './objectPosition'
|
|
17
17
|
import FastImageView from './specs/FastImageViewNativeComponent'
|
|
18
18
|
import NativeFastImageModule from './specs/NativeFastImageModule'
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
dataUri,
|
|
21
|
+
hasSourcePlaceholder,
|
|
22
|
+
isImageName,
|
|
23
|
+
placeholderSource,
|
|
24
|
+
} from './placeholders'
|
|
20
25
|
import { warnIfChildren, warnOnce } from './warnings'
|
|
21
26
|
|
|
22
27
|
/** How the image fits the view: see [`objectFit`](#objectfit). */
|
|
@@ -175,6 +180,12 @@ export type Source = {
|
|
|
175
180
|
/** A `thumbhash` or `blurDataUri` as `placeholder`. */
|
|
176
181
|
export type SourcePlaceholder = Pick<Source, 'thumbhash' | 'blurDataUri'>
|
|
177
182
|
|
|
183
|
+
/** A url as `placeholder`, loaded like a source (see `placeholder`). */
|
|
184
|
+
export type PlaceholderSource = Pick<
|
|
185
|
+
Source,
|
|
186
|
+
'uri' | 'headers' | 'cacheKey' | 'cache'
|
|
187
|
+
>
|
|
188
|
+
|
|
178
189
|
/** How the image fades in: see [`transition`](#transition). */
|
|
179
190
|
export type Transition = {
|
|
180
191
|
/**
|
|
@@ -365,17 +376,27 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
|
|
|
365
376
|
* `placeholder={{ thumbhash }}`.
|
|
366
377
|
* - A tiny image as a data uri, shown blurred:
|
|
367
378
|
* `placeholder={{ blurDataUri }}`.
|
|
379
|
+
* - A url, loaded and cached like a source:
|
|
380
|
+
* `placeholder="https://example.com/placeholder.png"`, or a source with
|
|
381
|
+
* `headers` or a `cacheKey`: `placeholder={{ uri, headers }}`.
|
|
382
|
+
* - A url only shown if it's already cached, e.g. a list's thumbnail while
|
|
383
|
+
* the full-size image loads: `placeholder={{ uri, cache: 'cacheOnly' }}`.
|
|
368
384
|
*
|
|
369
385
|
* A source's own `thumbhash` or `blurDataUri` is shown instead, if it has
|
|
370
|
-
* one.
|
|
386
|
+
* one; a url placeholder shows over it once it has loaded.
|
|
371
387
|
*
|
|
372
388
|
* Notes:
|
|
373
389
|
*
|
|
374
|
-
* - A url isn't loaded: it's ignored, with a warning in development.
|
|
375
390
|
* - On Android, a `require()`d image doesn't show in debug builds, where
|
|
376
391
|
* the dev server serves it. Release builds show it.
|
|
392
|
+
* - On the web, a url loads like any image, also with `'cacheOnly'`: a
|
|
393
|
+
* browser can't look only in its cache.
|
|
377
394
|
*/
|
|
378
|
-
placeholder?:
|
|
395
|
+
placeholder?:
|
|
396
|
+
| ImageRequireSource
|
|
397
|
+
| string
|
|
398
|
+
| SourcePlaceholder
|
|
399
|
+
| PlaceholderSource
|
|
379
400
|
/**
|
|
380
401
|
* @deprecated Use `placeholder`, which does the same.
|
|
381
402
|
*/
|
|
@@ -706,7 +727,8 @@ function sourcePlaceholder(source: unknown): string {
|
|
|
706
727
|
}
|
|
707
728
|
|
|
708
729
|
const resolvePlaceholder = (
|
|
709
|
-
|
|
730
|
+
// A source object here has no url (e.g. { uri: name }, a bundled image).
|
|
731
|
+
placeholder?: ImageRequireSource | string | PlaceholderSource,
|
|
710
732
|
): string | object | null => {
|
|
711
733
|
if (typeof placeholder === 'string') {
|
|
712
734
|
// An image bundled with the app, by name, as React Native's Image
|
|
@@ -725,7 +747,9 @@ const resolvePlaceholder = (
|
|
|
725
747
|
if (!placeholder) {
|
|
726
748
|
return null
|
|
727
749
|
}
|
|
728
|
-
const resolved = Image.resolveAssetSource(
|
|
750
|
+
const resolved = Image.resolveAssetSource(
|
|
751
|
+
placeholder as ImageRequireSource | { uri: string },
|
|
752
|
+
)
|
|
729
753
|
if (!resolved) {
|
|
730
754
|
return null
|
|
731
755
|
}
|
|
@@ -923,13 +947,17 @@ function FastImageBase({
|
|
|
923
947
|
: undefined,
|
|
924
948
|
}
|
|
925
949
|
}, [source, shared])
|
|
926
|
-
// A
|
|
927
|
-
//
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
950
|
+
// A url in `placeholder` goes to the native view as a source, loaded
|
|
951
|
+
// apart from the image, in place of defaultSource. A thumbhash or
|
|
952
|
+
// blurDataUri goes in sourcePlaceholders instead: it isn't an image to
|
|
953
|
+
// show as it is, but defaultSource still is.
|
|
954
|
+
const resolvedPlaceholder =
|
|
955
|
+
placeholderSource(placeholder) ??
|
|
956
|
+
resolvePlaceholder(
|
|
957
|
+
hasSourcePlaceholder(placeholder)
|
|
958
|
+
? defaultSource
|
|
959
|
+
: (placeholder ?? defaultSource),
|
|
960
|
+
)
|
|
933
961
|
|
|
934
962
|
return (
|
|
935
963
|
<FastImageView
|
package/src/index.web.tsx
CHANGED
|
@@ -32,7 +32,11 @@ import { getAssetByID } from 'react-native-web/dist/modules/AssetRegistry'
|
|
|
32
32
|
import { cacheControl, priority, resizeMode } from './constants'
|
|
33
33
|
import { resolveObjectFit } from './objectFit'
|
|
34
34
|
import { cssPosition, objectPositionOrCentered } from './objectPosition'
|
|
35
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
dataUri,
|
|
37
|
+
hasSourcePlaceholder,
|
|
38
|
+
placeholderSource,
|
|
39
|
+
} from './placeholders'
|
|
36
40
|
import { warnIfChildren, warnOnce } from './warnings'
|
|
37
41
|
import type {
|
|
38
42
|
CachePathResult,
|
|
@@ -132,11 +136,20 @@ function FastImageBase({
|
|
|
132
136
|
const blurUri =
|
|
133
137
|
resolveUri(dataUri(shared(source, 'blurDataUri', preview))) ??
|
|
134
138
|
thumbhashUri(shared(source, 'thumbhash', preview))
|
|
139
|
+
// `placeholder` as a url (or a source with one) shows over the other
|
|
140
|
+
// placeholders as it loads, as on iOS and Android. The browser loads it
|
|
141
|
+
// like any image: it can't look in its cache only (cache: 'cacheOnly').
|
|
142
|
+
const urlPlaceholder = placeholderSource(placeholder)?.uri
|
|
143
|
+
const [failedPlaceholder, setFailedPlaceholder] = useState<string>()
|
|
135
144
|
// A thumbhash or blurDataUri in `placeholder` isn't an image to show as it
|
|
136
|
-
// is: defaultSource still is.
|
|
145
|
+
// is, nor is a url (above): defaultSource still is.
|
|
137
146
|
const defaultUri =
|
|
138
147
|
blurUri ??
|
|
139
|
-
resolveUri(
|
|
148
|
+
resolveUri(
|
|
149
|
+
preview || urlPlaceholder
|
|
150
|
+
? defaultSource
|
|
151
|
+
: (placeholder ?? defaultSource),
|
|
152
|
+
)
|
|
140
153
|
// Whether the image for `key` loaded, or failed.
|
|
141
154
|
const [result, setResult] = useState<{ key: string; ok: boolean }>()
|
|
142
155
|
const ok = result && result.key === key ? result.ok : undefined
|
|
@@ -156,7 +169,7 @@ function FastImageBase({
|
|
|
156
169
|
.filter(Boolean)
|
|
157
170
|
.join(' ') || undefined
|
|
158
171
|
// The placeholder shows until the image has loaded, and if it fails.
|
|
159
|
-
const showDefault =
|
|
172
|
+
const showDefault = ok !== true
|
|
160
173
|
|
|
161
174
|
useClientLayoutEffect(() => {
|
|
162
175
|
if (key) onLoadStart?.()
|
|
@@ -207,6 +220,28 @@ function FastImageBase({
|
|
|
207
220
|
alt={key ? '' : (label ?? '')}
|
|
208
221
|
/>
|
|
209
222
|
) : null}
|
|
223
|
+
{urlPlaceholder && showDefault ? (
|
|
224
|
+
<Picture
|
|
225
|
+
key={`placeholder ${urlPlaceholder}`}
|
|
226
|
+
src={urlPlaceholder}
|
|
227
|
+
loading={loading}
|
|
228
|
+
// It only helps if it comes before the image, as on iOS
|
|
229
|
+
// and Android.
|
|
230
|
+
priority="high"
|
|
231
|
+
dynamicRangeLimit={dynamicRangeLimit}
|
|
232
|
+
// It stands for the image, as a source's preview does.
|
|
233
|
+
fit={
|
|
234
|
+
fit === 'scale-down' || fit === 'none' ? 'contain' : fit
|
|
235
|
+
}
|
|
236
|
+
position={position}
|
|
237
|
+
// Tinted, not blurred, as on iOS and Android.
|
|
238
|
+
filter={tintColor ? `url(#${tintId})` : undefined}
|
|
239
|
+
// Nothing (no broken image icon) if it fails.
|
|
240
|
+
hidden={failedPlaceholder === urlPlaceholder}
|
|
241
|
+
onError={() => setFailedPlaceholder(urlPlaceholder)}
|
|
242
|
+
alt=""
|
|
243
|
+
/>
|
|
244
|
+
) : null}
|
|
210
245
|
{key && src ? (
|
|
211
246
|
<Picture
|
|
212
247
|
key={several ? 'sizes' : key}
|
package/src/placeholders.ts
CHANGED
|
@@ -1,20 +1,37 @@
|
|
|
1
|
-
// Placeholders given as strings
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
import type { SourcePlaceholder } from './index'
|
|
1
|
+
// Placeholders given as strings, shared by the native (index.tsx) and web
|
|
2
|
+
// (index.web.tsx) versions: a source's `blurDataUri` is a data uri, decoded at
|
|
3
|
+
// once, so it shows in the view's first frame (a url there is ignored, with a
|
|
4
|
+
// warning in development). `placeholder` can also be the name of an image
|
|
5
|
+
// bundled with the app, or a url, loaded as a source (placeholderSource).
|
|
6
|
+
import type { PlaceholderSource, SourcePlaceholder } from './index'
|
|
8
7
|
import { warnOnce } from './warnings'
|
|
9
8
|
|
|
10
9
|
// Metro's (and React Native's Jest preset's) development flag; other web
|
|
11
10
|
// bundlers may not define it.
|
|
12
11
|
declare const __DEV__: boolean | undefined
|
|
13
12
|
|
|
13
|
+
const SCHEME = /^[a-z][a-z\d+.-]*:/i
|
|
14
|
+
|
|
14
15
|
// Whether `placeholder` is the name of an image bundled with the app (an iOS
|
|
15
16
|
// asset catalog image, an Android drawable): no scheme, not a path.
|
|
16
17
|
export function isImageName(value: string): boolean {
|
|
17
|
-
return
|
|
18
|
+
return !SCHEME.test(value) && !value.startsWith('/')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// `placeholder` as a source to load: an http or https url, as a string or a
|
|
22
|
+
// source object's uri. Undefined for anything else (a local file or a data uri
|
|
23
|
+
// is decoded at once, as the native placeholder prop).
|
|
24
|
+
export function placeholderSource(
|
|
25
|
+
value: unknown,
|
|
26
|
+
): PlaceholderSource | undefined {
|
|
27
|
+
const uri =
|
|
28
|
+
typeof value === 'string'
|
|
29
|
+
? value
|
|
30
|
+
: value && typeof value === 'object'
|
|
31
|
+
? (value as PlaceholderSource).uri
|
|
32
|
+
: undefined
|
|
33
|
+
if (typeof uri !== 'string' || !/^https?:/i.test(uri)) return undefined
|
|
34
|
+
return typeof value === 'string' ? { uri } : (value as PlaceholderSource)
|
|
18
35
|
}
|
|
19
36
|
|
|
20
37
|
// Whether a source has a thumbhash or blurDataUri, or `placeholder` is one
|
|
@@ -37,7 +54,7 @@ export function dataUri(value: unknown): string | undefined {
|
|
|
37
54
|
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
38
55
|
warnOnce(
|
|
39
56
|
'placeholder-url',
|
|
40
|
-
`react-native-fast-image: \`
|
|
57
|
+
`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\`.`,
|
|
41
58
|
'warn',
|
|
42
59
|
)
|
|
43
60
|
}
|
|
@@ -45,8 +45,10 @@ export interface NativeProps extends ViewProps {
|
|
|
45
45
|
source?: CodegenTypes.UnsafeMixed
|
|
46
46
|
// Several sources (sizes of one image), picked from for the view's size.
|
|
47
47
|
sources?: CodegenTypes.UnsafeMixed
|
|
48
|
-
// The `placeholder` prop: a
|
|
49
|
-
//
|
|
48
|
+
// The `placeholder` prop: a url to load apart from the image, as a source
|
|
49
|
+
// ({ uri, headers, cacheKey, cache }), or an image shown at once: a
|
|
50
|
+
// resolved asset source ({ uri, width, height, scale }) or a data uri
|
|
51
|
+
// ({ uri } on iOS; the uri on Android, as a string).
|
|
50
52
|
placeholder?: CodegenTypes.UnsafeMixed
|
|
51
53
|
// What shows in place of placeholder while a source loads: its ThumbHash,
|
|
52
54
|
// or its blurDataUri (blurred natively), by the source's index ('' for
|