react-native-fast-image 10.1.1 → 10.3.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 +107 -14
- package/android/src/main/java/com/fastimage/FastImageBlur.java +3 -2
- package/android/src/main/java/com/fastimage/FastImageThumbHash.java +246 -0
- package/android/src/main/java/com/fastimage/FastImageViewConverter.java +1 -1
- package/android/src/main/java/com/fastimage/FastImageViewManager.java +20 -5
- package/android/src/main/java/com/fastimage/FastImageViewWithUrl.java +125 -48
- package/dist/index.cjs.js +47 -9
- package/dist/index.cjs.js.flow +11 -0
- package/dist/index.cjs.web.js +67 -14
- package/dist/index.cjs.web.js.flow +11 -0
- package/dist/index.d.ts +77 -13
- package/dist/index.js +48 -10
- package/dist/index.js.flow +11 -0
- package/dist/index.web.js +68 -15
- package/dist/index.web.js.flow +11 -0
- package/dist/placeholders.d.ts +4 -0
- package/dist/specs/FastImageViewNativeComponent.d.ts +2 -1
- package/dist/thumbhash.cjs.js +241 -0
- package/dist/thumbhash.d.ts +12 -0
- package/dist/thumbhash.js +240 -0
- package/ios/FastImage/FFFBoxBlur.h +7 -0
- package/ios/FastImage/FFFBoxBlur.m +29 -0
- package/ios/FastImage/FFFThumbHash.h +24 -0
- package/ios/FastImage/FFFThumbHash.m +269 -0
- package/ios/FastImage/FFFastImageView.h +7 -1
- package/ios/FastImage/FFFastImageView.m +93 -46
- package/ios/FastImage/FFFastImageViewComponentView.mm +11 -4
- package/package.json +4 -2
- package/src/index.js.flow +11 -0
- package/src/index.tsx +166 -31
- package/src/index.web.tsx +166 -33
- package/src/placeholders.ts +45 -0
- package/src/specs/FastImageViewNativeComponent.ts +8 -2
- package/src/thumbhash.ts +363 -0
- package/thumbhash/package.json +8 -0
|
@@ -61,7 +61,8 @@ import javax.annotation.Nonnull;
|
|
|
61
61
|
class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEventsView, ReactHitSlopView {
|
|
62
62
|
private boolean mNeedsReload = false;
|
|
63
63
|
private ReadableMap mSource = null;
|
|
64
|
-
|
|
64
|
+
// The `placeholder` prop.
|
|
65
|
+
private Drawable mPlaceholder = null;
|
|
65
66
|
|
|
66
67
|
// Null when the view was created in a destroyed Activity (nothing loads).
|
|
67
68
|
@Nullable
|
|
@@ -307,9 +308,74 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
|
|
|
307
308
|
return true;
|
|
308
309
|
}
|
|
309
310
|
|
|
310
|
-
public void
|
|
311
|
+
public void setPlaceholder(@Nullable Drawable placeholder) {
|
|
311
312
|
mNeedsReload = true;
|
|
312
|
-
|
|
313
|
+
mPlaceholder = placeholder;
|
|
314
|
+
}
|
|
315
|
+
|
|
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.
|
|
320
|
+
@Nullable
|
|
321
|
+
private ReadableArray mSourcePlaceholders = null;
|
|
322
|
+
// What shows while the image loads and if it fails: the picked source's
|
|
323
|
+
// ThumbHash or blurDataUri decoded (FastImageThumbHash), or the placeholder
|
|
324
|
+
// prop. FastImage's own drawable either way, which Glide doesn't recycle.
|
|
325
|
+
@Nullable
|
|
326
|
+
private Drawable mShownPlaceholder = null;
|
|
327
|
+
// The source placeholder mShownPlaceholder shows, or null for the
|
|
328
|
+
// placeholder prop.
|
|
329
|
+
@Nullable
|
|
330
|
+
private String mPlaceholderKey = null;
|
|
331
|
+
// The last source placeholder that failed to decode, so it isn't decoded
|
|
332
|
+
// again on every update.
|
|
333
|
+
@Nullable
|
|
334
|
+
private String mInvalidPlaceholder = null;
|
|
335
|
+
|
|
336
|
+
public void setSourcePlaceholders(@Nullable ReadableArray placeholders) {
|
|
337
|
+
mSourcePlaceholders = placeholders;
|
|
338
|
+
updatePlaceholder();
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// The picked source's ThumbHash or blurDataUri, or null.
|
|
342
|
+
@Nullable
|
|
343
|
+
private String pickedSourcePlaceholder() {
|
|
344
|
+
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;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Sets mShownPlaceholder for the picked source. A view showing the previous
|
|
351
|
+
// placeholder shows the new one at once, without reloading the image.
|
|
352
|
+
private void updatePlaceholder() {
|
|
353
|
+
String key = pickedSourcePlaceholder();
|
|
354
|
+
Drawable placeholder;
|
|
355
|
+
if (key != null && key.equals(mPlaceholderKey)) {
|
|
356
|
+
placeholder = mShownPlaceholder;
|
|
357
|
+
} else if (key != null && key.equals(mInvalidPlaceholder)) {
|
|
358
|
+
placeholder = mPlaceholder;
|
|
359
|
+
mPlaceholderKey = null;
|
|
360
|
+
} else {
|
|
361
|
+
Bitmap bitmap = FastImageThumbHash.placeholder(key);
|
|
362
|
+
if (bitmap != null) {
|
|
363
|
+
BitmapDrawable drawable = new BitmapDrawable(getResources(), bitmap);
|
|
364
|
+
// Scaled up smoothly, as a blurred preview (also with
|
|
365
|
+
// imageRendering pixelated, which is for the image).
|
|
366
|
+
drawable.setFilterBitmap(true);
|
|
367
|
+
placeholder = drawable;
|
|
368
|
+
mPlaceholderKey = key;
|
|
369
|
+
} else {
|
|
370
|
+
placeholder = mPlaceholder;
|
|
371
|
+
mPlaceholderKey = null;
|
|
372
|
+
mInvalidPlaceholder = key;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
if (placeholder == mShownPlaceholder) return;
|
|
376
|
+
boolean showing = mShowsPlaceholder;
|
|
377
|
+
mShownPlaceholder = placeholder;
|
|
378
|
+
if (showing) setImageDrawable(placeholder);
|
|
313
379
|
}
|
|
314
380
|
|
|
315
381
|
// The events JS has a handler for (the handledEvents prop): the view only
|
|
@@ -482,7 +548,7 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
|
|
|
482
548
|
private ValueAnimator mFade;
|
|
483
549
|
@Nullable
|
|
484
550
|
private Bitmap mFadeFrom;
|
|
485
|
-
// Or, instead of a copy, the
|
|
551
|
+
// Or, instead of a copy, the placeholder prop the view showed, with the
|
|
486
552
|
// matrix it drew it with: FastImage's own drawable, which Glide doesn't
|
|
487
553
|
// recycle, so it's drawn again as it was, rather than copied into a
|
|
488
554
|
// bitmap the size of the view for every image that fades in over it.
|
|
@@ -508,7 +574,7 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
|
|
|
508
574
|
Drawable fromDrawable = null;
|
|
509
575
|
Matrix fromMatrix = null;
|
|
510
576
|
Drawable shown = getDrawable();
|
|
511
|
-
if (shown != null &&
|
|
577
|
+
if (shown != null && mShowsPlaceholder && mFade == null) {
|
|
512
578
|
// Its own state, so its alpha (see drawFadeFrom) is only its own.
|
|
513
579
|
fromDrawable = shown.mutate();
|
|
514
580
|
fromMatrix = new Matrix(getImageMatrix());
|
|
@@ -564,7 +630,7 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
|
|
|
564
630
|
invalidate();
|
|
565
631
|
}
|
|
566
632
|
|
|
567
|
-
// Draws the
|
|
633
|
+
// Draws the placeholder prop the view showed when the fade started (with
|
|
568
634
|
// `alpha`), as ImageView draws its drawable: inside the padding
|
|
569
635
|
// (cropToPadding), with the matrix it had then.
|
|
570
636
|
private void drawFadeFrom(Canvas canvas, Drawable drawable, int alpha) {
|
|
@@ -1001,14 +1067,14 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
|
|
|
1001
1067
|
: null;
|
|
1002
1068
|
}
|
|
1003
1069
|
|
|
1004
|
-
// Whether the image on screen is a cover one Glide cropped to what the
|
|
1005
|
-
//
|
|
1006
|
-
//
|
|
1007
|
-
//
|
|
1008
|
-
//
|
|
1070
|
+
// Whether the image on screen is a cover one Glide cropped to what the view
|
|
1071
|
+
// shows (CenterCrop or FastImageCrop, at the position it was loaded for).
|
|
1072
|
+
// Glide can't crop animated WebPs and AVIFs, APNGs and the placeholder
|
|
1073
|
+
// prop, and doesn't crop pixelated images or ones decoded at full size
|
|
1074
|
+
// (downsample false).
|
|
1009
1075
|
private boolean shownCropped(Drawable drawable) {
|
|
1010
1076
|
return mLayoutScaleType == ScaleType.CENTER_CROP && !mLayoutPixelated && mLayoutDownsampled
|
|
1011
|
-
&& drawable !=
|
|
1077
|
+
&& drawable != mShownPlaceholder
|
|
1012
1078
|
&& (drawable instanceof BitmapDrawable || drawable instanceof GifDrawable);
|
|
1013
1079
|
}
|
|
1014
1080
|
|
|
@@ -1025,13 +1091,14 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
|
|
|
1025
1091
|
updateImageMatrix();
|
|
1026
1092
|
}
|
|
1027
1093
|
|
|
1028
|
-
// Whether the view shows its
|
|
1029
|
-
// which Glide doesn't recycle (see
|
|
1030
|
-
|
|
1094
|
+
// Whether the view shows its placeholder (the placeholder prop, or a
|
|
1095
|
+
// source's): FastImage's own drawable, which Glide doesn't recycle (see
|
|
1096
|
+
// startFade).
|
|
1097
|
+
private boolean mShowsPlaceholder = false;
|
|
1031
1098
|
|
|
1032
1099
|
@Override
|
|
1033
1100
|
public void setImageDrawable(@Nullable Drawable drawable) {
|
|
1034
|
-
|
|
1101
|
+
mShowsPlaceholder = drawable != null && drawable == mShownPlaceholder;
|
|
1035
1102
|
super.setImageDrawable(drawable);
|
|
1036
1103
|
updateImageMatrix();
|
|
1037
1104
|
}
|
|
@@ -1082,7 +1149,7 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
|
|
|
1082
1149
|
// edges at (view - image) * fraction + offset. Glide decodes a scale-down
|
|
1083
1150
|
// image no larger than that (CenterInside), a none one at its size, and
|
|
1084
1151
|
// crops a cover one to what the view shows (FastImageCrop), so it's drawn
|
|
1085
|
-
// at this size.
|
|
1152
|
+
// at this size. The placeholder prop (a resource) is already at its size on
|
|
1086
1153
|
// screen, and so is a center image (its size in pixels, as ImageView's
|
|
1087
1154
|
// CENTER_INSIDE draws it).
|
|
1088
1155
|
private void updateImageMatrix() {
|
|
@@ -1116,13 +1183,17 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
|
|
|
1116
1183
|
setImageMatrix(matrix);
|
|
1117
1184
|
return;
|
|
1118
1185
|
}
|
|
1119
|
-
|
|
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;
|
|
1190
|
+
if (mLayoutScaleType == ScaleType.CENTER_CROP || mLayoutScaleType == ScaleType.FIT_CENTER || hashFits) {
|
|
1120
1191
|
float x = viewWidth / drawableWidth;
|
|
1121
1192
|
float y = viewHeight / drawableHeight;
|
|
1122
1193
|
float scale = mLayoutScaleType == ScaleType.CENTER_CROP ? Math.max(x, y) : Math.min(x, y);
|
|
1123
1194
|
width = drawableWidth * scale;
|
|
1124
1195
|
height = drawableHeight * scale;
|
|
1125
|
-
} else if (drawable ==
|
|
1196
|
+
} else if (drawable == mShownPlaceholder) {
|
|
1126
1197
|
width = drawableWidth;
|
|
1127
1198
|
height = drawableHeight;
|
|
1128
1199
|
} else if (mImageWidth > 0 && mImageHeight > 0) {
|
|
@@ -1239,12 +1310,12 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
|
|
|
1239
1310
|
private boolean mRestarting = false;
|
|
1240
1311
|
|
|
1241
1312
|
// The request for the image the view shows (from when it has loaded until
|
|
1242
|
-
// another image or
|
|
1243
|
-
// source starts with the shown one as a thumbnail, from the cache
|
|
1244
|
-
// the image stays until the new one has loaded instead of flashing
|
|
1245
|
-
// (#747), also if the source changes again meanwhile. This is Glide's
|
|
1246
|
-
// way to do that: the previous bitmap can't be kept in the view
|
|
1247
|
-
// since Glide reuses it once its request is cleared.
|
|
1313
|
+
// another image or the placeholder prop replaces it), and the one loading.
|
|
1314
|
+
// A new source starts with the shown one as a thumbnail, from the cache
|
|
1315
|
+
// only, so the image stays until the new one has loaded instead of flashing
|
|
1316
|
+
// blank (#747), also if the source changes again meanwhile. This is Glide's
|
|
1317
|
+
// safe way to do that: the previous bitmap can't be kept in the view
|
|
1318
|
+
// itself, since Glide reuses it once its request is cleared.
|
|
1248
1319
|
@Nullable
|
|
1249
1320
|
private RequestBuilder<Drawable> mShownRequest;
|
|
1250
1321
|
@Nullable
|
|
@@ -1270,8 +1341,8 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
|
|
|
1270
1341
|
mRecyclingKey = recyclingKey;
|
|
1271
1342
|
if (changed) {
|
|
1272
1343
|
// No thumbnail of the current image, so Glide clears the view to
|
|
1273
|
-
//
|
|
1274
|
-
// from it. Reload even if the source is the same.
|
|
1344
|
+
// the placeholder prop (or nothing) while the next one loads, nor a
|
|
1345
|
+
// fade from it. Reload even if the source is the same.
|
|
1275
1346
|
mShownRequest = null;
|
|
1276
1347
|
endFade();
|
|
1277
1348
|
mNeedsReload = true;
|
|
@@ -1356,12 +1427,12 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
|
|
|
1356
1427
|
// reloadForSize does: it's decoded again from the cache and blurred or
|
|
1357
1428
|
// fitted on Glide's threads, and the view keeps the current image (and its
|
|
1358
1429
|
// layout, see onImageLoaded) until then. If that fails, it shows the image
|
|
1359
|
-
// from Glide's caches; cache: 'web' skips them, so the view shows
|
|
1360
|
-
//
|
|
1361
|
-
// (e.g. the blur changes every frame): that load is replaced. A load
|
|
1362
|
-
// hasn't finished starts again with the new blur, without a second
|
|
1363
|
-
// onLoadStart. Nothing loaded (
|
|
1364
|
-
// nothing): the next load has the new blur.
|
|
1430
|
+
// from Glide's caches; cache: 'web' skips them, so the view shows the
|
|
1431
|
+
// placeholder prop (or nothing) then. Also while it's loading again that
|
|
1432
|
+
// way (e.g. the blur changes every frame): that load is replaced. A load
|
|
1433
|
+
// that hasn't finished starts again with the new blur, without a second
|
|
1434
|
+
// onLoadStart. Nothing loaded (the placeholder prop, which isn't blurred,
|
|
1435
|
+
// or nothing): the next load has the new blur.
|
|
1365
1436
|
@SuppressLint("CheckResult")
|
|
1366
1437
|
private void reblur() {
|
|
1367
1438
|
if (requestManager == null || mBaseRequest == null) return;
|
|
@@ -1445,20 +1516,24 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
|
|
|
1445
1516
|
mLoadEnded = true;
|
|
1446
1517
|
}
|
|
1447
1518
|
|
|
1448
|
-
// The loading image failed (FastImageRequestListener). Glide shows
|
|
1449
|
-
//
|
|
1519
|
+
// The loading image failed (FastImageRequestListener). Glide shows the
|
|
1520
|
+
// placeholder prop then, but not over a thumbnail: show it here, as iOS
|
|
1521
|
+
// does.
|
|
1450
1522
|
void onImageFailed(boolean hadThumbnail) {
|
|
1451
1523
|
mLoadEnded = true;
|
|
1452
1524
|
endProgress();
|
|
1453
1525
|
applyObjectFit();
|
|
1454
1526
|
mShownRequest = null;
|
|
1455
|
-
if (!hadThumbnail) return;
|
|
1456
1527
|
final int load = mLoadCount;
|
|
1457
|
-
// Not from within Glide's callback.
|
|
1528
|
+
// Not from within Glide's callback. Without a thumbnail Glide shows the
|
|
1529
|
+
// placeholder the request started with: a source placeholder that changed since
|
|
1530
|
+
// shows the new one.
|
|
1458
1531
|
post(new Runnable() {
|
|
1459
1532
|
@Override
|
|
1460
1533
|
public void run() {
|
|
1461
|
-
if (load == mLoadCount
|
|
1534
|
+
if (load == mLoadCount && (hadThumbnail || getDrawable() != mShownPlaceholder)) {
|
|
1535
|
+
setImageDrawable(mShownPlaceholder);
|
|
1536
|
+
}
|
|
1462
1537
|
}
|
|
1463
1538
|
});
|
|
1464
1539
|
}
|
|
@@ -1499,8 +1574,9 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
|
|
|
1499
1574
|
mWaitedForSize = false;
|
|
1500
1575
|
pickSource();
|
|
1501
1576
|
}
|
|
1577
|
+
updatePlaceholder();
|
|
1502
1578
|
// Only reload for changes that affect the request (source,
|
|
1503
|
-
//
|
|
1579
|
+
// the placeholder prop, objectFit), not for every prop update.
|
|
1504
1580
|
mNeedsReload = false;
|
|
1505
1581
|
mBlurChanged = false;
|
|
1506
1582
|
mFitChanged = false;
|
|
@@ -1520,7 +1596,7 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
|
|
|
1520
1596
|
mModel = null;
|
|
1521
1597
|
|
|
1522
1598
|
// Nothing to show.
|
|
1523
|
-
if (mSource == null &&
|
|
1599
|
+
if (mSource == null && mShownPlaceholder == null) {
|
|
1524
1600
|
mShownRequest = null;
|
|
1525
1601
|
// Cancel existing requests.
|
|
1526
1602
|
clearView(requestManager);
|
|
@@ -1541,8 +1617,8 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
|
|
|
1541
1617
|
: null;
|
|
1542
1618
|
|
|
1543
1619
|
// A source without a uri (empty, missing or null), or one that can't be
|
|
1544
|
-
// resolved: fire onError and onLoadEnd and show
|
|
1545
|
-
// (#1028, #945).
|
|
1620
|
+
// resolved: fire onError and onLoadEnd and show the placeholder prop,
|
|
1621
|
+
// as on iOS (#1028, #945).
|
|
1546
1622
|
if (mSource != null && (imageSource == null || imageSource.getUri().toString().length() == 0)) {
|
|
1547
1623
|
String error = "Invalid source: " + mSource;
|
|
1548
1624
|
WritableMap event = new WritableNativeMap();
|
|
@@ -1555,12 +1631,13 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
|
|
|
1555
1631
|
clearView(requestManager);
|
|
1556
1632
|
|
|
1557
1633
|
endProgress();
|
|
1558
|
-
setImageDrawable(
|
|
1634
|
+
setImageDrawable(mShownPlaceholder);
|
|
1559
1635
|
return;
|
|
1560
1636
|
}
|
|
1561
1637
|
|
|
1562
|
-
// `imageSource` may be null and we still continue, if
|
|
1563
|
-
// What Glide loads: a FastImageUrl for a remote
|
|
1638
|
+
// `imageSource` may be null and we still continue, if the placeholder
|
|
1639
|
+
// prop is not null. What Glide loads: a FastImageUrl for a remote
|
|
1640
|
+
// image.
|
|
1564
1641
|
Object model = imageSource == null ? null : imageSource.getSourceForLoad();
|
|
1565
1642
|
|
|
1566
1643
|
// Before the view clears (see shownCopy).
|
|
@@ -1600,8 +1677,8 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
|
|
|
1600
1677
|
.load(model)
|
|
1601
1678
|
.apply(FastImageViewConverter
|
|
1602
1679
|
.getOptions(getContext(), imageSource, mSource)
|
|
1603
|
-
.placeholder(
|
|
1604
|
-
.fallback(
|
|
1680
|
+
.placeholder(mShownPlaceholder) // show until loaded
|
|
1681
|
+
.fallback(mShownPlaceholder)); // null will not be treated as error
|
|
1605
1682
|
mBaseRequest = model == null ? null : base;
|
|
1606
1683
|
mModel = model;
|
|
1607
1684
|
// What into() would apply for the scale type, with the size
|
|
@@ -1609,7 +1686,7 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
|
|
|
1609
1686
|
RequestBuilder<Drawable> builder = base.clone().apply(renderingOptions(model));
|
|
1610
1687
|
RequestBuilder<Drawable> request = builder.clone();
|
|
1611
1688
|
|
|
1612
|
-
// Only
|
|
1689
|
+
// Only the placeholder prop: it replaces the image.
|
|
1613
1690
|
if (model == null) {
|
|
1614
1691
|
meanwhile = null;
|
|
1615
1692
|
mShownRequest = null;
|
package/dist/index.cjs.js
CHANGED
|
@@ -260,10 +260,39 @@ var FastImageViewNativeComponent_default = (0, react_native.codegenNativeCompone
|
|
|
260
260
|
//#region src/specs/NativeFastImageModule.ts
|
|
261
261
|
var NativeFastImageModule_default = react_native.TurboModuleRegistry.get("FastImageModule");
|
|
262
262
|
//#endregion
|
|
263
|
+
//#region src/placeholders.ts
|
|
264
|
+
function isImageName(value) {
|
|
265
|
+
return !/^[a-z][a-z\d+.-]*:/i.test(value) && !value.startsWith("/");
|
|
266
|
+
}
|
|
267
|
+
function hasSourcePlaceholder(value) {
|
|
268
|
+
return !!value && typeof value === "object" && ("thumbhash" in value || "blurDataUri" in value);
|
|
269
|
+
}
|
|
270
|
+
function dataUri(value) {
|
|
271
|
+
if (typeof value !== "string" || value === "") return void 0;
|
|
272
|
+
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");
|
|
274
|
+
}
|
|
275
|
+
//#endregion
|
|
263
276
|
//#region src/index.tsx
|
|
264
|
-
|
|
265
|
-
if (!
|
|
266
|
-
const
|
|
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
|
+
const resolvePlaceholder = (placeholder) => {
|
|
288
|
+
if (typeof placeholder === "string") {
|
|
289
|
+
if (isImageName(placeholder)) return react_native.Platform.OS === "android" ? placeholder : { uri: placeholder };
|
|
290
|
+
const uri = dataUri(placeholder);
|
|
291
|
+
if (!uri) return null;
|
|
292
|
+
return react_native.Platform.OS === "android" ? uri : { uri };
|
|
293
|
+
}
|
|
294
|
+
if (!placeholder) return null;
|
|
295
|
+
const resolved = react_native.Image.resolveAssetSource(placeholder);
|
|
267
296
|
if (!resolved) return null;
|
|
268
297
|
return react_native.Platform.OS === "android" ? resolved.uri : resolved;
|
|
269
298
|
};
|
|
@@ -318,7 +347,7 @@ function builtInTransition() {
|
|
|
318
347
|
return resolveTransition(react_native.Platform.OS === "android", NO_TRANSITION);
|
|
319
348
|
}
|
|
320
349
|
let defaultTransition;
|
|
321
|
-
function FastImageBase({ source, defaultSource, tintColor, onLoadStart, onProgress, onLoad, onError, onLoadEnd, style, children, objectFit, objectPosition, loop, transition, forwardedRef, ...viewProps }) {
|
|
350
|
+
function FastImageBase({ source, placeholder, defaultSource, tintColor, onLoadStart, onProgress, onLoad, onError, onLoadEnd, style, children, objectFit, objectPosition, loop, transition, loading: _loading, sizes: _sizes, forwardedRef, ...viewProps }) {
|
|
322
351
|
var _props$recyclingKey, _defaultTransition;
|
|
323
352
|
const { fallback, resizeMode: _resizeMode, ...props } = viewProps;
|
|
324
353
|
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");
|
|
@@ -326,10 +355,18 @@ function FastImageBase({ source, defaultSource, tintColor, onLoadStart, onProgre
|
|
|
326
355
|
const resolvedTintColor = tintColor != null ? tintColor : fromStyle(style, "tintColor");
|
|
327
356
|
const fit = resolveObjectFit(objectFit, style);
|
|
328
357
|
const position = objectPositionOrCentered(objectPosition);
|
|
329
|
-
const
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
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);
|
|
333
370
|
return /* @__PURE__ */ react.default.createElement(FastImageViewNativeComponent_default, {
|
|
334
371
|
...props,
|
|
335
372
|
ref: forwardedRef,
|
|
@@ -339,7 +376,8 @@ function FastImageBase({ source, defaultSource, tintColor, onLoadStart, onProgre
|
|
|
339
376
|
...resolveTransition(transition, (_defaultTransition = defaultTransition) !== null && _defaultTransition !== void 0 ? _defaultTransition : builtInTransition()),
|
|
340
377
|
source: resolvedSource,
|
|
341
378
|
sources,
|
|
342
|
-
|
|
379
|
+
sourcePlaceholders,
|
|
380
|
+
placeholder: resolvedPlaceholder,
|
|
343
381
|
onFastImageLoadStart: onLoadStart,
|
|
344
382
|
onFastImageProgress: onProgress,
|
|
345
383
|
handledEvents: handledEvents({
|
package/dist/index.cjs.js.flow
CHANGED
|
@@ -110,6 +110,11 @@ export type PreloadResult = PreloadSuccess | PreloadFailure
|
|
|
110
110
|
export type PreloadFn = (
|
|
111
111
|
sources: Array<FastImageSource>,
|
|
112
112
|
) => Promise<Array<PreloadResult>>
|
|
113
|
+
export type SourcePlaceholder = {
|
|
114
|
+
thumbhash?: string,
|
|
115
|
+
blurDataUri?: string,
|
|
116
|
+
}
|
|
117
|
+
|
|
113
118
|
export type FastImageSource = {
|
|
114
119
|
uri?: string,
|
|
115
120
|
headers?: Object,
|
|
@@ -118,6 +123,8 @@ export type FastImageSource = {
|
|
|
118
123
|
cacheKey?: string,
|
|
119
124
|
memoryCache?: boolean,
|
|
120
125
|
diskCache?: boolean,
|
|
126
|
+
thumbhash?: string,
|
|
127
|
+
blurDataUri?: string,
|
|
121
128
|
width?: number,
|
|
122
129
|
height?: number,
|
|
123
130
|
scale?: number,
|
|
@@ -132,6 +139,8 @@ export type FastImageProps = $ReadOnly<{|
|
|
|
132
139
|
onProgress?: ?(event: OnProgressEvent) => void,
|
|
133
140
|
|
|
134
141
|
source?: ?(FastImageSource | Array<FastImageSource> | number),
|
|
142
|
+
placeholder?: ?(number | string | SourcePlaceholder),
|
|
143
|
+
// Deprecated: use placeholder, which is the same.
|
|
135
144
|
defaultSource?: ?number,
|
|
136
145
|
|
|
137
146
|
tintColor?: number | string,
|
|
@@ -140,6 +149,8 @@ export type FastImageProps = $ReadOnly<{|
|
|
|
140
149
|
recyclingKey?: ?string,
|
|
141
150
|
loop?: ?(boolean | number),
|
|
142
151
|
imageRendering?: ?('auto' | 'smooth' | 'pixelated'),
|
|
152
|
+
loading?: ?('lazy' | 'eager'),
|
|
153
|
+
sizes?: ?string,
|
|
143
154
|
paused?: ?boolean,
|
|
144
155
|
transition?: ?(boolean | number | Transition),
|
|
145
156
|
downsample?: ?boolean,
|
package/dist/index.cjs.web.js
CHANGED
|
@@ -267,17 +267,24 @@ function cssPosition({ x, y }) {
|
|
|
267
267
|
return `${axis(x)} ${axis(y)}`;
|
|
268
268
|
}
|
|
269
269
|
//#endregion
|
|
270
|
+
//#region src/placeholders.ts
|
|
271
|
+
function hasSourcePlaceholder(value) {
|
|
272
|
+
return !!value && typeof value === "object" && ("thumbhash" in value || "blurDataUri" in value);
|
|
273
|
+
}
|
|
274
|
+
function dataUri(value) {
|
|
275
|
+
if (typeof value !== "string" || value === "") return void 0;
|
|
276
|
+
if (/^data:/i.test(value)) return value;
|
|
277
|
+
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");
|
|
278
|
+
}
|
|
279
|
+
//#endregion
|
|
270
280
|
//#region src/index.web.tsx
|
|
271
|
-
var _useId;
|
|
272
281
|
const notSupported = {
|
|
273
282
|
ok: false,
|
|
274
283
|
error: "Not supported on the web"
|
|
275
284
|
};
|
|
276
|
-
let ids = 0;
|
|
277
|
-
const useCounterId = () => (0, react.useState)(() => `${++ids}`)[0];
|
|
278
|
-
const useSvgId = (_useId = react.default.useId) !== null && _useId !== void 0 ? _useId : useCounterId;
|
|
279
285
|
const useClientLayoutEffect = globalThis.document === void 0 ? react.useEffect : react.useLayoutEffect;
|
|
280
|
-
function FastImageBase({ source, defaultSource, tintColor, blurRadius, onLoadStart, onLoad, onError, onLoadEnd, style, children, objectFit, objectPosition, forwardedRef, onLayout, pointerEvents, recyclingKey: _recyclingKey, loop: _loop, imageRendering: _imageRendering, paused: _paused, transition: _transition, downsample: _downsample, onProgress: _onProgress, ...props }) {
|
|
286
|
+
function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadius, onLoadStart, onLoad, onError, onLoadEnd, style, children, objectFit, objectPosition, loading, sizes, forwardedRef, onLayout, pointerEvents, recyclingKey: _recyclingKey, loop: _loop, imageRendering: _imageRendering, paused: _paused, transition: _transition, downsample: _downsample, onProgress: _onProgress, ...props }) {
|
|
287
|
+
var _resolveUri;
|
|
281
288
|
if (typeof __DEV__ !== "undefined" && __DEV__) warnIfChildren(children);
|
|
282
289
|
const { accessibilityLabel, "aria-label": ariaLabel, resizeMode: _resizeMode, ...viewProps } = props;
|
|
283
290
|
const label = ariaLabel || accessibilityLabel || void 0;
|
|
@@ -286,10 +293,15 @@ function FastImageBase({ source, defaultSource, tintColor, blurRadius, onLoadSta
|
|
|
286
293
|
const src = sized ? sized.src : resolveUri(source);
|
|
287
294
|
const srcSet = sized === null || sized === void 0 ? void 0 : sized.srcSet;
|
|
288
295
|
const key = srcSet !== null && srcSet !== void 0 ? srcSet : src;
|
|
289
|
-
const
|
|
296
|
+
const priority = sourcePriority(source);
|
|
297
|
+
const preview = hasSourcePlaceholder(placeholder) ? placeholder : void 0;
|
|
298
|
+
const blurUri = (_resolveUri = resolveUri(dataUri(shared(source, "blurDataUri", preview)))) !== null && _resolveUri !== void 0 ? _resolveUri : thumbhashUri(shared(source, "thumbhash", preview));
|
|
299
|
+
const defaultUri = blurUri !== null && blurUri !== void 0 ? blurUri : resolveUri(preview ? defaultSource : placeholder !== null && placeholder !== void 0 ? placeholder : defaultSource);
|
|
290
300
|
const [result, setResult] = (0, react.useState)();
|
|
291
301
|
const ok = result && result.key === key ? result.ok : void 0;
|
|
292
|
-
const
|
|
302
|
+
const svgId = (0, react.useId)().replace(/[^\w-]/g, "");
|
|
303
|
+
const tintId = `fast-image-tint-${svgId}`;
|
|
304
|
+
const blurId = `fast-image-blur-${svgId}`;
|
|
293
305
|
const fit = resolveObjectFit(objectFit, style);
|
|
294
306
|
const position = cssPosition(objectPositionOrCentered(objectPosition));
|
|
295
307
|
const filter = [blurRadius ? `blur(${blurRadius}px)` : "", tintColor ? `url(#${tintId})` : ""].filter(Boolean).join(" ") || void 0;
|
|
@@ -307,15 +319,19 @@ function FastImageBase({ source, defaultSource, tintColor, blurRadius, onLoadSta
|
|
|
307
319
|
}, defaultUri && showDefault ? /* @__PURE__ */ react.default.createElement(Picture, {
|
|
308
320
|
key: `default ${defaultUri}`,
|
|
309
321
|
src: defaultUri,
|
|
310
|
-
|
|
322
|
+
loading,
|
|
323
|
+
priority,
|
|
324
|
+
fit: blurUri && (fit === "scale-down" || fit === "none") ? "contain" : fit,
|
|
311
325
|
position,
|
|
312
|
-
filter,
|
|
326
|
+
filter: blurUri ? [`url(#${blurId})`, tintColor ? `url(#${tintId})` : ""].filter(Boolean).join(" ") : filter,
|
|
313
327
|
alt: key ? "" : label !== null && label !== void 0 ? label : ""
|
|
314
328
|
}) : null, key && src ? /* @__PURE__ */ react.default.createElement(Picture, {
|
|
315
329
|
key: several ? "sizes" : key,
|
|
316
330
|
src,
|
|
317
331
|
srcSet,
|
|
318
|
-
|
|
332
|
+
sizes,
|
|
333
|
+
loading,
|
|
334
|
+
priority,
|
|
319
335
|
fit,
|
|
320
336
|
position,
|
|
321
337
|
filter,
|
|
@@ -352,7 +368,29 @@ function FastImageBase({ source, defaultSource, tintColor, blurRadius, onLoadSta
|
|
|
352
368
|
}) : null, tintColor ? /* @__PURE__ */ react.default.createElement(TintFilter, {
|
|
353
369
|
id: tintId,
|
|
354
370
|
color: tintColor
|
|
355
|
-
}) : null);
|
|
371
|
+
}) : null, blurUri && showDefault ? /* @__PURE__ */ react.default.createElement(PreviewBlur, { id: blurId }) : null);
|
|
372
|
+
}
|
|
373
|
+
function thumbhashUri(hash) {
|
|
374
|
+
if (!hash) return void 0;
|
|
375
|
+
const decode = globalThis[Symbol.for("react-native-fast-image/thumbhash")];
|
|
376
|
+
if (decode) return decode(hash);
|
|
377
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) warnOnce("thumbhash-web", "react-native-fast-image: on the web, a `thumbhash` shows only with `import 'react-native-fast-image/thumbhash'` (it decodes them). Or make it a `blurDataUri` before it reaches the browser (e.g. on your server) with that module's thumbHashToBlurDataUri, which needs no JavaScript in the browser.", "warn");
|
|
378
|
+
}
|
|
379
|
+
function sourcePriority(source) {
|
|
380
|
+
const values = (Array.isArray(source) ? source : [source]).map((entry) => entry && typeof entry === "object" ? entry.priority : void 0);
|
|
381
|
+
return [
|
|
382
|
+
"high",
|
|
383
|
+
"normal",
|
|
384
|
+
"low"
|
|
385
|
+
].find((priority) => values.includes(priority));
|
|
386
|
+
}
|
|
387
|
+
function shared(source, key, preview) {
|
|
388
|
+
const values = (Array.isArray(source) ? source : [source]).map((entry) => {
|
|
389
|
+
var _ref;
|
|
390
|
+
const own = entry && typeof entry === "object" ? entry : void 0;
|
|
391
|
+
return (_ref = (own === null || own === void 0 ? void 0 : own.thumbhash) || dataUri(own === null || own === void 0 ? void 0 : own.blurDataUri) ? own : preview) === null || _ref === void 0 ? void 0 : _ref[key];
|
|
392
|
+
});
|
|
393
|
+
return values.every((value) => value && value === values[0]) ? values[0] : void 0;
|
|
356
394
|
}
|
|
357
395
|
function resolveUri(source) {
|
|
358
396
|
let uri;
|
|
@@ -400,7 +438,7 @@ const fill = {
|
|
|
400
438
|
width: "100%",
|
|
401
439
|
height: "100%"
|
|
402
440
|
};
|
|
403
|
-
function Picture({ src, srcSet,
|
|
441
|
+
function Picture({ src, srcSet, sizes, loading, priority, fit, position, filter, hidden, alt, onLoad, onError }) {
|
|
404
442
|
const image = (0, react.useRef)(null);
|
|
405
443
|
const mounted = (0, react.useRef)(true);
|
|
406
444
|
const handled = (0, react.useRef)(void 0);
|
|
@@ -437,8 +475,9 @@ function Picture({ src, srcSet, lazy, fit, position, filter, hidden, alt, onLoad
|
|
|
437
475
|
ref: image,
|
|
438
476
|
src,
|
|
439
477
|
srcSet,
|
|
440
|
-
sizes: srcSet ? "auto, 100vw" : void 0,
|
|
441
|
-
loading:
|
|
478
|
+
sizes: srcSet ? sizes !== null && sizes !== void 0 ? sizes : "auto, 100vw" : void 0,
|
|
479
|
+
loading: loading === "eager" ? void 0 : "lazy",
|
|
480
|
+
fetchPriority: priority === "high" || priority === "low" ? priority : void 0,
|
|
442
481
|
alt,
|
|
443
482
|
draggable: false,
|
|
444
483
|
style: {
|
|
@@ -464,6 +503,20 @@ function TintFilter({ id, color }) {
|
|
|
464
503
|
operator: "in"
|
|
465
504
|
}))));
|
|
466
505
|
}
|
|
506
|
+
function PreviewBlur({ id }) {
|
|
507
|
+
return /* @__PURE__ */ react.default.createElement("svg", { style: tintStyle }, /* @__PURE__ */ react.default.createElement("defs", null, /* @__PURE__ */ react.default.createElement("filter", {
|
|
508
|
+
id,
|
|
509
|
+
x: "0",
|
|
510
|
+
y: "0",
|
|
511
|
+
width: "1",
|
|
512
|
+
height: "1",
|
|
513
|
+
primitiveUnits: "objectBoundingBox",
|
|
514
|
+
colorInterpolationFilters: "sRGB"
|
|
515
|
+
}, /* @__PURE__ */ react.default.createElement("feGaussianBlur", { stdDeviation: "0.06" }), /* @__PURE__ */ react.default.createElement("feComponentTransfer", null, /* @__PURE__ */ react.default.createElement("feFuncA", {
|
|
516
|
+
type: "table",
|
|
517
|
+
tableValues: "0 1 1 1 1"
|
|
518
|
+
})))));
|
|
519
|
+
}
|
|
467
520
|
const tintStyle = {
|
|
468
521
|
position: "absolute",
|
|
469
522
|
width: 0,
|
|
@@ -110,6 +110,11 @@ export type PreloadResult = PreloadSuccess | PreloadFailure
|
|
|
110
110
|
export type PreloadFn = (
|
|
111
111
|
sources: Array<FastImageSource>,
|
|
112
112
|
) => Promise<Array<PreloadResult>>
|
|
113
|
+
export type SourcePlaceholder = {
|
|
114
|
+
thumbhash?: string,
|
|
115
|
+
blurDataUri?: string,
|
|
116
|
+
}
|
|
117
|
+
|
|
113
118
|
export type FastImageSource = {
|
|
114
119
|
uri?: string,
|
|
115
120
|
headers?: Object,
|
|
@@ -118,6 +123,8 @@ export type FastImageSource = {
|
|
|
118
123
|
cacheKey?: string,
|
|
119
124
|
memoryCache?: boolean,
|
|
120
125
|
diskCache?: boolean,
|
|
126
|
+
thumbhash?: string,
|
|
127
|
+
blurDataUri?: string,
|
|
121
128
|
width?: number,
|
|
122
129
|
height?: number,
|
|
123
130
|
scale?: number,
|
|
@@ -132,6 +139,8 @@ export type FastImageProps = $ReadOnly<{|
|
|
|
132
139
|
onProgress?: ?(event: OnProgressEvent) => void,
|
|
133
140
|
|
|
134
141
|
source?: ?(FastImageSource | Array<FastImageSource> | number),
|
|
142
|
+
placeholder?: ?(number | string | SourcePlaceholder),
|
|
143
|
+
// Deprecated: use placeholder, which is the same.
|
|
135
144
|
defaultSource?: ?number,
|
|
136
145
|
|
|
137
146
|
tintColor?: number | string,
|
|
@@ -140,6 +149,8 @@ export type FastImageProps = $ReadOnly<{|
|
|
|
140
149
|
recyclingKey?: ?string,
|
|
141
150
|
loop?: ?(boolean | number),
|
|
142
151
|
imageRendering?: ?('auto' | 'smooth' | 'pixelated'),
|
|
152
|
+
loading?: ?('lazy' | 'eager'),
|
|
153
|
+
sizes?: ?string,
|
|
143
154
|
paused?: ?boolean,
|
|
144
155
|
transition?: ?(boolean | number | Transition),
|
|
145
156
|
downsample?: ?boolean,
|