react-native-fast-image 8.20.0 → 8.22.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.
@@ -5,6 +5,7 @@
5
5
  #import <SDWebImage/SDWebImageError.h>
6
6
  #import <SDWebImage/SDImageTransformer.h>
7
7
  #import "FFFDownsampledImage.h"
8
+ #import <Accelerate/Accelerate.h>
8
9
 
9
10
  @interface FFFastImageView ()
10
11
 
@@ -16,9 +17,20 @@
16
17
 
17
18
  @property(nonatomic, strong) NSDictionary* onLoadEvent;
18
19
  @property(nonatomic, strong) NSDictionary* onErrorEvent;
19
- // The image before tinting, kept while a tint is applied so the tint can be
20
- // changed or removed. nil when there's no tint (super.image is untinted).
21
- @property(nonatomic, strong) UIImage* untintedImage;
20
+ // The image before tinting and blurring, kept while either is applied so they
21
+ // can be changed or removed. nil when neither is (super.image is the image).
22
+ @property(nonatomic, strong) UIImage* originalImage;
23
+ // Counts images set, so a blur that finishes can tell if it's still current.
24
+ @property(nonatomic, assign) NSUInteger imageCount;
25
+ // The view's size when the image showing was blurred.
26
+ @property(nonatomic, assign) CGSize blurredForSize;
27
+ // The blurred image showing (untinted), so a tint change can re-show it.
28
+ @property(nonatomic, strong) UIImage* blurredImage;
29
+ // The image being blurred fades in once it's blurred (transition).
30
+ @property(nonatomic, assign) BOOL fadesBlurredImage;
31
+ // The image being blurred is the first loaded one the view shows: until it's
32
+ // blurred, the view shows no loaded image (nothing, or defaultSource).
33
+ @property(nonatomic, assign) BOOL blursOverNothing;
22
34
  // Whether the current load was already restarted after the app came back
23
35
  // from the background (see downloadImage:).
24
36
  @property(nonatomic, assign) BOOL retriedAfterBackground;
@@ -41,6 +53,8 @@
41
53
  // When the app last went to the background (CACurrentMediaTime), or 0.
42
54
  static CFTimeInterval FFFEnteredBackgroundAt = 0;
43
55
 
56
+ static UIImage* FFFBlurredImage(UIImage* image, CGFloat scale, CGFloat radius, BOOL downscale);
57
+
44
58
  @implementation FFFastImageView
45
59
 
46
60
  + (void) initialize {
@@ -108,6 +122,77 @@ static CFTimeInterval FFFEnteredBackgroundAt = 0;
108
122
  }
109
123
  }
110
124
 
