react-native-fast-image 10.1.0 → 10.2.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 +84 -10
- 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 +9 -0
- package/dist/index.cjs.web.js +49 -6
- package/dist/index.cjs.web.js.flow +9 -0
- package/dist/index.d.ts +56 -13
- package/dist/index.js +48 -10
- package/dist/index.js.flow +9 -0
- package/dist/index.web.js +49 -6
- package/dist/index.web.js.flow +9 -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 +100 -46
- package/ios/FastImage/FFFastImageViewComponentView.mm +11 -4
- package/package.json +4 -2
- package/src/index.js.flow +9 -0
- package/src/index.tsx +142 -31
- package/src/index.web.tsx +119 -11
- 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
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
#import <SDWebImage/SDWebImageError.h>
|
|
6
6
|
#import <SDWebImage/SDImageTransformer.h>
|
|
7
7
|
#import "FFFDownsampledImage.h"
|
|
8
|
-
#import
|
|
8
|
+
#import "FFFThumbHash.h"
|
|
9
|
+
#import "FFFBoxBlur.h"
|
|
9
10
|
|
|
10
11
|
@interface FFFastImageView ()
|
|
11
12
|
|
|
@@ -28,15 +29,16 @@
|
|
|
28
29
|
// The image being blurred fades in once it's blurred (transition).
|
|
29
30
|
@property(nonatomic, assign) BOOL fadesBlurredImage;
|
|
30
31
|
// The image being blurred is the first loaded one the view shows: until it's
|
|
31
|
-
// blurred, the view shows no loaded image (nothing, or
|
|
32
|
+
// blurred, the view shows no loaded image (nothing, or the placeholder prop).
|
|
32
33
|
@property(nonatomic, assign) BOOL blursOverNothing;
|
|
33
34
|
// Whether the current load was already restarted after the app came back
|
|
34
35
|
// from the background (see downloadImage:).
|
|
35
36
|
@property(nonatomic, assign) BOOL retriedAfterBackground;
|
|
36
37
|
// Waits for the app to be active again, to restart a load.
|
|
37
38
|
@property(nonatomic, strong) id activeObserver;
|
|
38
|
-
// Whether the view shows an image that loaded (not
|
|
39
|
-
// A new source then keeps it until the new image has loaded (see
|
|
39
|
+
// Whether the view shows an image that loaded (not the placeholder prop or
|
|
40
|
+
// nothing). A new source then keeps it until the new image has loaded (see
|
|
41
|
+
// reloadImage).
|
|
40
42
|
@property(nonatomic, assign) BOOL showsLoadedImage;
|
|
41
43
|
// downsample, or several sources: a load waits for the view's size (see
|
|
42
44
|
// didSetProps).
|
|
@@ -62,6 +64,15 @@
|
|
|
62
64
|
// The view's own mask, which clips it to the box while it's moved, when React
|
|
63
65
|
// Native gives it none (the view doesn't clip: overflow visible).
|
|
64
66
|
@property(nonatomic, strong) CALayer* ownMask;
|
|
67
|
+
// The picked source's placeholder (its ThumbHash or blurDataUri) decoded, and
|
|
68
|
+
// the string, or nil: it shows instead of the placeholder prop (see
|
|
69
|
+
// shownPlaceholder).
|
|
70
|
+
@property(nonatomic, strong) UIImage* hashImage;
|
|
71
|
+
@property(nonatomic, copy) NSString* hashImageKey;
|
|
72
|
+
// Whether the view shows its placeholder (the placeholder prop or the
|
|
73
|
+
// source's), and whether that's the source's (hashImage).
|
|
74
|
+
@property(nonatomic, assign) BOOL showsPlaceholder;
|
|
75
|
+
@property(nonatomic, assign) BOOL showsHash;
|
|
65
76
|
|
|
66
77
|
@end
|
|
67
78
|
|
|
@@ -266,12 +277,14 @@ static UIImage* FFFBlurredImage(UIImage* image, CGFloat scale, CGFloat radius, B
|
|
|
266
277
|
contentMode = UIViewContentModeScaleToFill;
|
|
267
278
|
break;
|
|
268
279
|
case FFFObjectFitNone:
|
|
269
|
-
|
|
280
|
+
// A source's placeholder stands for an image whose size isn't
|
|
281
|
+
// known yet: as with contain, here and for scale-down.
|
|
282
|
+
contentMode = self.showsHash ? UIViewContentModeScaleAspectFit : UIViewContentModeCenter;
|
|
270
283
|
break;
|
|
271
284
|
case FFFObjectFitScaleDown: {
|
|
272
285
|
CGSize imageSize = super.image.size;
|
|
273
286
|
CGSize viewSize = self.bounds.size;
|
|
274
|
-
contentMode = imageSize.width > viewSize.width || imageSize.height > viewSize.height
|
|
287
|
+
contentMode = self.showsHash || imageSize.width > viewSize.width || imageSize.height > viewSize.height
|
|
275
288
|
? UIViewContentModeScaleAspectFit
|
|
276
289
|
: UIViewContentModeCenter;
|
|
277
290
|
break;
|
|
@@ -427,6 +440,13 @@ static UIImage* FFFBlurredImage(UIImage* image, CGFloat scale, CGFloat radius, B
|
|
|
427
440
|
|
|
428
441
|
- (void) setImage: (UIImage*)image {
|
|
429
442
|
self.imageCount++;
|
|
443
|
+
self.showsHash = image != nil && image == self.hashImage;
|
|
444
|
+
self.showsPlaceholder = self.showsHash || (image != nil && image == _placeholder);
|
|
445
|
+
if ([_imageRendering isEqualToString: @"pixelated"]) {
|
|
446
|
+
// A source's placeholder is a blurred preview, scaled up smoothly
|
|
447
|
+
// (pixelated is for the image).
|
|
448
|
+
self.layer.magnificationFilter = self.showsHash ? kCAFilterLinear : kCAFilterNearest;
|
|
449
|
+
}
|
|
430
450
|
if ([self blurs: image]) {
|
|
431
451
|
self.originalImage = image;
|
|
432
452
|
self.blurredImage = nil;
|
|
@@ -513,10 +533,10 @@ static UIImage* FFFBlurredImage(UIImage* image, CGFloat scale, CGFloat radius, B
|
|
|
513
533
|
}
|
|
514
534
|
}
|
|
515
535
|
|
|
516
|
-
// Whether the image is blurred: a loaded image, not
|
|
517
|
-
// React Native's Image, and on Android).
|
|
536
|
+
// Whether the image is blurred: a loaded image, not the placeholder prop (as
|
|
537
|
+
// with React Native's Image, and on Android).
|
|
518
538
|
- (BOOL) blurs: (UIImage*)image {
|
|
519
|
-
return _blurRadius > 0 && image && image !=
|
|
539
|
+
return _blurRadius > 0 && image && image != _placeholder && image != self.hashImage;
|
|
520
540
|
}
|
|
521
541
|
|
|
522
542
|
// Blurs the image off the main thread, and shows it (tinted as any image) unless
|
|
@@ -649,27 +669,7 @@ static UIImage* FFFBlurredImage(UIImage* image, CGFloat scale, CGFloat radius, B
|
|
|
649
669
|
[image drawInRect: CGRectMake(0, 0, width, height)];
|
|
650
670
|
UIGraphicsPopContext();
|
|
651
671
|
|
|
652
|
-
|
|
653
|
-
vImage_Buffer buffer = {
|
|
654
|
-
.data = CGBitmapContextGetData(context),
|
|
655
|
-
.height = height,
|
|
656
|
-
.width = width,
|
|
657
|
-
.rowBytes = CGBitmapContextGetBytesPerRow(context),
|
|
658
|
-
};
|
|
659
|
-
vImage_Buffer other = buffer;
|
|
660
|
-
other.data = malloc(buffer.rowBytes * height);
|
|
661
|
-
vImage_Error tempSize = vImageBoxConvolve_ARGB8888(&buffer, &other, NULL, 0, 0, boxSize, boxSize, NULL,
|
|
662
|
-
kvImageGetTempBufferSize | kvImageEdgeExtend);
|
|
663
|
-
void* temp = tempSize > 0 ? malloc(tempSize) : NULL;
|
|
664
|
-
if (other.data && temp) {
|
|
665
|
-
vImageBoxConvolve_ARGB8888(&buffer, &other, temp, 0, 0, boxSize, boxSize, NULL, kvImageEdgeExtend);
|
|
666
|
-
vImageBoxConvolve_ARGB8888(&other, &buffer, temp, 0, 0, boxSize, boxSize, NULL, kvImageEdgeExtend);
|
|
667
|
-
vImageBoxConvolve_ARGB8888(&buffer, &other, temp, 0, 0, boxSize, boxSize, NULL, kvImageEdgeExtend);
|
|
668
|
-
memcpy(buffer.data, other.data, buffer.rowBytes * height);
|
|
669
|
-
}
|
|
670
|
-
free(other.data);
|
|
671
|
-
free(temp);
|
|
672
|
-
}
|
|
672
|
+
FFFBoxBlur(context, boxSize);
|
|
673
673
|
|
|
674
674
|
CGImageRef cgImage = CGBitmapContextCreateImage(context);
|
|
675
675
|
CGContextRelease(context);
|
|
@@ -798,13 +798,51 @@ NSString *FFFErrorMessage(NSError *error)
|
|
|
798
798
|
}
|
|
799
799
|
}
|
|
800
800
|
|
|
801
|
-
- (void)
|
|
802
|
-
if (
|
|
803
|
-
|
|
801
|
+
- (void) setPlaceholder: (UIImage*)placeholder {
|
|
802
|
+
if (_placeholder != placeholder) {
|
|
803
|
+
_placeholder = placeholder;
|
|
804
804
|
_needsReload = YES;
|
|
805
805
|
}
|
|
806
806
|
}
|
|
807
807
|
|
|
808
|
+
- (void) setSourcePlaceholders: (NSArray<NSString*>*)sourcePlaceholders {
|
|
809
|
+
_sourcePlaceholders = [sourcePlaceholders copy];
|
|
810
|
+
[self updateHashImage];
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// What shows while the image loads and if it fails: the picked source's
|
|
814
|
+
// placeholder (its ThumbHash or blurDataUri), or the placeholder prop.
|
|
815
|
+
- (UIImage*) shownPlaceholder {
|
|
816
|
+
return self.hashImage ?: _placeholder;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// The picked source's ThumbHash or blurDataUri, or nil.
|
|
820
|
+
- (NSString*) pickedSourcePlaceholder {
|
|
821
|
+
NSUInteger index = 0;
|
|
822
|
+
if ([self picksSource]) {
|
|
823
|
+
index = _source ? [_sources indexOfObjectIdenticalTo: _source] : NSNotFound;
|
|
824
|
+
}
|
|
825
|
+
NSString* hash = index < _sourcePlaceholders.count ? _sourcePlaceholders[index] : nil;
|
|
826
|
+
return hash.length > 0 ? hash : nil;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// Decodes the picked source's placeholder. A view showing the previous
|
|
830
|
+
// placeholder shows the new one at once, without loading the image again.
|
|
831
|
+
- (void) updateHashImage {
|
|
832
|
+
NSString* hash = [self pickedSourcePlaceholder];
|
|
833
|
+
if (hash == self.hashImageKey || [hash isEqualToString: self.hashImageKey]) {
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
UIImage* previous = [self shownPlaceholder];
|
|
837
|
+
BOOL showing = self.showsPlaceholder;
|
|
838
|
+
self.hashImageKey = hash;
|
|
839
|
+
self.hashImage = [FFFThumbHash imageForPlaceholder: hash];
|
|
840
|
+
UIImage* placeholder = [self shownPlaceholder];
|
|
841
|
+
if (showing && placeholder != previous) {
|
|
842
|
+
[self setImage: placeholder];
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
808
846
|
- (void) didSetProps {
|
|
809
847
|
if (_needsReload) {
|
|
810
848
|
// With downsample on, the image is decoded for the view's size, and
|
|
@@ -906,6 +944,7 @@ NSString *FFFErrorMessage(NSError *error)
|
|
|
906
944
|
if ([self picksSource]) {
|
|
907
945
|
_source = [self sourceForSize];
|
|
908
946
|
}
|
|
947
|
+
[self updateHashImage];
|
|
909
948
|
self.decodedBox = CGSizeZero;
|
|
910
949
|
// The previous load, if it's still running, is for a source the view no
|
|
911
950
|
// longer shows: cancel it (a new download would, but a data uri or no
|
|
@@ -924,8 +963,9 @@ NSString *FFFErrorMessage(NSError *error)
|
|
|
924
963
|
// Use SDWebImage API to support external format like WebP images
|
|
925
964
|
UIImage* image = [UIImage sd_imageWithData: [NSData dataWithContentsOfURL: _source.url]];
|
|
926
965
|
if (!image) {
|
|
927
|
-
// Not decodable: fail like a remote image, showing
|
|
928
|
-
|
|
966
|
+
// Not decodable: fail like a remote image, showing the
|
|
967
|
+
// placeholder.
|
|
968
|
+
[self setImage: [self shownPlaceholder]];
|
|
929
969
|
self.showsLoadedImage = NO;
|
|
930
970
|
[self sendOnError: @"Failed to decode the image"];
|
|
931
971
|
if (self.onFastImageLoadEnd) {
|
|
@@ -954,9 +994,10 @@ NSString *FFFErrorMessage(NSError *error)
|
|
|
954
994
|
SDWebImageOptions options = [self loadOptions];
|
|
955
995
|
|
|
956
996
|
// Keep showing the loaded image until the new one has loaded, instead
|
|
957
|
-
// of clearing it to
|
|
958
|
-
// flashed (#747).
|
|
959
|
-
// Native's Image does; a `key` that changes starts from
|
|
997
|
+
// of clearing it to the placeholder prop (or nothing) while it loads,
|
|
998
|
+
// which flashed (#747). The placeholder prop shows if the new image
|
|
999
|
+
// fails. As React Native's Image does; a `key` that changes starts from
|
|
1000
|
+
// blank instead.
|
|
960
1001
|
if (self.showsLoadedImage) {
|
|
961
1002
|
options |= SDWebImageDelayPlaceholder;
|
|
962
1003
|
}
|
|
@@ -970,8 +1011,8 @@ NSString *FFFErrorMessage(NSError *error)
|
|
|
970
1011
|
[self stopWaitingForActive];
|
|
971
1012
|
|
|
972
1013
|
[self downloadImage: _source options: options context: [self loadContext] events: YES];
|
|
973
|
-
} else if (
|
|
974
|
-
[self setImage:
|
|
1014
|
+
} else if ([self shownPlaceholder]) {
|
|
1015
|
+
[self setImage: [self shownPlaceholder]];
|
|
975
1016
|
self.showsLoadedImage = NO;
|
|
976
1017
|
}
|
|
977
1018
|
}
|
|
@@ -1003,8 +1044,8 @@ NSString *FFFErrorMessage(NSError *error)
|
|
|
1003
1044
|
}
|
|
1004
1045
|
|
|
1005
1046
|
// Whether the view loads as it's laid out, so its image shows in this frame:
|
|
1006
|
-
// one in the memory cache, where SDWebImage finds it as the load starts, or
|
|
1007
|
-
//
|
|
1047
|
+
// one in the memory cache, where SDWebImage finds it as the load starts, or one
|
|
1048
|
+
// that isn't downloaded (no source, which shows the placeholder prop, or a data
|
|
1008
1049
|
// uri). Also when that can't be told (an app's own image cache), as before.
|
|
1009
1050
|
- (BOOL) loadsDuringLayout {
|
|
1010
1051
|
FFFastImageSource* source = [self picksSource] ? [self sourceForSize] : _source;
|
|
@@ -1143,13 +1184,13 @@ NSString *FFFErrorMessage(NSError *error)
|
|
|
1143
1184
|
// class to SDAnimatedImage (loadContext sets it). Only while this is the
|
|
1144
1185
|
// current load: SDWebImage sets the placeholder when a load it cancelled
|
|
1145
1186
|
// completes, which could replace a newer image.
|
|
1146
|
-
UIImage* placeholder =
|
|
1187
|
+
UIImage* placeholder = [self shownPlaceholder];
|
|
1147
1188
|
SDSetImageBlock setImage = ^(UIImage* _Nullable image, NSData* _Nullable data, SDImageCacheType cacheType, NSURL* _Nullable imageURL) {
|
|
1148
1189
|
if (weakSelf.loadCount != load) {
|
|
1149
1190
|
return;
|
|
1150
1191
|
}
|
|
1151
|
-
// The loaded image, not
|
|
1152
|
-
// fails).
|
|
1192
|
+
// The loaded image, not the placeholder prop (shown while it loads, or
|
|
1193
|
+
// if it fails).
|
|
1153
1194
|
BOOL loaded = image && image != placeholder;
|
|
1154
1195
|
[weakSelf showLoadedImage: image fade: loaded && [weakSelf fadesImageFromCache: cacheType]];
|
|
1155
1196
|
};
|
|
@@ -1174,6 +1215,13 @@ NSString *FFFErrorMessage(NSError *error)
|
|
|
1174
1215
|
if (weakSelf.loadCount != load) {
|
|
1175
1216
|
return;
|
|
1176
1217
|
}
|
|
1218
|
+
// cacheOnly: SDWebImage completes the load of an image that
|
|
1219
|
+
// isn't cached with neither an image nor an error.
|
|
1220
|
+
if (!error && !image) {
|
|
1221
|
+
error = [NSError errorWithDomain: @"FastImage"
|
|
1222
|
+
code: 0
|
|
1223
|
+
userInfo: @{NSLocalizedDescriptionKey: @"Not in the cache"}];
|
|
1224
|
+
}
|
|
1177
1225
|
// The download was running when the app went to the
|
|
1178
1226
|
// background, and failed: iOS suspends it there, and its
|
|
1179
1227
|
// timeout keeps counting, so it times out as the app comes
|
|
@@ -1187,7 +1235,13 @@ NSString *FFFErrorMessage(NSError *error)
|
|
|
1187
1235
|
return;
|
|
1188
1236
|
}
|
|
1189
1237
|
if (error) {
|
|
1190
|
-
// SDWebImage shows the placeholder
|
|
1238
|
+
// SDWebImage shows the placeholder the load started with
|
|
1239
|
+
// (the placeholder prop, a source's, or nothing): the
|
|
1240
|
+
// current one if the source's changed since.
|
|
1241
|
+
UIImage* current = [weakSelf shownPlaceholder];
|
|
1242
|
+
if (current != placeholder) {
|
|
1243
|
+
[weakSelf setImage: current];
|
|
1244
|
+
}
|
|
1191
1245
|
weakSelf.showsLoadedImage = NO;
|
|
1192
1246
|
[weakSelf sendOnError: [source errorMessage: error]];
|
|
1193
1247
|
if (weakSelf.onFastImageLoadEnd) {
|
|
@@ -95,7 +95,7 @@ static folly::dynamic FFFDynamicFromObject(id value)
|
|
|
95
95
|
return nullptr;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
// A prop of any type (a source, sources,
|
|
98
|
+
// A prop of any type (a source, sources, placeholder) as JSON: nil when
|
|
99
99
|
// it's null (not set).
|
|
100
100
|
static id _Nullable FFFJSON(const folly::dynamic &value)
|
|
101
101
|
{
|
|
@@ -232,9 +232,16 @@ typedef NS_OPTIONS(int32_t, FFFEvent) {
|
|
|
232
232
|
id sources = FFFJSON(newViewProps.sources);
|
|
233
233
|
view.sources = sources ? [RCTConvert FFFastImageSourceArray:sources] : nil;
|
|
234
234
|
}
|
|
235
|
-
if (all || oldViewProps.
|
|
236
|
-
id
|
|
237
|
-
view.
|
|
235
|
+
if (all || oldViewProps.placeholder != newViewProps.placeholder) {
|
|
236
|
+
id placeholder = FFFJSON(newViewProps.placeholder);
|
|
237
|
+
view.placeholder = placeholder ? [RCTConvert UIImage:placeholder] : nil;
|
|
238
|
+
}
|
|
239
|
+
if (all || oldViewProps.sourcePlaceholders != newViewProps.sourcePlaceholders) {
|
|
240
|
+
NSMutableArray<NSString *> *placeholders = [NSMutableArray arrayWithCapacity:newViewProps.sourcePlaceholders.size()];
|
|
241
|
+
for (const auto &placeholder : newViewProps.sourcePlaceholders) {
|
|
242
|
+
[placeholders addObject:[NSString stringWithUTF8String:placeholder.c_str()]];
|
|
243
|
+
}
|
|
244
|
+
view.sourcePlaceholders = placeholders.count > 0 ? placeholders : nil;
|
|
238
245
|
}
|
|
239
246
|
if (all || oldViewProps.objectFit != newViewProps.objectFit) {
|
|
240
247
|
view.objectFit = [RCTConvert FFFObjectFit:FFFString(newViewProps.objectFit)];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-fast-image",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.2.0",
|
|
4
4
|
"description": "FastImage, performant React Native image component.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cache",
|
|
@@ -32,9 +32,11 @@
|
|
|
32
32
|
"app.plugin.js",
|
|
33
33
|
"src",
|
|
34
34
|
"!src/**/*.test.tsx",
|
|
35
|
+
"!src/**/*.test.ts",
|
|
35
36
|
"!src/__snapshots__",
|
|
36
37
|
"dist",
|
|
37
|
-
"RNFastImage.podspec"
|
|
38
|
+
"RNFastImage.podspec",
|
|
39
|
+
"thumbhash"
|
|
38
40
|
],
|
|
39
41
|
"scripts": {
|
|
40
42
|
"build": "bun run build:package && bun run build:docs",
|
package/src/index.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,
|
package/src/index.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { forwardRef, memo } from 'react'
|
|
1
|
+
import React, { forwardRef, memo, useMemo } from 'react'
|
|
2
2
|
import {
|
|
3
3
|
ColorValue,
|
|
4
4
|
View,
|
|
@@ -16,6 +16,7 @@ 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 { dataUri, hasSourcePlaceholder, isImageName } from './placeholders'
|
|
19
20
|
import { warnIfChildren, warnOnce } from './warnings'
|
|
20
21
|
|
|
21
22
|
/** How the image fits the view: see [`objectFit`](#objectfit). */
|
|
@@ -129,6 +130,32 @@ export type Source = {
|
|
|
129
130
|
* @default true
|
|
130
131
|
*/
|
|
131
132
|
diskCache?: boolean
|
|
133
|
+
/**
|
|
134
|
+
* A [ThumbHash](https://evanw.github.io/thumbhash/) of the image, shown
|
|
135
|
+
* blurred while the image loads, and if it fails to load. It's a short
|
|
136
|
+
* string you make when the image is uploaded (e.g. with the `thumbhash`
|
|
137
|
+
* package) and send with the image's url.
|
|
138
|
+
*
|
|
139
|
+
* On the web, it needs `import 'react-native-fast-image/thumbhash'` (see
|
|
140
|
+
* [ThumbHashes on the web](#thumbhashes-on-the-web)).
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* ```jsx
|
|
144
|
+
* <FastImage source={{ uri: photo.url, thumbhash: photo.thumbhash }} />
|
|
145
|
+
* ```
|
|
146
|
+
*/
|
|
147
|
+
thumbhash?: string
|
|
148
|
+
/**
|
|
149
|
+
* A tiny version of the image (e.g. 8 to 20 px) as a data uri, shown
|
|
150
|
+
* blurred while the image loads, and if it fails to load. On the web it
|
|
151
|
+
* shows without any JavaScript, e.g. in a server-rendered page.
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
* ```jsx
|
|
155
|
+
* <FastImage source={{ uri: photo.url, blurDataUri: photo.lqip }} />
|
|
156
|
+
* ```
|
|
157
|
+
*/
|
|
158
|
+
blurDataUri?: string
|
|
132
159
|
/**
|
|
133
160
|
* With several sources (`source` as an array), the image's width at this
|
|
134
161
|
* uri, in pixels (times `scale`, if given).
|
|
@@ -143,6 +170,9 @@ export type Source = {
|
|
|
143
170
|
scale?: number
|
|
144
171
|
}
|
|
145
172
|
|
|
173
|
+
/** A `thumbhash` or `blurDataUri` as `placeholder`. */
|
|
174
|
+
export type SourcePlaceholder = Pick<Source, 'thumbhash' | 'blurDataUri'>
|
|
175
|
+
|
|
146
176
|
/** How the image fades in: see [`transition`](#transition). */
|
|
147
177
|
export type Transition = {
|
|
148
178
|
/**
|
|
@@ -154,7 +184,7 @@ export type Transition = {
|
|
|
154
184
|
/**
|
|
155
185
|
* Also fades between images: a new `source` replacing an image that's
|
|
156
186
|
* showing cross-dissolves from it. Otherwise only an image that appears
|
|
157
|
-
* over nothing (or over
|
|
187
|
+
* over nothing (or over a placeholder) fades in, and a new source
|
|
158
188
|
* replaces the image showing at once, once it has loaded.
|
|
159
189
|
* @default false
|
|
160
190
|
*/
|
|
@@ -323,14 +353,29 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
|
|
|
323
353
|
*/
|
|
324
354
|
source?: Source | Source[] | ImageRequireSource
|
|
325
355
|
/**
|
|
326
|
-
*
|
|
327
|
-
* and if an image fails to load. When `source` changes, the previous image
|
|
328
|
-
* shows while the new one loads instead (see `source` and
|
|
329
|
-
* `recyclingKey`).
|
|
356
|
+
* A placeholder shown while the image loads, and if it fails to load:
|
|
330
357
|
*
|
|
331
|
-
*
|
|
332
|
-
*
|
|
333
|
-
*
|
|
358
|
+
* - An image loaded with `require()`: `placeholder={require('./gray.png')}`.
|
|
359
|
+
* - A data uri: `placeholder="data:image/png;base64,…"`.
|
|
360
|
+
* - The name of an image bundled with the app (an iOS asset catalog image
|
|
361
|
+
* or an Android drawable): `placeholder="gray"`.
|
|
362
|
+
* - A [ThumbHash](#sourcethumbhash), shown blurred:
|
|
363
|
+
* `placeholder={{ thumbhash }}`.
|
|
364
|
+
* - A tiny image as a data uri, shown blurred:
|
|
365
|
+
* `placeholder={{ blurDataUri }}`.
|
|
366
|
+
*
|
|
367
|
+
* A source's own `thumbhash` or `blurDataUri` is shown instead, if it has
|
|
368
|
+
* one.
|
|
369
|
+
*
|
|
370
|
+
* Notes:
|
|
371
|
+
*
|
|
372
|
+
* - A url isn't loaded: it's ignored, with a warning in development.
|
|
373
|
+
* - On Android, a `require()`d image doesn't show in debug builds, where
|
|
374
|
+
* the dev server serves it. Release builds show it.
|
|
375
|
+
*/
|
|
376
|
+
placeholder?: ImageRequireSource | string | SourcePlaceholder
|
|
377
|
+
/**
|
|
378
|
+
* @deprecated Use `placeholder`, which does the same.
|
|
334
379
|
*/
|
|
335
380
|
defaultSource?: ImageRequireSource
|
|
336
381
|
/**
|
|
@@ -373,8 +418,8 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
|
|
|
373
418
|
* `{ right: 10, bottom: '20%' }`. A missing axis is centered.
|
|
374
419
|
*
|
|
375
420
|
* Points are dp on Android. An invalid value is ignored (the image is
|
|
376
|
-
* centered), with a warning in development.
|
|
377
|
-
*
|
|
421
|
+
* centered), with a warning in development. Placeholders are placed the
|
|
422
|
+
* same way.
|
|
378
423
|
*
|
|
379
424
|
* @default '50% 50%'
|
|
380
425
|
* @example
|
|
@@ -387,7 +432,7 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
|
|
|
387
432
|
* For views that get reused for other content, such as rows in FlashList
|
|
388
433
|
* or recyclerlistview. Set it to something that identifies the content,
|
|
389
434
|
* e.g. the item's id. When it changes, the image is cleared right away (to
|
|
390
|
-
* `
|
|
435
|
+
* `placeholder`, or blank) instead of staying until the new one has
|
|
391
436
|
* loaded. Unlike changing `key`, this keeps the view, which is what list
|
|
392
437
|
* recycling is for.
|
|
393
438
|
*
|
|
@@ -499,7 +544,7 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
|
|
|
499
544
|
* blurring a photo behind a sheet). Each change blurs the image again on
|
|
500
545
|
* the CPU, so don't animate it.
|
|
501
546
|
*
|
|
502
|
-
* - Only the loaded image is blurred, not
|
|
547
|
+
* - Only the loaded image is blurred, not a placeholder.
|
|
503
548
|
* - An animated image (GIF, animated WebP, APNG or AVIF) shows its first frame,
|
|
504
549
|
* blurred, and doesn't animate.
|
|
505
550
|
* - With `tintColor`, the blurred image is tinted.
|
|
@@ -588,7 +633,7 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
|
|
|
588
633
|
|
|
589
634
|
/**
|
|
590
635
|
* If supplied, changes the color of all the non-transparent pixels to the
|
|
591
|
-
* given color.
|
|
636
|
+
* given color, including the placeholder.
|
|
592
637
|
*/
|
|
593
638
|
tintColor?: ColorValue
|
|
594
639
|
|
|
@@ -601,13 +646,46 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
|
|
|
601
646
|
children?: "FastImage doesn't take children: use FastImageBackground for content over an image"
|
|
602
647
|
}
|
|
603
648
|
|
|
604
|
-
|
|
605
|
-
|
|
649
|
+
// A source without its thumbhash and blurDataUri.
|
|
650
|
+
function withoutPlaceholders<T>(source: T): T {
|
|
651
|
+
if (!hasSourcePlaceholder(source)) return source
|
|
652
|
+
const {
|
|
653
|
+
thumbhash: _thumbhash,
|
|
654
|
+
blurDataUri: _blurDataUri,
|
|
655
|
+
...rest
|
|
656
|
+
} = source as Source
|
|
657
|
+
return rest as T
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// What the native view shows while a source loads: its ThumbHash (decoded
|
|
661
|
+
// natively), or its blurDataUri, or '' for neither.
|
|
662
|
+
function sourcePlaceholder(source: unknown): string {
|
|
663
|
+
if (!source || typeof source !== 'object') return ''
|
|
664
|
+
const { thumbhash, blurDataUri } = source as Source
|
|
665
|
+
return thumbhash || dataUri(blurDataUri) || ''
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
const resolvePlaceholder = (
|
|
669
|
+
placeholder?: ImageRequireSource | string,
|
|
606
670
|
): string | object | null => {
|
|
607
|
-
if (
|
|
671
|
+
if (typeof placeholder === 'string') {
|
|
672
|
+
// An image bundled with the app, by name, as React Native's Image
|
|
673
|
+
// takes it ({ uri: name }).
|
|
674
|
+
if (isImageName(placeholder)) {
|
|
675
|
+
return Platform.OS === 'android'
|
|
676
|
+
? placeholder
|
|
677
|
+
: { uri: placeholder }
|
|
678
|
+
}
|
|
679
|
+
const uri = dataUri(placeholder)
|
|
680
|
+
if (!uri) return null
|
|
681
|
+
// Android decodes it; iOS takes it as an image source, as it does a
|
|
682
|
+
// require()d image's.
|
|
683
|
+
return Platform.OS === 'android' ? uri : { uri }
|
|
684
|
+
}
|
|
685
|
+
if (!placeholder) {
|
|
608
686
|
return null
|
|
609
687
|
}
|
|
610
|
-
const resolved = Image.resolveAssetSource(
|
|
688
|
+
const resolved = Image.resolveAssetSource(placeholder)
|
|
611
689
|
if (!resolved) {
|
|
612
690
|
return null
|
|
613
691
|
}
|
|
@@ -719,6 +797,7 @@ declare const __DEV__: boolean
|
|
|
719
797
|
|
|
720
798
|
function FastImageBase({
|
|
721
799
|
source,
|
|
800
|
+
placeholder,
|
|
722
801
|
defaultSource,
|
|
723
802
|
tintColor,
|
|
724
803
|
onLoadStart,
|
|
@@ -765,18 +844,49 @@ function FastImageBase({
|
|
|
765
844
|
const fit = resolveObjectFit(objectFit, style)
|
|
766
845
|
const position = objectPositionOrCentered(objectPosition)
|
|
767
846
|
|
|
768
|
-
//
|
|
769
|
-
//
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
const resolvedSource =
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
847
|
+
// Memoized on source, so an unchanged source reaches the native view as
|
|
848
|
+
// the same objects, and React's prop diff stops at them.
|
|
849
|
+
// `placeholder` as a thumbhash or blurDataUri, for the sources without
|
|
850
|
+
// their own: a string, so an object made on every render doesn't make
|
|
851
|
+
// new sourcePlaceholders.
|
|
852
|
+
const shared = hasSourcePlaceholder(placeholder)
|
|
853
|
+
? sourcePlaceholder(placeholder)
|
|
854
|
+
: ''
|
|
855
|
+
const { resolvedSource, sources, sourcePlaceholders } = useMemo(() => {
|
|
856
|
+
// Several sources are picked from natively, for the view's size; one
|
|
857
|
+
// in an array is a plain source.
|
|
858
|
+
const sources = Array.isArray(source)
|
|
859
|
+
? source.length === 1
|
|
860
|
+
? undefined
|
|
861
|
+
: source
|
|
862
|
+
: undefined
|
|
863
|
+
const single = Array.isArray(source) ? source[0] : source
|
|
864
|
+
// Sources' thumbhashes and blurDataUris go to the native view apart
|
|
865
|
+
// from the sources, so a new one for the same image doesn't load it
|
|
866
|
+
// again, by the source's index ('' for none). A source's own wins
|
|
867
|
+
// over `placeholder`'s.
|
|
868
|
+
const placeholders = (sources ?? [single]).map(
|
|
869
|
+
(entry) => sourcePlaceholder(entry) || shared,
|
|
870
|
+
)
|
|
871
|
+
return {
|
|
872
|
+
resolvedSource: sources
|
|
873
|
+
? undefined
|
|
874
|
+
: Image.resolveAssetSource(withoutPlaceholders(single) as any),
|
|
875
|
+
sources: sources?.some(hasSourcePlaceholder)
|
|
876
|
+
? sources.map(withoutPlaceholders)
|
|
877
|
+
: sources,
|
|
878
|
+
sourcePlaceholders: placeholders.some(Boolean)
|
|
879
|
+
? placeholders
|
|
880
|
+
: undefined,
|
|
881
|
+
}
|
|
882
|
+
}, [source, shared])
|
|
883
|
+
// A thumbhash or blurDataUri in `placeholder` goes in sourcePlaceholders
|
|
884
|
+
// instead: it isn't an image to show as it is, but defaultSource still is.
|
|
885
|
+
const resolvedPlaceholder = resolvePlaceholder(
|
|
886
|
+
hasSourcePlaceholder(placeholder)
|
|
887
|
+
? defaultSource
|
|
888
|
+
: (placeholder ?? defaultSource),
|
|
889
|
+
)
|
|
780
890
|
|
|
781
891
|
return (
|
|
782
892
|
<FastImageView
|
|
@@ -794,7 +904,8 @@ function FastImageBase({
|
|
|
794
904
|
)}
|
|
795
905
|
source={resolvedSource}
|
|
796
906
|
sources={sources}
|
|
797
|
-
|
|
907
|
+
sourcePlaceholders={sourcePlaceholders}
|
|
908
|
+
placeholder={resolvedPlaceholder}
|
|
798
909
|
onFastImageLoadStart={onLoadStart}
|
|
799
910
|
onFastImageProgress={onProgress}
|
|
800
911
|
// The native views only send the events with a handler (and only
|