react-native-fast-image 10.3.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 +28 -4
- package/android/src/main/java/com/fastimage/FastImageViewConverter.java +1 -1
- package/android/src/main/java/com/fastimage/FastImageViewManager.java +14 -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 +9 -1
- package/dist/index.cjs.web.js +29 -7
- package/dist/index.cjs.web.js.flow +9 -1
- package/dist/index.d.ts +30 -3
- package/dist/index.js +10 -4
- package/dist/index.js.flow +9 -1
- package/dist/index.web.js +29 -7
- package/dist/index.web.js.flow +9 -1
- package/dist/placeholders.d.ts +2 -1
- package/dist/specs/FastImageViewNativeComponent.d.ts +1 -0
- package/ios/FastImage/FFFDownsampledImage.h +10 -7
- package/ios/FastImage/FFFDownsampledImage.m +25 -3
- package/ios/FastImage/FFFastImageModule.mm +1 -1
- package/ios/FastImage/FFFastImageView.h +8 -0
- package/ios/FastImage/FFFastImageView.m +239 -16
- package/ios/FastImage/FFFastImageViewComponentView.mm +22 -1
- package/package.json +1 -1
- package/src/index.js.flow +9 -1
- package/src/index.tsx +60 -13
- package/src/index.web.tsx +48 -4
- package/src/placeholders.ts +26 -9
- package/src/specs/FastImageViewNativeComponent.ts +7 -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
|
|
|
@@ -424,6 +426,20 @@ If you control the images, serve them at the size they're shown (resized on your
|
|
|
424
426
|
|
|
425
427
|
---
|
|
426
428
|
|
|
429
|
+
#### `dynamicRangeLimit`
|
|
430
|
+
|
|
431
|
+
**Type:** `'standard' | 'constrained' | 'no-limit'` · iOS, Web only
|
|
432
|
+
|
|
433
|
+
How bright HDR photos (e.g. photos taken on an iPhone) can be, on displays that support it:
|
|
434
|
+
|
|
435
|
+
- `'standard'`: in standard range, like the rest of the screen.
|
|
436
|
+
- `'constrained'`: in HDR, their highlights brighter than the rest of the screen but held below the display's peak, e.g. for images among other content (a feed).
|
|
437
|
+
- `'no-limit'`: in full HDR, e.g. for a full-screen photo viewer.
|
|
438
|
+
|
|
439
|
+
Not set, iOS shows them in standard range, and the web as the browser does by default (usually HDR). On iOS and tvOS 17 and later; Android shows them in standard range. On the web it's CSS's `dynamic-range-limit`.
|
|
440
|
+
|
|
441
|
+
---
|
|
442
|
+
|
|
427
443
|
#### `blurRadius`
|
|
428
444
|
|
|
429
445
|
**Type:** `number` · **Default:** `0`
|
|
@@ -830,6 +846,14 @@ Where the image is in the view: see [`objectPosition`](#objectposition).
|
|
|
830
846
|
|
|
831
847
|
---
|
|
832
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
|
+
|
|
833
857
|
### `PositionLength`
|
|
834
858
|
|
|
835
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);
|
|
@@ -389,6 +396,12 @@ class FastImageViewManager extends SimpleViewManager<FastImageViewWithUrl>
|
|
|
389
396
|
view.setDownsample(downsample);
|
|
390
397
|
}
|
|
391
398
|
|
|
399
|
+
// iOS only: Android shows an HDR photo (a gain map) in HDR only when its
|
|
400
|
+
// whole window is in HDR color mode.
|
|
401
|
+
@Override
|
|
402
|
+
public void setDynamicRangeLimit(FastImageViewWithUrl view, @Nullable String dynamicRangeLimit) {
|
|
403
|
+
}
|
|
404
|
+
|
|
392
405
|
// Which events JS has a handler for (FastImageEvents); with onProgress,
|
|
393
406
|
// the view gets its download's progress (see onDownloadProgress).
|
|
394
407
|
@Override
|
|
@@ -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
|
|
|
@@ -151,6 +158,7 @@ export type FastImageProps = $ReadOnly<{|
|
|
|
151
158
|
imageRendering?: ?('auto' | 'smooth' | 'pixelated'),
|
|
152
159
|
loading?: ?('lazy' | 'eager'),
|
|
153
160
|
sizes?: ?string,
|
|
161
|
+
dynamicRangeLimit?: ?('standard' | 'constrained' | 'no-limit'),
|
|
154
162
|
paused?: ?boolean,
|
|
155
163
|
transition?: ?(boolean | number | Transition),
|
|
156
164
|
downsample?: ?boolean,
|
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
|
|
@@ -283,8 +288,8 @@ const notSupported = {
|
|
|
283
288
|
error: "Not supported on the web"
|
|
284
289
|
};
|
|
285
290
|
const useClientLayoutEffect = globalThis.document === void 0 ? react.useEffect : react.useLayoutEffect;
|
|
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;
|
|
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 }) {
|
|
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]);
|
|
@@ -321,10 +328,23 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
|
|
|
321
328
|
src: defaultUri,
|
|
322
329
|
loading,
|
|
323
330
|
priority,
|
|
331
|
+
dynamicRangeLimit,
|
|
324
332
|
fit: blurUri && (fit === "scale-down" || fit === "none") ? "contain" : fit,
|
|
325
333
|
position,
|
|
326
334
|
filter: blurUri ? [`url(#${blurId})`, tintColor ? `url(#${tintId})` : ""].filter(Boolean).join(" ") : filter,
|
|
327
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: ""
|
|
328
348
|
}) : null, key && src ? /* @__PURE__ */ react.default.createElement(Picture, {
|
|
329
349
|
key: several ? "sizes" : key,
|
|
330
350
|
src,
|
|
@@ -332,6 +352,7 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
|
|
|
332
352
|
sizes,
|
|
333
353
|
loading,
|
|
334
354
|
priority,
|
|
355
|
+
dynamicRangeLimit,
|
|
335
356
|
fit,
|
|
336
357
|
position,
|
|
337
358
|
filter,
|
|
@@ -438,7 +459,7 @@ const fill = {
|
|
|
438
459
|
width: "100%",
|
|
439
460
|
height: "100%"
|
|
440
461
|
};
|
|
441
|
-
function Picture({ src, srcSet, sizes, loading, priority, fit, position, filter, hidden, alt, onLoad, onError }) {
|
|
462
|
+
function Picture({ src, srcSet, sizes, loading, priority, dynamicRangeLimit, fit, position, filter, hidden, alt, onLoad, onError }) {
|
|
442
463
|
const image = (0, react.useRef)(null);
|
|
443
464
|
const mounted = (0, react.useRef)(true);
|
|
444
465
|
const handled = (0, react.useRef)(void 0);
|
|
@@ -485,7 +506,8 @@ function Picture({ src, srcSet, sizes, loading, priority, fit, position, filter,
|
|
|
485
506
|
objectFit: fit,
|
|
486
507
|
objectPosition: position,
|
|
487
508
|
filter,
|
|
488
|
-
opacity: hidden ? 0 : void 0
|
|
509
|
+
opacity: hidden ? 0 : void 0,
|
|
510
|
+
dynamicRangeLimit
|
|
489
511
|
},
|
|
490
512
|
onLoad: (event) => loaded(event.currentTarget),
|
|
491
513
|
onError: (event) => {
|
|
@@ -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
|
|
|
@@ -151,6 +158,7 @@ export type FastImageProps = $ReadOnly<{|
|
|
|
151
158
|
imageRendering?: ?('auto' | 'smooth' | 'pixelated'),
|
|
152
159
|
loading?: ?('lazy' | 'eager'),
|
|
153
160
|
sizes?: ?string,
|
|
161
|
+
dynamicRangeLimit?: ?('standard' | 'constrained' | 'no-limit'),
|
|
154
162
|
paused?: ?boolean,
|
|
155
163
|
transition?: ?(boolean | number | Transition),
|
|
156
164
|
downsample?: ?boolean,
|
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
|
*/
|
|
@@ -533,6 +541,25 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
|
|
|
533
541
|
* @default true
|
|
534
542
|
*/
|
|
535
543
|
downsample?: boolean;
|
|
544
|
+
/**
|
|
545
|
+
* How bright HDR photos (e.g. photos taken on an iPhone) can be, on
|
|
546
|
+
* displays that support it:
|
|
547
|
+
*
|
|
548
|
+
* - `'standard'`: in standard range, like the rest of the screen.
|
|
549
|
+
* - `'constrained'`: in HDR, their highlights brighter than the rest of
|
|
550
|
+
* the screen but held below the display's peak, e.g. for images among
|
|
551
|
+
* other content (a feed).
|
|
552
|
+
* - `'no-limit'`: in full HDR, e.g. for a full-screen photo viewer.
|
|
553
|
+
*
|
|
554
|
+
* Not set, iOS shows them in standard range, and the web as the browser
|
|
555
|
+
* does by default (usually HDR). On iOS and tvOS 17 and later; Android
|
|
556
|
+
* shows them in standard range. On the web it's CSS's
|
|
557
|
+
* `dynamic-range-limit`.
|
|
558
|
+
*
|
|
559
|
+
* @platform ios
|
|
560
|
+
* @platform web
|
|
561
|
+
*/
|
|
562
|
+
dynamicRangeLimit?: 'standard' | 'constrained' | 'no-limit';
|
|
536
563
|
/**
|
|
537
564
|
* Blurs the image by this radius, in points (the same radius looks about
|
|
538
565
|
* the same on iOS and Android). `0` is no blur.
|