125
+ // Whether a loaded image fades in (the `transition` prop). Over a loaded
126
+ // image (a new source) only with betweenImages: otherwise it replaces it at
127
+ // once, as Android's image libraries fade an image in from nothing and never
128
+ // between images. Then as SDWebImage's transitions and those libraries
129
+ // decide: from the memory cache it shows at once (skipOnCacheHit 'memory',
130
+ // the default), and from the disk cache too with 'all'. Downloads, local
131
+ // files and bundled images fade.
132
+ - (BOOL) fadesImageFromCache: (SDImageCacheType)cacheType {
133
+ if (self.transitionDuration <= 0 || ([self displaysLoadedImage] && !self.transitionBetweenImages)) {
134
+ return NO;
135
+ }
136
+ NSString* skip = self.transitionSkipOnCacheHit;
137
+ if ([skip isEqualToString: @"none"]) {
138
+ return YES;
139
+ }
140
+ if (cacheType == SDImageCacheTypeMemory) {
141
+ return NO;
142
+ }
143
+ // SDWebImage also keeps local files and bundled images in its disk cache,
144
+ // so with 'all' they usually only fade the first time.
145
+ return !(cacheType == SDImageCacheTypeDisk && [skip isEqualToString: @"all"]);
146
+ }
147
+
148
+ // Whether the view shows a loaded image now: a loaded image that's still
149
+ // being blurred isn't showing yet.
150
+ - (BOOL) displaysLoadedImage {
151
+ return self.showsLoadedImage && !self.blursOverNothing;
152
+ }
153
+
154
+ // Shows a loaded image, fading it in over what the view shows if `fade`. A
155
+ // blurred image shows, and fades in, once it's blurred.
156
+ - (void) showLoadedImage: (UIImage*)image fade: (BOOL)fade {
157
+ if ([self blurs: image]) {
158
+ self.blursOverNothing = ![self displaysLoadedImage];
159
+ self.fadesBlurredImage = fade;
160
+ self.image = image;
161
+ return;
162
+ }
163
+ self.blursOverNothing = NO;
164
+ self.fadesBlurredImage = NO;
165
+ if (fade) {
166
+ [self fadeIn: ^{
167
+ self.image = image;
168
+ }];
169
+ } else {
170
+ self.image = image;
171
+ }
172
+ }
173
+
174
+ // Makes the change (showing another image), fading it in over what the view
175
+ // showed.
176
+ - (void) fadeIn: (void (^)(void))change {
177
+ if (!super.image || !self.layer.presentationLayer) {
178
+ // Nothing to cross-dissolve from: the view shows no image, or isn't
179
+ // on screen yet (e.g. a new view whose image loads as it mounts),
180
+ // where a cross-dissolve has nothing drawn to start from and shows
181
+ // the image at once. Fade the view in instead.
182
+ change();
183
+ CABasicAnimation* fadeIn = [CABasicAnimation animationWithKeyPath: @"opacity"];
184
+ fadeIn.fromValue = @0;
185
+ fadeIn.duration = self.transitionDuration / 1000;
186
+ [self.layer addAnimation: fadeIn forKey: @"FFFFadeIn"];
187
+ return;
188
+ }
189
+ [UIView transitionWithView: self
190
+ duration: self.transitionDuration / 1000
191
+ options: UIViewAnimationOptionTransitionCrossDissolve | UIViewAnimationOptionAllowUserInteraction
192
+ animations: change
193
+ completion: nil];
194
+ }
195
+
111
196
  - (void) setPaused: (BOOL)paused {
112
197
  if (_paused == paused) {
113
198
  return;
@@ -128,6 +213,10 @@ static CFTimeInterval FFFEnteredBackgroundAt = 0;
128
213
  if (_resizeMode != resizeMode) {
129
214
  _resizeMode = resizeMode;
130
215
  [self updateContentMode];
216
+ // The blur is made for the size the image is shown at.
217
+ if ([self blurs: self.originalImage]) {
218
+ [self setImage: self.originalImage];
219
+ }
131
220
  }
132
221
  }
133
222
 
@@ -153,6 +242,7 @@ static CFTimeInterval FFFEnteredBackgroundAt = 0;
153
242
  - (void) layoutSubviews {
154
243
  [super layoutSubviews];
155
244
  [self updateContentMode];
245
+ [self blurAgainIfResized];
156
246
  if (self.waitsForSize) {
157
247
  if ([self hasSize]) {
158
248
  [self reloadImage];
@@ -198,8 +288,16 @@ static CFTimeInterval FFFEnteredBackgroundAt = 0;
198
288
 
199
289
  - (void) setImageColor: (UIColor*)imageColor {
200
290
  _imageColor = imageColor;
201
- // Re-apply to the untinted image, so the tint can change or be removed.
202
- UIImage* image = self.untintedImage ?: super.image;
291
+ // A blurred image is a still image: show it with the new tint, without
292
+ // blurring it again.
293
+ if ([self blurs: self.originalImage]) {
294
+ if (self.blurredImage) {
295
+ [self showImage: self.blurredImage];
296
+ }
297
+ return;
298
+ }
299
+ // Re-apply to the original image, so the tint can change or be removed.
300
+ UIImage* image = self.originalImage ?: super.image;
203
301
  if (image) {
204
302
  // SDAnimatedImageView ignores the image it already shows, which would
205
303
  // keep an animated image's frames as they were tinted.
@@ -215,14 +313,36 @@ static CFTimeInterval FFFEnteredBackgroundAt = 0;
215
313
  }
216
314
 
217
315
  - (void) setImage: (UIImage*)image {
218
- // Tinted as React Native's Image does it: UIKit draws a template image in
219
- // the view's tintColor, so no tinted copy of the image is made.
220
- // SDAnimatedImageView draws the frames of an animated image itself,
221
- // without the tint, so each frame is tinted as it's decoded (source-in,
222
- // as the template is), or, before SDWebImage 5.20, the first frame shows.
316
+ self.imageCount++;
317
+ if ([self blurs: image]) {
318
+ self.originalImage = image;
319
+ self.blurredImage = nil;
320
+ [self blurImage: image];
321
+ return;
322
+ }
323
+ self.originalImage = image && self.imageColor ? image : nil;
324
+ self.blurredImage = nil;
325
+ self.blursOverNothing = NO;
326
+ // The blur was removed before it was ready: the fade it was waiting for
327
+ // happens now.
328
+ if (self.fadesBlurredImage) {
329
+ self.fadesBlurredImage = NO;
330
+ [self fadeIn: ^{
331
+ [self showImage: image];
332
+ }];
333
+ } else {
334
+ [self showImage: image];
335
+ }
336
+ }
337
+
338
+ // Shows the image, tinted as React Native's Image does it: UIKit draws a
339
+ // template image in the view's tintColor, so no tinted copy of the image is
340
+ // made. SDAnimatedImageView draws the frames of an animated image itself,
341
+ // without the tint, so each frame is tinted as it's decoded (source-in, as the
342
+ // template is), or, before SDWebImage 5.20, the first frame shows.
343
+ - (void) showImage: (UIImage*)image {
223
344
  UIColor* tint = image ? self.imageColor : nil;
224
345
  BOOL animated = [image conformsToProtocol: @protocol(SDAnimatedImage)] && [(id<SDAnimatedImage>)image animatedImageFrameCount] > 1;
225
- self.untintedImage = tint ? image : nil;
226
346
  self.tintColor = tint;
227
347
  if ([self tintsFrames]) {
228
348
  [self setValue: tint && animated ? [SDImageTintTransformer transformerWithColor: tint] : nil forKey: @"animationTransformer"];
@@ -247,6 +367,187 @@ static CFTimeInterval FFFEnteredBackgroundAt = 0;
247
367
  [self updateContentMode];
248
368
  }
249
369
 
370
+ - (void) setBlurRadius: (CGFloat)blurRadius {
371
+ if (_blurRadius == blurRadius) {
372
+ return;
373
+ }
374
+ _blurRadius = blurRadius;
375
+ // Re-apply to the original image, so the blur can change or be removed.
376
+ UIImage* image = self.originalImage ?: super.image;
377
+ if (image) {
378
+ [self setImage: image];
379
+ }
380
+ }
381
+
382
+ // Whether the image is blurred: a loaded image, not defaultSource (as with
383
+ // React Native's Image, and on Android).
384
+ - (BOOL) blurs: (UIImage*)image {
385
+ return _blurRadius > 0 && image && image != _defaultSource;
386
+ }
387
+
388
+ // Blurs the image off the main thread, and shows it (tinted as any image) unless
389
+ // another image was set meanwhile. Until then the view keeps showing what it
390
+ // showed: never the image without the blur. An animated image shows its first
391
+ // frame.
392
+ - (void) blurImage: (UIImage*)image {
393
+ // Not laid out yet (an image from the memory cache is set before the
394
+ // view's first layout): it's blurred for its size once it has one
395
+ // (blurAgainIfResized), instead of twice.
396
+ if (![self hasSize]) {
397
+ self.blurredForSize = CGSizeZero;
398
+ return;
399
+ }
400
+ CGFloat screenScale = self.window.screen.scale ?: [UIScreen mainScreen].scale;
401
+ // How many pixels on screen a pixel of the image takes.
402
+ CGFloat shownScale = [self shownScale: image] * screenScale / image.scale;
403
+ // Blurred at the size it's shown at (a blurred image needs no more
404
+ // detail), or at its own size if it's shown larger.
405
+ CGFloat scale = MIN(shownScale, 1);
406
+ // The radius in pixels of the image that's blurred.
407
+ CGFloat radius = _blurRadius * screenScale * scale / shownScale;
408
+ // Pixelated images are drawn larger without smoothing, so a smaller copy
409
+ // would show its pixels.
410
+ BOOL downscale = ![_imageRendering isEqualToString: @"pixelated"];
411
+ NSUInteger count = self.imageCount;
412
+ self.blurredForSize = self.bounds.size;
413
+ __weak typeof(self) weakSelf = self;
414
+ dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
415
+ UIImage* blurred = FFFBlurredImage(image, scale, radius, downscale);
416
+ dispatch_async(dispatch_get_main_queue(), ^{
417
+ [weakSelf showBlurredImage: blurred count: count];
418
+ });
419
+ });
420
+ }
421
+
422
+ - (void) showBlurredImage: (UIImage*)image count: (NSUInteger)count {
423
+ if (self.imageCount == count) {
424
+ self.blurredImage = image;
425
+ self.blursOverNothing = NO;
426
+ if (self.fadesBlurredImage) {
427
+ self.fadesBlurredImage = NO;
428
+ [self fadeIn: ^{
429
+ [self showImage: image];
430
+ }];
431
+ } else {
432
+ [self showImage: image];
433
+ }
434
+ }
435
+ }
436
+
437
+ // Points on screen per point of the image, for the view's size and
438
+ // resizeMode, or 1 before the view has a size.
439
+ - (CGFloat) shownScale: (UIImage*)image {
440
+ CGSize size = image.size;
441
+ CGSize view = self.bounds.size;
442
+ if (size.width <= 0 || size.height <= 0 || view.width <= 0 || view.height <= 0) {
443
+ return 1;
444
+ }
445
+ CGFloat x = view.width / size.width;
446
+ CGFloat y = view.height / size.height;
447
+ switch (_resizeMode) {
448
+ case RCTResizeModeCover:
449
+ return MAX(x, y);
450
+ case RCTResizeModeContain:
451
+ return MIN(x, y);
452
+ case RCTResizeModeStretch:
453
+ return sqrt(x * y);
454
+ case RCTResizeModeCenter:
455
+ return MIN(MIN(x, y), 1);
456
+ default:
457
+ return 1;
458
+ }
459
+ }
460
+
461
+ // The view's size changed a lot (by more than a fifth either way) since the
462
+ // image was blurred, which was for the size it was shown at then: blur it
463
+ // again for the new size.
464
+ - (void) blurAgainIfResized {
465
+ if (![self blurs: self.originalImage] || ![self hasSize]) {
466
+ return;
467
+ }
468
+ CGSize view = self.bounds.size;
469
+ CGSize blurred = self.blurredForSize;
470
+ if (blurred.width > 0 && blurred.height > 0 &&
471
+ view.width <= blurred.width * 1.2 && blurred.width <= view.width * 1.2 &&
472
+ view.height <= blurred.height * 1.2 && blurred.height <= view.height * 1.2) {
473
+ return;
474
+ }
475
+ [self setImage: self.originalImage];
476
+ }
477
+
478
+ // The image (its first frame, for an animated image) drawn at scale times its
479
+ // size in pixels, and blurred as React Native's Image blurs on iOS: three box
480
+ // blurs, close to a Gaussian blur, with the box size worked out from the
481
+ // radius (in pixels) as the SVG spec describes and halved, as React Native
482
+ // does. A large box blurs a copy 2, 4 or 8 times smaller instead, with a box
483
+ // that much smaller, of at least 11 pixels: away from the edges that matches
484
+ // blurring at full size within a few levels of 255 (about as much as a
485
+ // slightly different radius changes it), for a quarter to a sixty-fourth of
486
+ // the work. It keeps the image's size in points.
487
+ static UIImage* FFFBlurredImage(UIImage* image, CGFloat scale, CGFloat radius, BOOL downscale)
488
+ {
489
+ if (image.size.width <= 0 || image.size.height <= 0) {
490
+ return image;
491
+ }
492
+ double fullWidth = MAX(1, round(image.size.width * image.scale * scale));
493
+ double fullHeight = MAX(1, round(image.size.height * image.scale * scale));
494
+ // At most the image's larger side (a larger box looks the same, and an
495
+ // absurd radius would overflow).
496
+ double box = MIN(floor((radius * 3 * sqrt(2 * M_PI) / 4 + 0.5) / 2), MAX(fullWidth, fullHeight));
497
+ double factor = !downscale ? 1 : box >= 88 ? 8 : box >= 44 ? 4 : box >= 22 ? 2 : 1;
498
+ size_t width = MAX(1, (size_t) round(fullWidth / factor));
499
+ size_t height = MAX(1, (size_t) round(fullHeight / factor));
500
+ // Odd, so it's centered.
501
+ uint32_t boxSize = factor == 1 ? (uint32_t) box | 1 : (uint32_t) round((box / factor - 1) / 2) * 2 + 1;
502
+ CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
503
+ CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, 0, colorSpace,
504
+ kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Host);
505
+ CGColorSpaceRelease(colorSpace);
506
+ if (!context) {
507
+ return image;
508
+ }
509
+ // Drawn as UIKit draws (top-left origin), so it follows the image's
510
+ // orientation.
511
+ CGContextTranslateCTM(context, 0, height);
512
+ CGContextScaleCTM(context, 1, -1);
513
+ CGContextSetInterpolationQuality(context, kCGInterpolationHigh);
514
+ UIGraphicsPushContext(context);
515
+ [image drawInRect: CGRectMake(0, 0, width, height)];
516
+ UIGraphicsPopContext();
517
+
518
+ if (boxSize > 1) {
519
+ vImage_Buffer buffer = {
520
+ .data = CGBitmapContextGetData(context),
521
+ .height = height,
522
+ .width = width,
523
+ .rowBytes = CGBitmapContextGetBytesPerRow(context),
524
+ };
525
+ vImage_Buffer other = buffer;
526
+ other.data = malloc(buffer.rowBytes * height);
527
+ vImage_Error tempSize = vImageBoxConvolve_ARGB8888(&buffer, &other, NULL, 0, 0, boxSize, boxSize, NULL,
528
+ kvImageGetTempBufferSize | kvImageEdgeExtend);
529
+ void* temp = tempSize > 0 ? malloc(tempSize) : NULL;
530
+ if (other.data && temp) {
531
+ vImageBoxConvolve_ARGB8888(&buffer, &other, temp, 0, 0, boxSize, boxSize, NULL, kvImageEdgeExtend);
532
+ vImageBoxConvolve_ARGB8888(&other, &buffer, temp, 0, 0, boxSize, boxSize, NULL, kvImageEdgeExtend);
533
+ vImageBoxConvolve_ARGB8888(&buffer, &other, temp, 0, 0, boxSize, boxSize, NULL, kvImageEdgeExtend);
534
+ memcpy(buffer.data, other.data, buffer.rowBytes * height);
535
+ }
536
+ free(other.data);
537
+ free(temp);
538
+ }
539
+
540
+ CGImageRef cgImage = CGBitmapContextCreateImage(context);
541
+ CGContextRelease(context);
542
+ if (!cgImage) {
543
+ return image;
544
+ }
545
+ // The same size in points, also when it's smaller.
546
+ UIImage* blurred = [UIImage imageWithCGImage: cgImage scale: width / image.size.width orientation: UIImageOrientationUp];
547
+ CGImageRelease(cgImage);
548
+ return blurred;
549
+ }
550
+
250
551
  // The error's description, with the HTTP status code when there is one (as on
251
552
  // Android): SDWebImage keeps the code out of the description. Also for preload
252
553
  // results.
@@ -306,8 +607,9 @@ NSString *FFFErrorMessage(NSError *error)
306
607
  if (changed) {
307
608
  // The view shows other content now: don't keep the current image
308
609
  // while the next one loads (reloadImage clears it), even if the
309
- // source is the same.
610
+ // source is the same, nor a fade from it.
310
611
  self.showsLoadedImage = NO;
612
+ [self.layer removeAllAnimations];
311
613
  _needsReload = YES;
312
614
  }
313
615
  }
@@ -381,7 +683,7 @@ NSString *FFFErrorMessage(NSError *error)
381
683
  return;
382
684
  }
383
685
  // Already at full size (it's no larger than the view it was decoded for).
384
- UIImage* image = self.untintedImage ?: super.image;
686
+ UIImage* image = self.originalImage ?: super.image;
385
687
  if (self.hasCompleted && CGSizeEqualToSize(image.size, [FFFDownsampledImage sourceSizeOfImage: image])) {
386
688
  return;
387
689
  }
@@ -439,7 +741,7 @@ NSString *FFFErrorMessage(NSError *error)
439
741
  }
