@depup/react-native-fast-image 8.6.3-depup.0 → 8.13.1-depup.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 +2 -2
- package/RNFastImage.podspec +5 -2
- package/android/build.gradle +13 -0
- package/android/src/main/AndroidManifestNew.xml +3 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageCookieHandler.java +84 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageEvent.java +45 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageEvents.java +60 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageGif.java +110 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageOkHttpProgressGlideModule.java +65 -5
- package/android/src/main/java/com/dylanvann/fastimage/FastImageRequestListener.java +65 -22
- package/android/src/main/java/com/dylanvann/fastimage/FastImageShadowNode.java +21 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageSource.java +21 -1
- package/android/src/main/java/com/dylanvann/fastimage/FastImageSourceSize.java +237 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageViewConverter.java +14 -5
- package/android/src/main/java/com/dylanvann/fastimage/FastImageViewManager.java +66 -22
- package/android/src/main/java/com/dylanvann/fastimage/FastImageViewModule.java +130 -24
- package/android/src/main/java/com/dylanvann/fastimage/FastImageViewWithUrl.java +359 -39
- package/android/src/main/java/com/dylanvann/fastimage/FastImageWebGlideUrl.java +13 -0
- package/changes.json +1 -1
- package/dist/index.cjs.js +166 -130
- package/dist/index.cjs.js.flow +33 -2
- package/dist/index.d.ts +77 -22
- package/dist/index.js +141 -121
- package/dist/index.js.flow +33 -2
- package/ios/FastImage/FFFDownsampledImage.h +28 -0
- package/ios/FastImage/FFFDownsampledImage.m +169 -0
- package/ios/FastImage/FFFastImageSource.h +6 -0
- package/ios/FastImage/FFFastImageSource.m +13 -0
- package/ios/FastImage/FFFastImageView.h +17 -0
- package/ios/FastImage/FFFastImageView.m +498 -71
- package/ios/FastImage/FFFastImageViewManager.m +106 -8
- package/ios/FastImage/RCTConvert+FFFastImage.m +4 -1
- package/package.json +31 -43
- package/dist/index.d.ts.map +0 -1
- package/dist/index.test.d.ts +0 -2
- package/dist/index.test.d.ts.map +0 -1
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
#import "FFFastImageView.h"
|
|
2
2
|
#import <SDWebImage/UIImage+MultiFormat.h>
|
|
3
3
|
#import <SDWebImage/UIView+WebCache.h>
|
|
4
|
+
#import <React/RCTUtils.h>
|
|
5
|
+
#import <SDWebImage/SDWebImageError.h>
|
|
6
|
+
#import "FFFDownsampledImage.h"
|
|
4
7
|
|
|
5
8
|
@interface FFFastImageView ()
|
|
6
9
|
|
|
@@ -11,48 +14,175 @@
|
|
|
11
14
|
@property(nonatomic, assign) BOOL needsReload;
|
|
12
15
|
|
|
13
16
|
@property(nonatomic, strong) NSDictionary* onLoadEvent;
|
|
17
|
+
@property(nonatomic, strong) NSDictionary* onErrorEvent;
|
|
18
|
+
// The image before tinting, kept while a tint is applied so the tint can be
|
|
19
|
+
// changed or removed. nil when there's no tint (super.image is untinted).
|
|
20
|
+
@property(nonatomic, strong) UIImage* untintedImage;
|
|
21
|
+
// Whether the current load was already restarted after the app came back
|
|
22
|
+
// from the background (see downloadImage:).
|
|
23
|
+
@property(nonatomic, assign) BOOL retriedAfterBackground;
|
|
24
|
+
// Waits for the app to be active again, to restart a load.
|
|
25
|
+
@property(nonatomic, strong) id activeObserver;
|
|
26
|
+
// Whether the view shows an image that loaded (not defaultSource or nothing).
|
|
27
|
+
// A new source then keeps it until the new image has loaded (see reloadImage).
|
|
28
|
+
@property(nonatomic, assign) BOOL showsLoadedImage;
|
|
29
|
+
// downsample: a load waits for the view's size (see didSetProps).
|
|
30
|
+
@property(nonatomic, assign) BOOL waitsForSize;
|
|
31
|
+
// The size (in pixels) the image showing or loading was decoded for, and
|
|
32
|
+
// whether it covers it; zero for a full-size image.
|
|
33
|
+
@property(nonatomic, assign) CGSize decodedBox;
|
|
34
|
+
@property(nonatomic, assign) BOOL decodedCover;
|
|
35
|
+
// Counts loads, so a quiet reload's completion can tell if it's still current.
|
|
36
|
+
@property(nonatomic, assign) NSUInteger loadCount;
|
|
14
37
|
|
|
15
38
|
@end
|
|
16
39
|
|
|
40
|
+
// When the app last went to the background (CACurrentMediaTime), or 0.
|
|
41
|
+
static CFTimeInterval FFFEnteredBackgroundAt = 0;
|
|
42
|
+
|
|
17
43
|
@implementation FFFastImageView
|
|
18
44
|
|
|
45
|
+
+ (void) initialize {
|
|
46
|
+
if (self != [FFFastImageView class]) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
[[NSNotificationCenter defaultCenter] addObserverForName: UIApplicationDidEnterBackgroundNotification
|
|
50
|
+
object: nil
|
|
51
|
+
queue: [NSOperationQueue mainQueue]
|
|
52
|
+
usingBlock: ^(NSNotification* notification) {
|
|
53
|
+
FFFEnteredBackgroundAt = CACurrentMediaTime();
|
|
54
|
+
}];
|
|
55
|
+
}
|
|
56
|
+
|
|
19
57
|
- (id) init {
|
|
20
58
|
self = [super init];
|
|
21
59
|
self.resizeMode = RCTResizeModeCover;
|
|
22
60
|
self.clipsToBounds = YES;
|
|
61
|
+
_loopCount = -1;
|
|
23
62
|
return self;
|
|
24
63
|
}
|
|
25
64
|
|
|
65
|
+
- (void) setImageRendering: (NSString*)imageRendering {
|
|
66
|
+
_imageRendering = [imageRendering copy];
|
|
67
|
+
if ([imageRendering isEqualToString: @"smooth"]) {
|
|
68
|
+
// Trilinear: drawn smaller from mipmaps, so a large image drawn much
|
|
69
|
+
// smaller is smoothed instead of aliased (#445). The mipmaps take
|
|
70
|
+
// about a third more memory than the decoded image.
|
|
71
|
+
self.layer.minificationFilter = kCAFilterTrilinear;
|
|
72
|
+
self.layer.magnificationFilter = kCAFilterLinear;
|
|
73
|
+
} else if ([imageRendering isEqualToString: @"pixelated"]) {
|
|
74
|
+
// Nearest neighbor: sharp pixels, e.g. for pixel art (#926).
|
|
75
|
+
self.layer.minificationFilter = kCAFilterNearest;
|
|
76
|
+
self.layer.magnificationFilter = kCAFilterNearest;
|
|
77
|
+
} else {
|
|
78
|
+
self.layer.minificationFilter = kCAFilterLinear;
|
|
79
|
+
self.layer.magnificationFilter = kCAFilterLinear;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
- (void) setLoopCount: (NSInteger)loopCount {
|
|
84
|
+
if (_loopCount == loopCount) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
_loopCount = loopCount;
|
|
88
|
+
// SDAnimatedImageView uses animationRepeatCount (0 is forever) instead of
|
|
89
|
+
// the file's loop count when shouldCustomLoopCount is set, for the next
|
|
90
|
+
// image it shows.
|
|
91
|
+
self.shouldCustomLoopCount = loopCount >= 0;
|
|
92
|
+
if (loopCount >= 0) {
|
|
93
|
+
self.animationRepeatCount = loopCount;
|
|
94
|
+
} else if (self.player && [self.image conformsToProtocol: @protocol(SDAnimatedImage)]) {
|
|
95
|
+
self.player.totalLoopCount = [(id<SDAnimatedImage>) self.image animatedImageLoopCount];
|
|
96
|
+
}
|
|
97
|
+
// Apply it to the image that's showing, and play it again (unless it's
|
|
98
|
+
// paused: then it waits on the first frame).
|
|
99
|
+
if (self.player) {
|
|
100
|
+
[self.player seekToFrameAtIndex: 0 loopCount: 0];
|
|
101
|
+
if (!_paused) {
|
|
102
|
+
[self startAnimating];
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
- (void) setPaused: (BOOL)paused {
|
|
108
|
+
if (_paused == paused) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
_paused = paused;
|
|
112
|
+
// SDAnimatedImageView starts an animated image when it's shown, and again
|
|
113
|
+
// when the view comes back on screen, unless autoPlayAnimatedImage is off.
|
|
114
|
+
// Stopping keeps the frame it's on (resetFrameIndexWhenStopped is off).
|
|
115
|
+
self.autoPlayAnimatedImage = !paused;
|
|
116
|
+
if (paused) {
|
|
117
|
+
[self stopAnimating];
|
|
118
|
+
} else {
|
|
119
|
+
[self startAnimating];
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
26
123
|
- (void) setResizeMode: (RCTResizeMode)resizeMode {
|
|
27
124
|
if (_resizeMode != resizeMode) {
|
|
28
125
|
_resizeMode = resizeMode;
|
|
29
|
-
self
|
|
126
|
+
[self updateContentMode];
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// The content mode for resizeMode. `center` scales an image larger than the
|
|
131
|
+
// view down to fit, as React Native's Image and Android do (#866); only a
|
|
132
|
+
// smaller one is shown at its own size. UIViewContentModeCenter alone showed
|
|
133
|
+
// large images at full size, cropped. So it depends on the image and the
|
|
134
|
+
// view's size.
|
|
135
|
+
- (void) updateContentMode {
|
|
136
|
+
UIViewContentMode contentMode = (UIViewContentMode) _resizeMode;
|
|
137
|
+
if (_resizeMode == RCTResizeModeCenter) {
|
|
138
|
+
CGSize imageSize = super.image.size;
|
|
139
|
+
CGSize viewSize = self.bounds.size;
|
|
140
|
+
if (imageSize.width > viewSize.width || imageSize.height > viewSize.height) {
|
|
141
|
+
contentMode = UIViewContentModeScaleAspectFit;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (self.contentMode != contentMode) {
|
|
145
|
+
self.contentMode = contentMode;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
- (void) layoutSubviews {
|
|
150
|
+
[super layoutSubviews];
|
|
151
|
+
[self updateContentMode];
|
|
152
|
+
if (self.waitsForSize) {
|
|
153
|
+
if ([self hasSize]) {
|
|
154
|
+
[self reloadImage];
|
|
155
|
+
}
|
|
156
|
+
} else {
|
|
157
|
+
[self reloadIfResized];
|
|
30
158
|
}
|
|
31
159
|
}
|
|
32
160
|
|
|
33
161
|
- (void) setOnFastImageLoadEnd: (RCTDirectEventBlock)onFastImageLoadEnd {
|
|
34
162
|
_onFastImageLoadEnd = onFastImageLoadEnd;
|
|
35
|
-
if (self.hasCompleted) {
|
|
163
|
+
if (self.hasCompleted && _onFastImageLoadEnd) {
|
|
36
164
|
_onFastImageLoadEnd(@{});
|
|
37
165
|
}
|
|
38
166
|
}
|
|
39
167
|
|
|
40
168
|
- (void) setOnFastImageLoad: (RCTDirectEventBlock)onFastImageLoad {
|
|
41
169
|
_onFastImageLoad = onFastImageLoad;
|
|
42
|
-
if (self.hasCompleted) {
|
|
170
|
+
if (self.hasCompleted && _onFastImageLoad) {
|
|
43
171
|
_onFastImageLoad(self.onLoadEvent);
|
|
44
172
|
}
|
|
45
173
|
}
|
|
46
174
|
|
|
47
175
|
- (void) setOnFastImageError: (RCTDirectEventBlock)onFastImageError {
|
|
48
176
|
_onFastImageError = onFastImageError;
|
|
49
|
-
if (self.hasErrored) {
|
|
50
|
-
_onFastImageError(
|
|
177
|
+
if (self.hasErrored && _onFastImageError) {
|
|
178
|
+
_onFastImageError(self.onErrorEvent);
|
|
51
179
|
}
|
|
52
180
|
}
|
|
53
181
|
|
|
54
182
|
- (void) setOnFastImageLoadStart: (RCTDirectEventBlock)onFastImageLoadStart {
|
|
55
|
-
|
|
183
|
+
// Send it for a load that has already started. When a reload is pending
|
|
184
|
+
// (e.g. source set in the same update), reloadImage sends it.
|
|
185
|
+
if (_source && !_needsReload && !self.hasSentOnLoadStart && onFastImageLoadStart) {
|
|
56
186
|
_onFastImageLoadStart = onFastImageLoadStart;
|
|
57
187
|
onFastImageLoadStart(@{});
|
|
58
188
|
self.hasSentOnLoadStart = YES;
|
|
@@ -63,36 +193,91 @@
|
|
|
63
193
|
}
|
|
64
194
|
|
|
65
195
|
- (void) setImageColor: (UIColor*)imageColor {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
196
|
+
_imageColor = imageColor;
|
|
197
|
+
// Re-apply to the untinted image, so the tint can change or be removed.
|
|
198
|
+
UIImage* image = self.untintedImage ?: super.image;
|
|
199
|
+
if (image) {
|
|
200
|
+
[self setImage: image];
|
|
71
201
|
}
|
|
72
202
|
}
|
|
73
203
|
|
|
74
204
|
- (UIImage*) makeImage: (UIImage*)image withTint: (UIColor*)color {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
205
|
+
// FIX: Prevent crash on zero/invalid image dimensions
|
|
206
|
+
if (!image || image.size.width <= 0 || image.size.height <= 0) {
|
|
207
|
+
return image;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
UIImage* templateImage = [image imageWithRenderingMode: UIImageRenderingModeAlwaysTemplate];
|
|
211
|
+
CGRect rect = CGRectMake(0, 0, image.size.width, image.size.height);
|
|
212
|
+
UIImage* newImage;
|
|
213
|
+
if (@available(iOS 10.0, tvOS 10.0, *)) {
|
|
214
|
+
// UIGraphicsBeginImageContextWithOptions is deprecated since iOS 17.
|
|
215
|
+
// Keep the source image's scale and a standard-range (8-bit) bitmap,
|
|
216
|
+
// matching what it produced.
|
|
217
|
+
UIGraphicsImageRendererFormat* format = [[UIGraphicsImageRendererFormat alloc] init];
|
|
218
|
+
format.scale = image.scale;
|
|
219
|
+
format.opaque = NO;
|
|
220
|
+
if (@available(iOS 12.0, tvOS 12.0, *)) {
|
|
221
|
+
format.preferredRange = UIGraphicsImageRendererFormatRangeStandard;
|
|
222
|
+
} else {
|
|
223
|
+
format.prefersExtendedRange = NO;
|
|
224
|
+
}
|
|
225
|
+
UIGraphicsImageRenderer* renderer = [[UIGraphicsImageRenderer alloc] initWithSize: image.size format: format];
|
|
226
|
+
newImage = [renderer imageWithActions: ^(UIGraphicsImageRendererContext* context) {
|
|
227
|
+
[color set];
|
|
228
|
+
[templateImage drawInRect: rect];
|
|
229
|
+
}];
|
|
230
|
+
} else {
|
|
231
|
+
// iOS/tvOS 9. Remove this branch once the minimum is iOS 10+.
|
|
232
|
+
UIGraphicsBeginImageContextWithOptions(image.size, NO, image.scale);
|
|
233
|
+
[color set];
|
|
234
|
+
[templateImage drawInRect: rect];
|
|
235
|
+
newImage = UIGraphicsGetImageFromCurrentImageContext();
|
|
236
|
+
UIGraphicsEndImageContext();
|
|
237
|
+
}
|
|
81
238
|
return newImage;
|
|
82
239
|
}
|
|
83
240
|
|
|
84
241
|
- (void) setImage: (UIImage*)image {
|
|
85
242
|
if (self.imageColor != nil) {
|
|
243
|
+
self.untintedImage = image;
|
|
86
244
|
super.image = [self makeImage: image withTint: self.imageColor];
|
|
87
245
|
} else {
|
|
246
|
+
self.untintedImage = nil;
|
|
88
247
|
super.image = image;
|
|
89
248
|
}
|
|
249
|
+
[self updateContentMode];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// The error's description, with the HTTP status code when there is one (as on
|
|
253
|
+
// Android): SDWebImage keeps the code out of the description. Also for preload
|
|
254
|
+
// results.
|
|
255
|
+
NSString *FFFErrorMessage(NSError *error)
|
|
256
|
+
{
|
|
257
|
+
NSNumber *statusCode = error.userInfo[SDWebImageErrorDownloadStatusCodeKey];
|
|
258
|
+
if (statusCode) {
|
|
259
|
+
return [NSString stringWithFormat: @"%@, status code: %@", error.localizedDescription, statusCode];
|
|
260
|
+
}
|
|
261
|
+
return error.localizedDescription ?: @"Failed to load the image";
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
- (void) sendOnError: (nullable NSString*)message {
|
|
265
|
+
self.hasErrored = YES;
|
|
266
|
+
self.onErrorEvent = @{ @"error": message ?: @"Failed to load the image" };
|
|
267
|
+
if (self.onFastImageError) {
|
|
268
|
+
self.onFastImageError(self.onErrorEvent);
|
|
269
|
+
}
|
|
90
270
|
}
|
|
91
271
|
|
|
92
272
|
- (void) sendOnLoad: (UIImage*)image {
|
|
273
|
+
// The full image's size, also when it was decoded smaller.
|
|
274
|
+
CGSize size = [FFFDownsampledImage sourceSizeOfImage: image];
|
|
275
|
+
if (CGSizeEqualToSize(size, CGSizeZero)) {
|
|
276
|
+
size = image.size;
|
|
277
|
+
}
|
|
93
278
|
self.onLoadEvent = @{
|
|
94
|
-
@"width": [NSNumber numberWithDouble:
|
|
95
|
-
@"height": [NSNumber numberWithDouble:
|
|
279
|
+
@"width": [NSNumber numberWithDouble: size.width],
|
|
280
|
+
@"height": [NSNumber numberWithDouble: size.height]
|
|
96
281
|
};
|
|
97
282
|
if (self.onFastImageLoad) {
|
|
98
283
|
self.onFastImageLoad(self.onLoadEvent);
|
|
@@ -106,6 +291,21 @@
|
|
|
106
291
|
}
|
|
107
292
|
}
|
|
108
293
|
|
|
294
|
+
- (void) setRecyclingKey: (NSString*)recyclingKey {
|
|
295
|
+
if (_recyclingKey == recyclingKey || [_recyclingKey isEqualToString: recyclingKey]) {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
BOOL changed = _recyclingKey != nil;
|
|
299
|
+
_recyclingKey = [recyclingKey copy];
|
|
300
|
+
if (changed) {
|
|
301
|
+
// The view shows other content now: don't keep the current image
|
|
302
|
+
// while the next one loads (reloadImage clears it), even if the
|
|
303
|
+
// source is the same.
|
|
304
|
+
self.showsLoadedImage = NO;
|
|
305
|
+
_needsReload = YES;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
109
309
|
- (void) setDefaultSource: (UIImage*)defaultSource {
|
|
110
310
|
if (_defaultSource != defaultSource) {
|
|
111
311
|
_defaultSource = defaultSource;
|
|
@@ -115,12 +315,101 @@
|
|
|
115
315
|
|
|
116
316
|
- (void) didSetProps: (NSArray<NSString*>*)changedProps {
|
|
117
317
|
if (_needsReload) {
|
|
318
|
+
// With downsample on, the image is decoded for the view's size, so
|
|
319
|
+
// a view that hasn't been laid out yet loads once it has. Props and
|
|
320
|
+
// layout are applied in the same update, so that's before the next
|
|
321
|
+
// frame (in layoutSubviews). A view that still has no size then
|
|
322
|
+
// (e.g. one sized from onLoad) loads at full size.
|
|
323
|
+
if ([self downsamples] && ![self hasSize]) {
|
|
324
|
+
if (!self.waitsForSize) {
|
|
325
|
+
self.waitsForSize = YES;
|
|
326
|
+
__weak typeof(self) weakSelf = self;
|
|
327
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
328
|
+
if (weakSelf.waitsForSize) {
|
|
329
|
+
[weakSelf reloadImage];
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
118
335
|
[self reloadImage];
|
|
336
|
+
} else {
|
|
337
|
+
[self reloadIfResized];
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Whether images are decoded at about the view's size (downsample).
|
|
342
|
+
// Not for `repeat`, which tiles the image at its own size, or SDWebImage
|
|
343
|
+
// before 5.19.
|
|
344
|
+
- (BOOL) downsamples {
|
|
345
|
+
return _downsample && _resizeMode != RCTResizeModeRepeat && [FFFDownsampledImage isSupported];
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Whether the view has been laid out with an area. One that's 0 wide or tall
|
|
349
|
+
// (e.g. sized from onLoad) has no size to decode the image for.
|
|
350
|
+
- (BOOL) hasSize {
|
|
351
|
+
return self.bounds.size.width > 0 && self.bounds.size.height > 0;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// The size in pixels to decode the image for, or zero for full size.
|
|
355
|
+
- (CGSize) decodeBox {
|
|
356
|
+
if (![self downsamples] || ![self hasSize]) {
|
|
357
|
+
return CGSizeZero;
|
|
119
358
|
}
|
|
359
|
+
CGFloat scale = self.window.screen.scale ?: [UIScreen mainScreen].scale;
|
|
360
|
+
CGSize size = self.bounds.size;
|
|
361
|
+
return CGSizeMake(ceil(size.width * scale), ceil(size.height * scale));
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Whether the image has to cover the box, rather than fit in it.
|
|
365
|
+
- (BOOL) decodeCovers {
|
|
366
|
+
return _resizeMode == RCTResizeModeCover || _resizeMode == RCTResizeModeStretch;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// The view grew (by more than a fifth, as React Native's Image reloads), or
|
|
370
|
+
// now needs a covering or full-size image: loads the image again for its
|
|
371
|
+
// size, keeping the current one until then. It comes from the disk cache
|
|
372
|
+
// (the downloaded file is kept there whatever size it's decoded at).
|
|
373
|
+
- (void) reloadIfResized {
|
|
374
|
+
if (!_source || _needsReload || self.hasErrored || CGSizeEqualToSize(self.decodedBox, CGSizeZero)) {
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
// Already at full size (it's no larger than the view it was decoded for).
|
|
378
|
+
UIImage* image = self.untintedImage ?: super.image;
|
|
379
|
+
if (self.hasCompleted && CGSizeEqualToSize(image.size, [FFFDownsampledImage sourceSizeOfImage: image])) {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
CGSize box = [self decodeBox];
|
|
383
|
+
BOOL cover = [self decodeCovers];
|
|
384
|
+
if ([self downsamples]) {
|
|
385
|
+
if (![self hasSize]) {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
BOOL grew = box.width > self.decodedBox.width * 1.2 || box.height > self.decodedBox.height * 1.2;
|
|
389
|
+
if (!grew && (self.decodedCover || !cover)) {
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
SDWebImageOptions options = [self loadOptions];
|
|
394
|
+
if (self.showsLoadedImage) {
|
|
395
|
+
options |= SDWebImageDelayPlaceholder;
|
|
396
|
+
}
|
|
397
|
+
// Without events once it has loaded: it's the same image. If it's still
|
|
398
|
+
// loading, it restarts for the new size, and sends its events as usual.
|
|
399
|
+
BOOL events = !self.hasCompleted;
|
|
400
|
+
[self downloadImage: _source options: options context: [self loadContext] events: events];
|
|
120
401
|
}
|
|
121
402
|
|
|
122
403
|
- (void) reloadImage {
|
|
123
404
|
_needsReload = NO;
|
|
405
|
+
self.waitsForSize = NO;
|
|
406
|
+
self.decodedBox = CGSizeZero;
|
|
407
|
+
// The previous load, if it's still running, is for a source the view no
|
|
408
|
+
// longer shows: cancel it (a new download would, but a data uri or no
|
|
409
|
+
// source doesn't start one), and ignore what it still sends (SDWebImage
|
|
410
|
+
// completes a cancelled load with an error; see downloadImage:).
|
|
411
|
+
self.loadCount++;
|
|
412
|
+
[self sd_cancelCurrentImageLoad];
|
|
124
413
|
|
|
125
414
|
if (_source) {
|
|
126
415
|
// Load base64 images.
|
|
@@ -134,6 +423,16 @@
|
|
|
134
423
|
}
|
|
135
424
|
// Use SDWebImage API to support external format like WebP images
|
|
136
425
|
UIImage* image = [UIImage sd_imageWithData: [NSData dataWithContentsOfURL: _source.url]];
|
|
426
|
+
if (!image) {
|
|
427
|
+
// Not decodable: fail like a remote image, showing defaultSource.
|
|
428
|
+
[self setImage: _defaultSource];
|
|
429
|
+
self.showsLoadedImage = NO;
|
|
430
|
+
[self sendOnError: @"Failed to decode the image"];
|
|
431
|
+
if (self.onFastImageLoadEnd) {
|
|
432
|
+
self.onFastImageLoadEnd(@{});
|
|
433
|
+
}
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
137
436
|
[self setImage: image];
|
|
138
437
|
if (self.onFastImageProgress) {
|
|
139
438
|
self.onFastImageProgress(@{
|
|
@@ -142,6 +441,7 @@
|
|
|
142
441
|
});
|
|
143
442
|
}
|
|
144
443
|
self.hasCompleted = YES;
|
|
444
|
+
self.showsLoadedImage = YES;
|
|
145
445
|
[self sendOnLoad: image];
|
|
146
446
|
|
|
147
447
|
if (self.onFastImageLoadEnd) {
|
|
@@ -150,41 +450,14 @@
|
|
|
150
450
|
return;
|
|
151
451
|
}
|
|
152
452
|
|
|
153
|
-
|
|
154
|
-
NSDictionary* headers = _source.headers;
|
|
155
|
-
SDWebImageDownloaderRequestModifier* requestModifier = [SDWebImageDownloaderRequestModifier requestModifierWithBlock: ^NSURLRequest* _Nullable (NSURLRequest* _Nonnull request) {
|
|
156
|
-
NSMutableURLRequest* mutableRequest = [request mutableCopy];
|
|
157
|
-
for (NSString* header in headers) {
|
|
158
|
-
NSString* value = headers[header];
|
|
159
|
-
[mutableRequest setValue: value forHTTPHeaderField: header];
|
|
160
|
-
}
|
|
161
|
-
return [mutableRequest copy];
|
|
162
|
-
}];
|
|
163
|
-
SDWebImageContext* context = @{SDWebImageContextDownloadRequestModifier: requestModifier};
|
|
164
|
-
|
|
165
|
-
// Set priority.
|
|
166
|
-
SDWebImageOptions options = SDWebImageRetryFailed | SDWebImageHandleCookies;
|
|
167
|
-
switch (_source.priority) {
|
|
168
|
-
case FFFPriorityLow:
|
|
169
|
-
options |= SDWebImageLowPriority;
|
|
170
|
-
break;
|
|
171
|
-
case FFFPriorityNormal:
|
|
172
|
-
// Priority is normal by default.
|
|
173
|
-
break;
|
|
174
|
-
case FFFPriorityHigh:
|
|
175
|
-
options |= SDWebImageHighPriority;
|
|
176
|
-
break;
|
|
177
|
-
}
|
|
453
|
+
SDWebImageOptions options = [self loadOptions];
|
|
178
454
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
break;
|
|
186
|
-
case FFFCacheControlImmutable:
|
|
187
|
-
break;
|
|
455
|
+
// Keep showing the loaded image until the new one has loaded, instead
|
|
456
|
+
// of clearing it to defaultSource (or nothing) while it loads, which
|
|
457
|
+
// flashed (#747). defaultSource shows if the new image fails. As React
|
|
458
|
+
// Native's Image does; a `key` that changes starts from blank instead.
|
|
459
|
+
if (self.showsLoadedImage) {
|
|
460
|
+
options |= SDWebImageDelayPlaceholder;
|
|
188
461
|
}
|
|
189
462
|
|
|
190
463
|
if (self.onFastImageLoadStart) {
|
|
@@ -195,40 +468,164 @@
|
|
|
195
468
|
}
|
|
196
469
|
self.hasCompleted = NO;
|
|
197
470
|
self.hasErrored = NO;
|
|
471
|
+
self.retriedAfterBackground = NO;
|
|
472
|
+
[self stopWaitingForActive];
|
|
198
473
|
|
|
199
|
-
[self downloadImage: _source options: options context:
|
|
474
|
+
[self downloadImage: _source options: options context: [self loadContext] events: YES];
|
|
200
475
|
} else if (_defaultSource) {
|
|
201
476
|
[self setImage: _defaultSource];
|
|
477
|
+
self.showsLoadedImage = NO;
|
|
202
478
|
}
|
|
203
479
|
}
|
|
204
480
|
|
|
205
|
-
- (
|
|
481
|
+
- (SDWebImageOptions) loadOptions {
|
|
482
|
+
SDWebImageOptions options = SDWebImageRetryFailed | SDWebImageHandleCookies;
|
|
483
|
+
switch (_source.priority) {
|
|
484
|
+
case FFFPriorityLow:
|
|
485
|
+
options |= SDWebImageLowPriority;
|
|
486
|
+
break;
|
|
487
|
+
case FFFPriorityNormal:
|
|
488
|
+
// Priority is normal by default.
|
|
489
|
+
break;
|
|
490
|
+
case FFFPriorityHigh:
|
|
491
|
+
options |= SDWebImageHighPriority;
|
|
492
|
+
break;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
switch (_source.cacheControl) {
|
|
496
|
+
case FFFCacheControlWeb:
|
|
497
|
+
options |= SDWebImageRefreshCached;
|
|
498
|
+
break;
|
|
499
|
+
case FFFCacheControlCacheOnly:
|
|
500
|
+
options |= SDWebImageFromCacheOnly;
|
|
501
|
+
break;
|
|
502
|
+
case FFFCacheControlImmutable:
|
|
503
|
+
break;
|
|
504
|
+
}
|
|
505
|
+
return options;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Headers, and the size to decode at (see FFFDownsampledImage), which it
|
|
509
|
+
// records as decodedBox.
|
|
510
|
+
- (SDWebImageContext*) loadContext {
|
|
511
|
+
SDWebImageMutableContext* context = [NSMutableDictionary dictionary];
|
|
512
|
+
context[SDWebImageContextDownloadRequestModifier] = _source.requestModifier;
|
|
513
|
+
CGSize box = [self decodeBox];
|
|
514
|
+
self.decodedBox = box;
|
|
515
|
+
self.decodedCover = [self decodeCovers];
|
|
516
|
+
if (CGSizeEqualToSize(box, CGSizeZero)) {
|
|
517
|
+
context[SDWebImageContextAnimatedImageClass] = [SDAnimatedImage class];
|
|
518
|
+
return context;
|
|
519
|
+
}
|
|
520
|
+
[FFFDownsampledImage addToContext: context forURL: _source.url box: box cover: self.decodedCover];
|
|
521
|
+
return context;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// events: NO for a quiet reload at a new size (see reloadIfResized), which
|
|
525
|
+
// sends no events and keeps the current image if it fails.
|
|
526
|
+
- (void) downloadImage: (FFFastImageSource*)source options: (SDWebImageOptions)options context: (SDWebImageContext*)context events: (BOOL)events {
|
|
206
527
|
__weak typeof(self) weakSelf = self; // Always use a weak reference to self in blocks
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
528
|
+
NSUInteger load = ++self.loadCount;
|
|
529
|
+
// Most images have no onProgress, so only ask SDWebImage for progress when
|
|
530
|
+
// there's a handler as the load starts. A handler added while loading is
|
|
531
|
+
// used from the next load.
|
|
532
|
+
SDImageLoaderProgressBlock progress = nil;
|
|
533
|
+
if (events && self.onFastImageProgress) {
|
|
534
|
+
progress = ^(NSInteger receivedSize, NSInteger expectedSize, NSURL* _Nullable targetURL) {
|
|
535
|
+
// Without a Content-Length the total is unknown (-1 or 0), and a
|
|
536
|
+
// percentage can't be worked out from it, so don't send those.
|
|
537
|
+
if (expectedSize <= 0) {
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
// SDWebImage calls this on its download queue, while React Native
|
|
541
|
+
// sets onFastImageProgress (and deallocates the view) on the main
|
|
542
|
+
// queue. Read and call it there, so it can't change or be released
|
|
543
|
+
// in between (EXC_BAD_ACCESS).
|
|
544
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
545
|
+
RCTDirectEventBlock onProgress = weakSelf.onFastImageProgress;
|
|
546
|
+
if (onProgress) {
|
|
547
|
+
onProgress(@{
|
|
548
|
+
@"loaded": @(receivedSize),
|
|
549
|
+
@"total": @(expectedSize)
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
CFTimeInterval startedAt = CACurrentMediaTime();
|
|
556
|
+
NSURL* url = context[SDWebImageContextImageThumbnailPixelSize] ? [FFFDownsampledImage loadURLForURL: source.url] : source.url;
|
|
557
|
+
if (!events) {
|
|
558
|
+
// Only this load's image: SDWebImage would clear the view (to the
|
|
559
|
+
// placeholder) if it fails, and a cancelled load can still complete.
|
|
560
|
+
SDSetImageBlock setImage = ^(UIImage* _Nullable image, NSData* _Nullable data, SDImageCacheType cacheType, NSURL* _Nullable imageURL) {
|
|
561
|
+
if (image && weakSelf.loadCount == load) {
|
|
562
|
+
weakSelf.image = image;
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
[self sd_internalSetImageWithURL: url
|
|
566
|
+
placeholderImage: nil
|
|
567
|
+
options: options
|
|
568
|
+
context: context
|
|
569
|
+
setImageBlock: setImage
|
|
570
|
+
progress: nil
|
|
571
|
+
completed: ^(UIImage* _Nullable image, NSData* _Nullable data, NSError* _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL* _Nullable imageURL) {
|
|
572
|
+
if (error && weakSelf.loadCount == load) {
|
|
573
|
+
// Keeps the image it has, without trying again on every
|
|
574
|
+
// layout (e.g. while offline).
|
|
575
|
+
weakSelf.decodedBox = CGSizeZero;
|
|
576
|
+
}
|
|
577
|
+
}];
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
// Like SDAnimatedImageView's sd_setImageWithURL, which sets the image
|
|
581
|
+
// class to SDAnimatedImage (loadContext sets it). Only while this is the
|
|
582
|
+
// current load: SDWebImage sets the placeholder when a load it cancelled
|
|
583
|
+
// completes, which could replace a newer image.
|
|
584
|
+
SDSetImageBlock setImage = ^(UIImage* _Nullable image, NSData* _Nullable data, SDImageCacheType cacheType, NSURL* _Nullable imageURL) {
|
|
585
|
+
if (weakSelf.loadCount == load) {
|
|
586
|
+
weakSelf.image = image;
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
[self sd_internalSetImageWithURL: url
|
|
590
|
+
placeholderImage: _defaultSource
|
|
591
|
+
options: options
|
|
592
|
+
context: context
|
|
593
|
+
setImageBlock: setImage
|
|
594
|
+
progress: progress
|
|
595
|
+
completed: ^(UIImage* _Nullable image,
|
|
596
|
+
NSData* _Nullable data,
|
|
219
597
|
NSError* _Nullable error,
|
|
220
598
|
SDImageCacheType cacheType,
|
|
599
|
+
BOOL finished,
|
|
221
600
|
NSURL* _Nullable imageURL) {
|
|
601
|
+
// Replaced by another load (a new source, or the same one
|
|
602
|
+
// restarted for a new size), which sends the events. This
|
|
603
|
+
// one was cancelled, which SDWebImage reports as an error.
|
|
604
|
+
if (weakSelf.loadCount != load) {
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
// The download was running when the app went to the
|
|
608
|
+
// background, and failed: iOS suspends it there, and its
|
|
609
|
+
// timeout keeps counting, so it times out as the app comes
|
|
610
|
+
// back (NSURLErrorTimedOut), or loses its connection. Load it
|
|
611
|
+
// again once the app is active, as Android does (#758).
|
|
612
|
+
if (error && [error.domain isEqualToString: NSURLErrorDomain] &&
|
|
613
|
+
FFFEnteredBackgroundAt > startedAt && !weakSelf.retriedAfterBackground &&
|
|
614
|
+
weakSelf.source == source) {
|
|
615
|
+
weakSelf.retriedAfterBackground = YES;
|
|
616
|
+
[weakSelf downloadWhenActive: source options: options context: context];
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
222
619
|
if (error) {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
}
|
|
620
|
+
// SDWebImage shows the placeholder (defaultSource or nothing).
|
|
621
|
+
weakSelf.showsLoadedImage = NO;
|
|
622
|
+
[weakSelf sendOnError: FFFErrorMessage(error)];
|
|
227
623
|
if (weakSelf.onFastImageLoadEnd) {
|
|
228
624
|
weakSelf.onFastImageLoadEnd(@{});
|
|
229
625
|
}
|
|
230
626
|
} else {
|
|
231
627
|
weakSelf.hasCompleted = YES;
|
|
628
|
+
weakSelf.showsLoadedImage = YES;
|
|
232
629
|
[weakSelf sendOnLoad: image];
|
|
233
630
|
if (weakSelf.onFastImageLoadEnd) {
|
|
234
631
|
weakSelf.onFastImageLoadEnd(@{});
|
|
@@ -237,7 +634,37 @@
|
|
|
237
634
|
}];
|
|
238
635
|
}
|
|
239
636
|
|
|
637
|
+
// Downloads the source now if the app is active, or once it is (unless
|
|
638
|
+
// another load started meanwhile).
|
|
639
|
+
- (void) downloadWhenActive: (FFFastImageSource*)source options: (SDWebImageOptions)options context: (SDWebImageContext*)context {
|
|
640
|
+
// nil in app extensions, which don't get these notifications.
|
|
641
|
+
UIApplication* application = RCTSharedApplication();
|
|
642
|
+
if (!application || application.applicationState == UIApplicationStateActive) {
|
|
643
|
+
[self downloadImage: source options: options context: context events: YES];
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
[self stopWaitingForActive];
|
|
647
|
+
__weak typeof(self) weakSelf = self;
|
|
648
|
+
self.activeObserver = [[NSNotificationCenter defaultCenter] addObserverForName: UIApplicationDidBecomeActiveNotification
|
|
649
|
+
object: nil
|
|
650
|
+
queue: [NSOperationQueue mainQueue]
|
|
651
|
+
usingBlock: ^(NSNotification* notification) {
|
|
652
|
+
[weakSelf stopWaitingForActive];
|
|
653
|
+
if (weakSelf.source == source) {
|
|
654
|
+
[weakSelf downloadImage: source options: options context: context events: YES];
|
|
655
|
+
}
|
|
656
|
+
}];
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
- (void) stopWaitingForActive {
|
|
660
|
+
if (self.activeObserver) {
|
|
661
|
+
[[NSNotificationCenter defaultCenter] removeObserver: self.activeObserver];
|
|
662
|
+
self.activeObserver = nil;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
240
666
|
- (void) dealloc {
|
|
667
|
+
[self stopWaitingForActive];
|
|
241
668
|
[self sd_cancelCurrentImageLoad];
|
|
242
669
|
}
|
|
243
670
|
|