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
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
#import <React/RCTUtils.h>
|
|
5
5
|
#import <SDWebImage/SDWebImageError.h>
|
|
6
6
|
#import <SDWebImage/SDImageTransformer.h>
|
|
7
|
+
#import <SDWebImage/SDImageCodersManager.h>
|
|
7
8
|
#import "FFFDownsampledImage.h"
|
|
8
9
|
#import "FFFThumbHash.h"
|
|
9
10
|
#import "FFFBoxBlur.h"
|
|
@@ -69,10 +70,23 @@
|
|
|
69
70
|
// shownPlaceholder).
|
|
70
71
|
@property(nonatomic, strong) UIImage* hashImage;
|
|
71
72
|
@property(nonatomic, copy) NSString* hashImageKey;
|
|
72
|
-
//
|
|
73
|
-
//
|
|
73
|
+
// placeholderSource's image once it has loaded, or nil: it shows instead of
|
|
74
|
+
// the other placeholders (see shownPlaceholder). Its load, and which it is.
|
|
75
|
+
@property(nonatomic, strong) UIImage* urlPlaceholderImage;
|
|
76
|
+
@property(nonatomic, strong) id<SDWebImageOperation> urlPlaceholderOperation;
|
|
77
|
+
@property(nonatomic, assign) NSUInteger urlPlaceholderLoad;
|
|
78
|
+
// placeholderSource changed: its load starts with the image's, or after the
|
|
79
|
+
// props if the image is still loading (see startUrlPlaceholderLoad).
|
|
80
|
+
@property(nonatomic, assign) BOOL needsUrlPlaceholderLoad;
|
|
81
|
+
// Whether the view shows a placeholder (the placeholder prop, the source's,
|
|
82
|
+
// or placeholderSource's), and whether it stands for the image (the source's
|
|
83
|
+
// or placeholderSource's): drawn as with contain for scale-down and none, as
|
|
84
|
+
// the image's size isn't known yet, scaled smoothly and not blurred.
|
|
74
85
|
@property(nonatomic, assign) BOOL showsPlaceholder;
|
|
75
86
|
@property(nonatomic, assign) BOOL showsHash;
|
|
87
|
+
// dynamicRangeLimit switched between HDR and SDR: the image is decoded again,
|
|
88
|
+
// quietly (see reloadForDynamicRange).
|
|
89
|
+
@property(nonatomic, assign) BOOL needsDynamicRangeReload;
|
|
76
90
|
|
|
77
91
|
@end
|
|
78
92
|
|
|
@@ -440,7 +454,7 @@ static UIImage* FFFBlurredImage(UIImage* image, CGFloat scale, CGFloat radius, B
|
|
|
440
454
|
|
|
441
455
|
- (void) setImage: (UIImage*)image {
|
|
442
456
|
self.imageCount++;
|
|
443
|
-
self.showsHash = image != nil && image == self.hashImage;
|
|
457
|
+
self.showsHash = image != nil && (image == self.hashImage || image == self.urlPlaceholderImage);
|
|
444
458
|
self.showsPlaceholder = self.showsHash || (image != nil && image == _placeholder);
|
|
445
459
|
if ([_imageRendering isEqualToString: @"pixelated"]) {
|
|
446
460
|
// A source's placeholder is a blurred preview, scaled up smoothly
|
|
@@ -536,7 +550,7 @@ static UIImage* FFFBlurredImage(UIImage* image, CGFloat scale, CGFloat radius, B
|
|
|
536
550
|
// Whether the image is blurred: a loaded image, not the placeholder prop (as
|
|
537
551
|
// with React Native's Image, and on Android).
|
|
538
552
|
- (BOOL) blurs: (UIImage*)image {
|
|
539
|
-
return _blurRadius > 0 && image && image != _placeholder && image != self.hashImage;
|
|
553
|
+
return _blurRadius > 0 && image && image != _placeholder && image != self.hashImage && image != self.urlPlaceholderImage;
|
|
540
554
|
}
|
|
541
555
|
|
|
542
556
|
// Blurs the image off the main thread, and shows it (tinted as any image) unless
|
|
@@ -643,6 +657,14 @@ static UIImage* FFFBlurredImage(UIImage* image, CGFloat scale, CGFloat radius, B
|
|
|
643
657
|
if (image.size.width <= 0 || image.size.height <= 0) {
|
|
644
658
|
return image;
|
|
645
659
|
}
|
|
660
|
+
// Blurred in standard range (the bitmap below has 8 bits per channel): an
|
|
661
|
+
// HDR image (dynamicRangeLimit) tone mapped to it, rather than its
|
|
662
|
+
// highlights clipped.
|
|
663
|
+
if (@available(iOS 17.0, tvOS 17.0, *)) {
|
|
664
|
+
if (image.isHighDynamicRange) {
|
|
665
|
+
image = [image imageRestrictedToStandardDynamicRange];
|
|
666
|
+
}
|
|
667
|
+
}
|
|
646
668
|
double fullWidth = MAX(1, round(image.size.width * image.scale * scale));
|
|
647
669
|
double fullHeight = MAX(1, round(image.size.height * image.scale * scale));
|
|
648
670
|
// At most the image's larger side (a larger box looks the same, and an
|
|
@@ -798,6 +820,33 @@ NSString *FFFErrorMessage(NSError *error)
|
|
|
798
820
|
}
|
|
799
821
|
}
|
|
800
822
|
|
|
823
|
+
- (void) setDynamicRangeLimit: (NSString*)dynamicRangeLimit {
|
|
824
|
+
if (_dynamicRangeLimit == dynamicRangeLimit || [_dynamicRangeLimit isEqualToString: dynamicRangeLimit]) {
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
BOOL decodedToHDR = [self decodesToHDR];
|
|
828
|
+
_dynamicRangeLimit = [dynamicRangeLimit copy];
|
|
829
|
+
if (@available(iOS 17.0, tvOS 17.0, *)) {
|
|
830
|
+
// Constrained: the highlights brighter than SDR white, but not as
|
|
831
|
+
// bright as the display can go, as Apple suggests for images among
|
|
832
|
+
// other content.
|
|
833
|
+
self.preferredImageDynamicRange = [_dynamicRangeLimit isEqualToString: @"no-limit"] ? UIImageDynamicRangeHigh
|
|
834
|
+
: [_dynamicRangeLimit isEqualToString: @"constrained"] ? UIImageDynamicRangeConstrainedHigh
|
|
835
|
+
: [_dynamicRangeLimit isEqualToString: @"standard"] ? UIImageDynamicRangeStandard
|
|
836
|
+
: UIImageDynamicRangeUnspecified;
|
|
837
|
+
}
|
|
838
|
+
// Decoded again only to switch between HDR and SDR.
|
|
839
|
+
if ([self decodesToHDR] != decodedToHDR) {
|
|
840
|
+
self.needsDynamicRangeReload = YES;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// Whether HDR images are decoded to HDR (dynamicRangeLimit constrained or
|
|
845
|
+
// no-limit), rather than to SDR, their standard range version.
|
|
846
|
+
- (BOOL) decodesToHDR {
|
|
847
|
+
return [_dynamicRangeLimit isEqualToString: @"constrained"] || [_dynamicRangeLimit isEqualToString: @"no-limit"];
|
|
848
|
+
}
|
|
849
|
+
|
|
801
850
|
- (void) setPlaceholder: (UIImage*)placeholder {
|
|
802
851
|
if (_placeholder != placeholder) {
|
|
803
852
|
_placeholder = placeholder;
|
|
@@ -810,10 +859,124 @@ NSString *FFFErrorMessage(NSError *error)
|
|
|
810
859
|
[self updateHashImage];
|
|
811
860
|
}
|
|
812
861
|
|
|
813
|
-
// What shows while the image loads and if it fails:
|
|
814
|
-
//
|
|
862
|
+
// What shows while the image loads and if it fails: placeholderSource's
|
|
863
|
+
// image once it has loaded, the picked source's placeholder (its ThumbHash or
|
|
864
|
+
// blurDataUri), or the placeholder prop.
|
|
815
865
|
- (UIImage*) shownPlaceholder {
|
|
816
|
-
return self.hashImage ?: _placeholder;
|
|
866
|
+
return self.urlPlaceholderImage ?: self.hashImage ?: _placeholder;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// Shows the placeholder that shows now (shownPlaceholder) in place of
|
|
870
|
+
// `previous` if the view showed it, or if it showed nothing (one that arrived
|
|
871
|
+
// while nothing showed): never over a loaded image, or the image kept while
|
|
872
|
+
// a new source loads.
|
|
873
|
+
- (void) showChangedPlaceholder: (UIImage*)previous showing: (BOOL)showing {
|
|
874
|
+
UIImage* placeholder = [self shownPlaceholder];
|
|
875
|
+
if (placeholder == previous) {
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
BOOL showsNothing = super.image == nil && !self.showsLoadedImage && !self.blursOverNothing;
|
|
879
|
+
if (showing || (placeholder && showsNothing)) {
|
|
880
|
+
[self setImage: placeholder];
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// Images that views showed, by their source's cacheKey or url, while they're
|
|
885
|
+
// still in memory (held by a view or SDWebImage's memory cache), so
|
|
886
|
+
// placeholderSource finds one at once whatever size it was decoded at (the
|
|
887
|
+
// memory cache's key includes the size).
|
|
888
|
+
+ (NSMapTable<NSString*, UIImage*>*) shownImages {
|
|
889
|
+
static NSMapTable* images;
|
|
890
|
+
static dispatch_once_t once;
|
|
891
|
+
dispatch_once(&once, ^{
|
|
892
|
+
images = [NSMapTable strongToWeakObjectsMapTable];
|
|
893
|
+
});
|
|
894
|
+
return images;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// The key a remote image (http or https) is recorded under: its cacheKey or
|
|
898
|
+
// url. nil for others (data uris, files), which aren't recorded.
|
|
899
|
+
+ (NSString*) shownImageKey: (FFFastImageSource*)source {
|
|
900
|
+
NSString* scheme = source.url.scheme.lowercaseString;
|
|
901
|
+
if (![scheme isEqualToString: @"http"] && ![scheme isEqualToString: @"https"]) {
|
|
902
|
+
return nil;
|
|
903
|
+
}
|
|
904
|
+
return source.cacheKey.length > 0 ? source.cacheKey : source.url.absoluteString;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
+ (void) rememberShownImage: (UIImage*)image source: (FFFastImageSource*)source {
|
|
908
|
+
NSString* key = [self shownImageKey: source];
|
|
909
|
+
if (image && key) {
|
|
910
|
+
[[self shownImages] setObject: image forKey: key];
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
- (void) setPlaceholderSource: (FFFastImageSource*)placeholderSource {
|
|
915
|
+
_placeholderSource = placeholderSource;
|
|
916
|
+
[self cancelUrlPlaceholderLoad];
|
|
917
|
+
[self showUrlPlaceholderImage: nil];
|
|
918
|
+
self.needsUrlPlaceholderLoad = YES;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
- (void) cancelUrlPlaceholderLoad {
|
|
922
|
+
self.urlPlaceholderLoad++;
|
|
923
|
+
[self.urlPlaceholderOperation cancel];
|
|
924
|
+
self.urlPlaceholderOperation = nil;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// Loads placeholderSource's image with the image's load (after layout, so at
|
|
928
|
+
// the view's size, with the props applied), or after the image failed: not
|
|
929
|
+
// if the image shows at once (from the memory cache, or a data uri), or the
|
|
930
|
+
// placeholder is already loaded or loading.
|
|
931
|
+
- (void) startUrlPlaceholderLoad {
|
|
932
|
+
self.needsUrlPlaceholderLoad = NO;
|
|
933
|
+
if (!_placeholderSource.url || self.urlPlaceholderImage || self.urlPlaceholderOperation) {
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
if (_source && !self.hasErrored && [self loadsDuringLayout]) {
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
[self loadUrlPlaceholder];
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// Loads placeholderSource's image, as a source would load (its cache,
|
|
943
|
+
// headers and cacheKey), at a high priority, as it only helps if it comes
|
|
944
|
+
// before the image. One a view still shows (or that's still in memory) shows
|
|
945
|
+
// at once. A failure shows nothing more.
|
|
946
|
+
- (void) loadUrlPlaceholder {
|
|
947
|
+
[self cancelUrlPlaceholderLoad];
|
|
948
|
+
NSUInteger load = self.urlPlaceholderLoad;
|
|
949
|
+
FFFastImageSource* source = _placeholderSource;
|
|
950
|
+
NSString* key = [FFFastImageView shownImageKey: source];
|
|
951
|
+
UIImage* shown = key ? [[FFFastImageView shownImages] objectForKey: key] : nil;
|
|
952
|
+
if (shown) {
|
|
953
|
+
[self showUrlPlaceholderImage: shown];
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
SDWebImageOptions options = [source cacheOptions] | SDWebImageHighPriority | SDWebImageRetryFailed | SDWebImageHandleCookies;
|
|
957
|
+
SDWebImageContext* context = [self contextForSource: source box: [self decodeBox] cover: NO];
|
|
958
|
+
__weak typeof(self) weakSelf = self;
|
|
959
|
+
self.urlPlaceholderOperation = [[SDWebImageManager sharedManager] loadImageWithURL: source.url options: options context: context progress: nil completed: ^(UIImage* _Nullable image, NSData* _Nullable data, NSError* _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL* _Nullable imageURL) {
|
|
960
|
+
if (!finished || weakSelf.urlPlaceholderLoad != load) {
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
weakSelf.urlPlaceholderOperation = nil;
|
|
964
|
+
if (!image) {
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
[FFFastImageView rememberShownImage: image source: source];
|
|
968
|
+
[weakSelf showUrlPlaceholderImage: image];
|
|
969
|
+
}];
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
- (void) showUrlPlaceholderImage: (UIImage*)image {
|
|
973
|
+
if (self.urlPlaceholderImage == image) {
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
UIImage* previous = [self shownPlaceholder];
|
|
977
|
+
BOOL showing = self.showsPlaceholder;
|
|
978
|
+
self.urlPlaceholderImage = image;
|
|
979
|
+
[self showChangedPlaceholder: previous showing: showing];
|
|
817
980
|
}
|
|
818
981
|
|
|
819
982
|
// The picked source's ThumbHash or blurDataUri, or nil.
|
|
@@ -837,10 +1000,7 @@ NSString *FFFErrorMessage(NSError *error)
|
|
|
837
1000
|
BOOL showing = self.showsPlaceholder;
|
|
838
1001
|
self.hashImageKey = hash;
|
|
839
1002
|
self.hashImage = [FFFThumbHash imageForPlaceholder: hash];
|
|
840
|
-
|
|
841
|
-
if (showing && placeholder != previous) {
|
|
842
|
-
[self setImage: placeholder];
|
|
843
|
-
}
|
|
1003
|
+
[self showChangedPlaceholder: previous showing: showing];
|
|
844
1004
|
}
|
|
845
1005
|
|
|
846
1006
|
- (void) didSetProps {
|
|
@@ -865,9 +1025,42 @@ NSString *FFFErrorMessage(NSError *error)
|
|
|
865
1025
|
return;
|
|
866
1026
|
}
|
|
867
1027
|
[self reloadImage];
|
|
1028
|
+
} else if (self.needsDynamicRangeReload) {
|
|
1029
|
+
[self reloadForDynamicRange];
|
|
868
1030
|
} else {
|
|
869
1031
|
[self reloadIfResized];
|
|
870
1032
|
}
|
|
1033
|
+
// A new placeholderSource while the image loads, or after it failed (a
|
|
1034
|
+
// load waiting for the view's size starts it with the image's).
|
|
1035
|
+
if (self.needsUrlPlaceholderLoad && !_needsReload && !self.waitsForSize) {
|
|
1036
|
+
if (!self.hasCompleted || self.hasErrored) {
|
|
1037
|
+
[self startUrlPlaceholderLoad];
|
|
1038
|
+
}
|
|
1039
|
+
self.needsUrlPlaceholderLoad = NO;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// The same image decoded again for a dynamicRangeLimit between HDR and SDR,
|
|
1044
|
+
// as a resize is (reloadIfResized): without events once it has loaded, and
|
|
1045
|
+
// the image showing stays if the load fails. A display setting shouldn't
|
|
1046
|
+
// look like a new load.
|
|
1047
|
+
- (void) reloadForDynamicRange {
|
|
1048
|
+
self.needsDynamicRangeReload = NO;
|
|
1049
|
+
// Nothing to load, it failed, or it waits for the view's size: the next
|
|
1050
|
+
// load decodes as asked.
|
|
1051
|
+
if (!_source || self.hasErrored || self.waitsForSize) {
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
// A data uri is decoded at once, with its events (see reloadImage).
|
|
1055
|
+
if ([[_source.url absoluteString] hasPrefix: @"data:image"]) {
|
|
1056
|
+
[self reloadImage];
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
SDWebImageOptions options = [self loadOptions];
|
|
1060
|
+
if (self.showsLoadedImage) {
|
|
1061
|
+
options |= SDWebImageDelayPlaceholder;
|
|
1062
|
+
}
|
|
1063
|
+
[self downloadImage: _source options: options context: [self loadContext] events: !self.hasCompleted];
|
|
871
1064
|
}
|
|
872
1065
|
|
|
873
1066
|
// Whether images are decoded at about the view's size (downsample, and always
|
|
@@ -938,6 +1131,8 @@ NSString *FFFErrorMessage(NSError *error)
|
|
|
938
1131
|
|
|
939
1132
|
- (void) reloadImage {
|
|
940
1133
|
_needsReload = NO;
|
|
1134
|
+
self.needsDynamicRangeReload = NO;
|
|
1135
|
+
self.needsUrlPlaceholderLoad = NO;
|
|
941
1136
|
self.waitsForSize = NO;
|
|
942
1137
|
self.loadsSwitchedSource = self.switchesSource;
|
|
943
1138
|
self.switchesSource = NO;
|
|
@@ -961,13 +1156,19 @@ NSString *FFFErrorMessage(NSError *error)
|
|
|
961
1156
|
self.onFastImageLoadStart(@{});
|
|
962
1157
|
}
|
|
963
1158
|
// Use SDWebImage API to support external format like WebP images
|
|
964
|
-
|
|
1159
|
+
// (as sd_imageWithData does), decoded to HDR or SDR as a
|
|
1160
|
+
// download is (see contextForSource).
|
|
1161
|
+
UIImage* image = [[SDImageCodersManager sharedManager] decodedImageWithData: [NSData dataWithContentsOfURL: _source.url] options: @{
|
|
1162
|
+
SDImageCoderDecodeScaleFactor: @1,
|
|
1163
|
+
SDImageCoderDecodeToHDR: @([self decodesToHDR]),
|
|
1164
|
+
}];
|
|
965
1165
|
if (!image) {
|
|
966
1166
|
// Not decodable: fail like a remote image, showing the
|
|
967
1167
|
// placeholder.
|
|
968
1168
|
[self setImage: [self shownPlaceholder]];
|
|
969
1169
|
self.showsLoadedImage = NO;
|
|
970
1170
|
[self sendOnError: @"Failed to decode the image"];
|
|
1171
|
+
[self startUrlPlaceholderLoad];
|
|
971
1172
|
if (self.onFastImageLoadEnd) {
|
|
972
1173
|
self.onFastImageLoadEnd([self loadEndEvent: NO]);
|
|
973
1174
|
}
|
|
@@ -1010,10 +1211,15 @@ NSString *FFFErrorMessage(NSError *error)
|
|
|
1010
1211
|
self.retriedAfterBackground = NO;
|
|
1011
1212
|
[self stopWaitingForActive];
|
|
1012
1213
|
|
|
1214
|
+
[self startUrlPlaceholderLoad];
|
|
1013
1215
|
[self downloadImage: _source options: options context: [self loadContext] events: YES];
|
|
1014
|
-
} else
|
|
1015
|
-
|
|
1016
|
-
|
|
1216
|
+
} else {
|
|
1217
|
+
if ([self shownPlaceholder]) {
|
|
1218
|
+
[self setImage: [self shownPlaceholder]];
|
|
1219
|
+
self.showsLoadedImage = NO;
|
|
1220
|
+
}
|
|
1221
|
+
// No image: only placeholders show.
|
|
1222
|
+
[self startUrlPlaceholderLoad];
|
|
1017
1223
|
}
|
|
1018
1224
|
}
|
|
1019
1225
|
|
|
@@ -1070,10 +1276,15 @@ NSString *FFFErrorMessage(NSError *error)
|
|
|
1070
1276
|
context[SDWebImageContextDownloadRequestModifier] = source.requestModifier;
|
|
1071
1277
|
context[SDWebImageContextImageLoader] = source.imageLoader;
|
|
1072
1278
|
context[SDWebImageContextCacheKeyFilter] = source.cacheKeyFilter;
|
|
1279
|
+
if (@available(iOS 17.0, tvOS 17.0, *)) {
|
|
1280
|
+
// An HDR image (e.g. a photo with a gain map) decoded to HDR, or to
|
|
1281
|
+
// SDR, its standard range version.
|
|
1282
|
+
context[SDWebImageContextImageDecodeToHDR] = @([self decodesToHDR]);
|
|
1283
|
+
}
|
|
1073
1284
|
if (CGSizeEqualToSize(box, CGSizeZero)) {
|
|
1074
1285
|
[FFFDownsampledImage addFullSizeToContext: context];
|
|
1075
1286
|
[self applyCacheOptions: context source: source fullSize: YES];
|
|
1076
|
-
[FFFDownsampledImage
|
|
1287
|
+
[FFFDownsampledImage forgetUnusableImageForURL: source.url context: context];
|
|
1077
1288
|
return context;
|
|
1078
1289
|
}
|
|
1079
1290
|
[FFFDownsampledImage addToContext: context box: box cover: cover];
|
|
@@ -1085,6 +1296,7 @@ NSString *FFFErrorMessage(NSError *error)
|
|
|
1085
1296
|
context[SDWebImageContextOriginalQueryCacheType] = @(SDImageCacheTypeNone);
|
|
1086
1297
|
}
|
|
1087
1298
|
[self applyCacheOptions: context source: source fullSize: NO];
|
|
1299
|
+
[FFFDownsampledImage forgetUnusableImageForURL: source.url context: context];
|
|
1088
1300
|
return context;
|
|
1089
1301
|
}
|
|
1090
1302
|
|
|
@@ -1243,6 +1455,8 @@ NSString *FFFErrorMessage(NSError *error)
|
|
|
1243
1455
|
[weakSelf setImage: current];
|
|
1244
1456
|
}
|
|
1245
1457
|
weakSelf.showsLoadedImage = NO;
|
|
1458
|
+
// Shows instead of the other placeholders once it loads.
|
|
1459
|
+
[weakSelf startUrlPlaceholderLoad];
|
|
1246
1460
|
[weakSelf sendOnError: [source errorMessage: error]];
|
|
1247
1461
|
if (weakSelf.onFastImageLoadEnd) {
|
|
1248
1462
|
weakSelf.onFastImageLoadEnd([weakSelf loadEndEvent: NO]);
|
|
@@ -1250,6 +1464,12 @@ NSString *FFFErrorMessage(NSError *error)
|
|
|
1250
1464
|
} else {
|
|
1251
1465
|
weakSelf.hasCompleted = YES;
|
|
1252
1466
|
weakSelf.showsLoadedImage = YES;
|
|
1467
|
+
// For other views' placeholderSource.
|
|
1468
|
+
[FFFastImageView rememberShownImage: image source: source];
|
|
1469
|
+
// The image came first: its placeholder isn't needed.
|
|
1470
|
+
if (!weakSelf.urlPlaceholderImage) {
|
|
1471
|
+
[weakSelf cancelUrlPlaceholderLoad];
|
|
1472
|
+
}
|
|
1253
1473
|
[weakSelf sendOnLoad: image];
|
|
1254
1474
|
if (weakSelf.onFastImageLoadEnd) {
|
|
1255
1475
|
weakSelf.onFastImageLoadEnd([weakSelf loadEndEvent: YES]);
|
|
@@ -1299,11 +1519,14 @@ NSString *FFFErrorMessage(NSError *error)
|
|
|
1299
1519
|
self.waitsForSize = NO;
|
|
1300
1520
|
[self stopWaitingForActive];
|
|
1301
1521
|
[self sd_cancelLatestImageLoad];
|
|
1522
|
+
[self cancelUrlPlaceholderLoad];
|
|
1302
1523
|
}
|
|
1303
1524
|
|
|
1304
1525
|
- (void) dealloc {
|
|
1305
1526
|
[self stopWaitingForActive];
|
|
1306
1527
|
[self sd_cancelLatestImageLoad];
|
|
1528
|
+
// Fabric frees views it doesn't recycle without stop.
|
|
1529
|
+
[_urlPlaceholderOperation cancel];
|
|
1307
1530
|
}
|
|
1308
1531
|
|
|
1309
1532
|
@end
|
|
@@ -102,6 +102,22 @@ static id _Nullable FFFJSON(const folly::dynamic &value)
|
|
|
102
102
|
return value.isNull() ? nil : FFFObjectFromDynamic(value);
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
// Whether the placeholder prop is a url to load apart from the image (a
|
|
106
|
+
// source with an http or https uri), rather than an image to show at once: a
|
|
107
|
+
// bundled image (served by the packager in development), a file or a data uri.
|
|
108
|
+
static BOOL FFFIsUrlPlaceholder(id _Nullable placeholder)
|
|
109
|
+
{
|
|
110
|
+
if (![placeholder isKindOfClass:[NSDictionary class]] || placeholder[@"__packager_asset"]) {
|
|
111
|
+
return NO;
|
|
112
|
+
}
|
|
113
|
+
id uri = placeholder[@"uri"];
|
|
114
|
+
if (![uri isKindOfClass:[NSString class]]) {
|
|
115
|
+
return NO;
|
|
116
|
+
}
|
|
117
|
+
NSString *scheme = [NSURL URLWithString:uri].scheme.lowercaseString;
|
|
118
|
+
return [scheme isEqualToString:@"http"] || [scheme isEqualToString:@"https"];
|
|
119
|
+
}
|
|
120
|
+
|
|
105
121
|
// The events JS has a handler for: the handledEvents prop's bits (see
|
|
106
122
|
// src/specs/FastImageViewNativeComponent.ts).
|
|
107
123
|
typedef NS_OPTIONS(int32_t, FFFEvent) {
|
|
@@ -234,7 +250,9 @@ typedef NS_OPTIONS(int32_t, FFFEvent) {
|
|
|
234
250
|
}
|
|
235
251
|
if (all || oldViewProps.placeholder != newViewProps.placeholder) {
|
|
236
252
|
id placeholder = FFFJSON(newViewProps.placeholder);
|
|
237
|
-
|
|
253
|
+
BOOL loads = FFFIsUrlPlaceholder(placeholder);
|
|
254
|
+
view.placeholder = placeholder && !loads ? [RCTConvert UIImage:placeholder] : nil;
|
|
255
|
+
view.placeholderSource = loads ? [RCTConvert FFFastImageSource:placeholder] : nil;
|
|
238
256
|
}
|
|
239
257
|
if (all || oldViewProps.sourcePlaceholders != newViewProps.sourcePlaceholders) {
|
|
240
258
|
NSMutableArray<NSString *> *placeholders = [NSMutableArray arrayWithCapacity:newViewProps.sourcePlaceholders.size()];
|
|
@@ -287,6 +305,9 @@ typedef NS_OPTIONS(int32_t, FFFEvent) {
|
|
|
287
305
|
if (all || oldViewProps.downsample != newViewProps.downsample) {
|
|
288
306
|
view.downsample = newViewProps.downsample;
|
|
289
307
|
}
|
|
308
|
+
if (all || oldViewProps.dynamicRangeLimit != newViewProps.dynamicRangeLimit) {
|
|
309
|
+
view.dynamicRangeLimit = FFFString(newViewProps.dynamicRangeLimit);
|
|
310
|
+
}
|
|
290
311
|
if (all || oldViewProps.blurRadius != newViewProps.blurRadius) {
|
|
291
312
|
view.blurRadius = newViewProps.blurRadius;
|
|
292
313
|
}
|
package/package.json
CHANGED
package/src/index.js.flow
CHANGED
|
@@ -110,6 +110,13 @@ export type PreloadResult = PreloadSuccess | PreloadFailure
|
|
|
110
110
|
export type PreloadFn = (
|
|
111
111
|
sources: Array<FastImageSource>,
|
|
112
112
|
) => Promise<Array<PreloadResult>>
|
|
113
|
+
export type PlaceholderSource = {
|
|
114
|
+
uri?: string,
|
|
115
|
+
headers?: Object,
|
|
116
|
+
cacheKey?: string,
|
|
117
|
+
cache?: CacheControls,
|
|
118
|
+
}
|
|
119
|
+
|
|
113
120
|
export type SourcePlaceholder = {
|
|
114
121
|
thumbhash?: string,
|
|
115
122
|
blurDataUri?: string,
|
|
@@ -139,7 +146,7 @@ export type FastImageProps = $ReadOnly<{|
|
|
|
139
146
|
onProgress?: ?(event: OnProgressEvent) => void,
|
|
140
147
|
|
|
141
148
|
source?: ?(FastImageSource | Array<FastImageSource> | number),
|
|
142
|
-
placeholder?: ?(number | string | SourcePlaceholder),
|
|
149
|
+
placeholder?: ?(number | string | SourcePlaceholder | PlaceholderSource),
|
|
143
150
|
// Deprecated: use placeholder, which is the same.
|
|
144
151
|
defaultSource?: ?number,
|
|
145
152
|
|
|
@@ -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/src/index.tsx
CHANGED
|
@@ -16,7 +16,12 @@ import { fromStyle, resolveObjectFit } from './objectFit'
|
|
|
16
16
|
import { objectPositionOrCentered, type ObjectPosition } from './objectPosition'
|
|
17
17
|
import FastImageView from './specs/FastImageViewNativeComponent'
|
|
18
18
|
import NativeFastImageModule from './specs/NativeFastImageModule'
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
dataUri,
|
|
21
|
+
hasSourcePlaceholder,
|
|
22
|
+
isImageName,
|
|
23
|
+
placeholderSource,
|
|
24
|
+
} from './placeholders'
|
|
20
25
|
import { warnIfChildren, warnOnce } from './warnings'
|
|
21
26
|
|
|
22
27
|
/** How the image fits the view: see [`objectFit`](#objectfit). */
|
|
@@ -175,6 +180,12 @@ export type Source = {
|
|
|
175
180
|
/** A `thumbhash` or `blurDataUri` as `placeholder`. */
|
|
176
181
|
export type SourcePlaceholder = Pick<Source, 'thumbhash' | 'blurDataUri'>
|
|
177
182
|
|
|
183
|
+
/** A url as `placeholder`, loaded like a source (see `placeholder`). */
|
|
184
|
+
export type PlaceholderSource = Pick<
|
|
185
|
+
Source,
|
|
186
|
+
'uri' | 'headers' | 'cacheKey' | 'cache'
|
|
187
|
+
>
|
|
188
|
+
|
|
178
189
|
/** How the image fades in: see [`transition`](#transition). */
|
|
179
190
|
export type Transition = {
|
|
180
191
|
/**
|
|
@@ -365,17 +376,27 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
|
|
|
365
376
|
* `placeholder={{ thumbhash }}`.
|
|
366
377
|
* - A tiny image as a data uri, shown blurred:
|
|
367
378
|
* `placeholder={{ blurDataUri }}`.
|
|
379
|
+
* - A url, loaded and cached like a source:
|
|
380
|
+
* `placeholder="https://example.com/placeholder.png"`, or a source with
|
|
381
|
+
* `headers` or a `cacheKey`: `placeholder={{ uri, headers }}`.
|
|
382
|
+
* - A url only shown if it's already cached, e.g. a list's thumbnail while
|
|
383
|
+
* the full-size image loads: `placeholder={{ uri, cache: 'cacheOnly' }}`.
|
|
368
384
|
*
|
|
369
385
|
* A source's own `thumbhash` or `blurDataUri` is shown instead, if it has
|
|
370
|
-
* one.
|
|
386
|
+
* one; a url placeholder shows over it once it has loaded.
|
|
371
387
|
*
|
|
372
388
|
* Notes:
|
|
373
389
|
*
|
|
374
|
-
* - A url isn't loaded: it's ignored, with a warning in development.
|
|
375
390
|
* - On Android, a `require()`d image doesn't show in debug builds, where
|
|
376
391
|
* the dev server serves it. Release builds show it.
|
|
392
|
+
* - On the web, a url loads like any image, also with `'cacheOnly'`: a
|
|
393
|
+
* browser can't look only in its cache.
|
|
377
394
|
*/
|
|
378
|
-
placeholder?:
|
|
395
|
+
placeholder?:
|
|
396
|
+
| ImageRequireSource
|
|
397
|
+
| string
|
|
398
|
+
| SourcePlaceholder
|
|
399
|
+
| PlaceholderSource
|
|
379
400
|
/**
|
|
380
401
|
* @deprecated Use `placeholder`, which does the same.
|
|
381
402
|
*/
|
|
@@ -555,6 +576,25 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
|
|
|
555
576
|
* @default true
|
|
556
577
|
*/
|
|
557
578
|
downsample?: boolean
|
|
579
|
+
/**
|
|
580
|
+
* How bright HDR photos (e.g. photos taken on an iPhone) can be, on
|
|
581
|
+
* displays that support it:
|
|
582
|
+
*
|
|
583
|
+
* - `'standard'`: in standard range, like the rest of the screen.
|
|
584
|
+
* - `'constrained'`: in HDR, their highlights brighter than the rest of
|
|
585
|
+
* the screen but held below the display's peak, e.g. for images among
|
|
586
|
+
* other content (a feed).
|
|
587
|
+
* - `'no-limit'`: in full HDR, e.g. for a full-screen photo viewer.
|
|
588
|
+
*
|
|
589
|
+
* Not set, iOS shows them in standard range, and the web as the browser
|
|
590
|
+
* does by default (usually HDR). On iOS and tvOS 17 and later; Android
|
|
591
|
+
* shows them in standard range. On the web it's CSS's
|
|
592
|
+
* `dynamic-range-limit`.
|
|
593
|
+
*
|
|
594
|
+
* @platform ios
|
|
595
|
+
* @platform web
|
|
596
|
+
*/
|
|
597
|
+
dynamicRangeLimit?: 'standard' | 'constrained' | 'no-limit'
|
|
558
598
|
// The radius is converted from points to the bitmap's pixels on both
|
|
559
599
|
// platforms, so it matches React Native's Image.
|
|
560
600
|
/**
|
|
@@ -687,7 +727,8 @@ function sourcePlaceholder(source: unknown): string {
|
|
|
687
727
|
}
|
|
688
728
|
|
|
689
729
|
const resolvePlaceholder = (
|
|
690
|
-
|
|
730
|
+
// A source object here has no url (e.g. { uri: name }, a bundled image).
|
|
731
|
+
placeholder?: ImageRequireSource | string | PlaceholderSource,
|
|
691
732
|
): string | object | null => {
|
|
692
733
|
if (typeof placeholder === 'string') {
|
|
693
734
|
// An image bundled with the app, by name, as React Native's Image
|
|
@@ -706,7 +747,9 @@ const resolvePlaceholder = (
|
|
|
706
747
|
if (!placeholder) {
|
|
707
748
|
return null
|
|
708
749
|
}
|
|
709
|
-
const resolved = Image.resolveAssetSource(
|
|
750
|
+
const resolved = Image.resolveAssetSource(
|
|
751
|
+
placeholder as ImageRequireSource | { uri: string },
|
|
752
|
+
)
|
|
710
753
|
if (!resolved) {
|
|
711
754
|
return null
|
|
712
755
|
}
|
|
@@ -904,13 +947,17 @@ function FastImageBase({
|
|
|
904
947
|
: undefined,
|
|
905
948
|
}
|
|
906
949
|
}, [source, shared])
|
|
907
|
-
// A
|
|
908
|
-
//
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
950
|
+
// A url in `placeholder` goes to the native view as a source, loaded
|
|
951
|
+
// apart from the image, in place of defaultSource. A thumbhash or
|
|
952
|
+
// blurDataUri goes in sourcePlaceholders instead: it isn't an image to
|
|
953
|
+
// show as it is, but defaultSource still is.
|
|
954
|
+
const resolvedPlaceholder =
|
|
955
|
+
placeholderSource(placeholder) ??
|
|
956
|
+
resolvePlaceholder(
|
|
957
|
+
hasSourcePlaceholder(placeholder)
|
|
958
|
+
? defaultSource
|
|
959
|
+
: (placeholder ?? defaultSource),
|
|
960
|
+
)
|
|
914
961
|
|
|
915
962
|
return (
|
|
916
963
|
<FastImageView
|