440
742
  return;
441
743
  }
442
- [self setImage: image];
744
+ [self showLoadedImage: image fade: [self fadesImageFromCache: SDImageCacheTypeNone]];
443
745
  if (self.onFastImageProgress) {
444
746
  self.onFastImageProgress(@{
445
747
  @"loaded": @(1),
@@ -592,13 +894,18 @@ NSString *FFFErrorMessage(NSError *error)
592
894
  // class to SDAnimatedImage (loadContext sets it). Only while this is the
593
895
  // current load: SDWebImage sets the placeholder when a load it cancelled
594
896
  // completes, which could replace a newer image.
897
+ UIImage* placeholder = _defaultSource;
595
898
  SDSetImageBlock setImage = ^(UIImage* _Nullable image, NSData* _Nullable data, SDImageCacheType cacheType, NSURL* _Nullable imageURL) {
596
- if (weakSelf.loadCount == load) {
597
- weakSelf.image = image;
899
+ if (weakSelf.loadCount != load) {
900
+ return;
598
901
  }
902
+ // The loaded image, not defaultSource (shown while it loads, or if it
903
+ // fails).
904
+ BOOL loaded = image && image != placeholder;
905
+ [weakSelf showLoadedImage: image fade: loaded && [weakSelf fadesImageFromCache: cacheType]];
599
906
  };
600
907
  [self sd_internalSetImageWithURL: url
601
- placeholderImage: _defaultSource
908
+ placeholderImage: placeholder
602
909
  options: options
603
910
  context: context
604
911
  setImageBlock: setImage
@@ -107,7 +107,11 @@ RCT_EXPORT_VIEW_PROPERTY(recyclingKey, NSString)
107
107
  RCT_EXPORT_VIEW_PROPERTY(loopCount, NSInteger)
108
108
  RCT_EXPORT_VIEW_PROPERTY(imageRendering, NSString)
109
109
  RCT_EXPORT_VIEW_PROPERTY(paused, BOOL)
110
+ RCT_EXPORT_VIEW_PROPERTY(transitionDuration, double)
111
+ RCT_EXPORT_VIEW_PROPERTY(transitionBetweenImages, BOOL)
112
+ RCT_EXPORT_VIEW_PROPERTY(transitionSkipOnCacheHit, NSString)
110
113
  RCT_EXPORT_VIEW_PROPERTY(downsample, BOOL)
114
+ RCT_EXPORT_VIEW_PROPERTY(blurRadius, CGFloat)
111
115
  RCT_EXPORT_VIEW_PROPERTY(onFastImageLoadStart, RCTDirectEventBlock)
112
116
  RCT_EXPORT_VIEW_PROPERTY(onFastImageProgress, RCTDirectEventBlock)
113
117
  RCT_EXPORT_VIEW_PROPERTY(onFastImageError, RCTDirectEventBlock)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-fast-image",
3
- "version": "8.20.0",
3
+ "version": "8.22.0",
4
4
  "description": "🚩 FastImage, performant React Native image component.",
5
5
  "keywords": [
6
6
  "cache",