react-native-fast-image 8.25.0 → 8.27.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.
@@ -41,8 +41,17 @@
41
41
  // Whether the view shows an image that loaded (not defaultSource or nothing).
42
42
  // A new source then keeps it until the new image has loaded (see reloadImage).
43
43
  @property(nonatomic, assign) BOOL showsLoadedImage;
44
- // downsample: a load waits for the view's size (see didSetProps).
44
+ // downsample, or several sources: a load waits for the view's size (see
45
+ // didSetProps).
45
46
  @property(nonatomic, assign) BOOL waitsForSize;
47
+ // The `source` prop. With several `sources`, _source is the one picked for
48
+ // the view's size instead.
49
+ @property(nonatomic, strong) FFFastImageSource* propSource;
50
+ // Several sources: the next load switches to another one for a new view
51
+ // size, and the one after it is loading. It's the same picture at another
52
+ // size, so it doesn't fade in.
53
+ @property(nonatomic, assign) BOOL switchesSource;
54
+ @property(nonatomic, assign) BOOL loadsSwitchedSource;
46
55
  // The size (in pixels) the image showing or loading was decoded for, and
47
56
  // whether it covers it; zero for a full-size image.
48
57
  @property(nonatomic, assign) CGSize decodedBox;
@@ -132,7 +141,8 @@ static UIImage* FFFBlurredImage(UIImage* image, CGFloat scale, CGFloat radius, B
132
141
  // the default), and from the disk cache too with 'all'. Downloads, local
133
142
  // files and bundled images fade.
134
143
  - (BOOL) fadesImageFromCache: (SDImageCacheType)cacheType {
135
- if (self.transitionDuration <= 0 || ([self displaysLoadedImage] && !self.transitionBetweenImages)) {
144
+ if (self.transitionDuration <= 0 || self.loadsSwitchedSource ||
145
+ ([self displaysLoadedImage] && !self.transitionBetweenImages)) {
136
146
  return NO;
137
147
  }
138
148
  NSString* skip = self.transitionSkipOnCacheHit;
@@ -256,7 +266,7 @@ static UIImage* FFFBlurredImage(UIImage* image, CGFloat scale, CGFloat radius, B
256
266
  if ([self hasSize]) {
257
267
  [self reloadImage];
258
268
  }
259
- } else {
269
+ } else if (![self switchSourceIfResized]) {
260
270
  [self reloadIfResized];
261
271
  }
262
272
  }
@@ -345,6 +355,25 @@ static UIImage* FFFBlurredImage(UIImage* image, CGFloat scale, CGFloat radius, B
345
355
  }
346
356
  }
347
357
 
358
+ // A vector image drawn into a bitmap, with its size in points, at a scale
359
+ // that's sharp at the size the view shows it (or its own size before the view
360
+ // has one).
361
+ - (UIImage*) rasterizedImage: (UIImage*)image {
362
+ CGSize size = image.size;
363
+ if (size.width <= 0 || size.height <= 0) {
364
+ return image;
365
+ }
366
+ CGSize view = self.bounds.size;
367
+ CGFloat shown = MAX(1, MAX(view.width / size.width, view.height / size.height));
368
+ UIGraphicsImageRendererFormat* format = [UIGraphicsImageRendererFormat preferredFormat];
369
+ format.scale = (self.window.screen.scale ?: [UIScreen mainScreen].scale) * shown;
370
+ format.opaque = NO;
371
+ UIGraphicsImageRenderer* renderer = [[UIGraphicsImageRenderer alloc] initWithSize: size format: format];
372
+ return [renderer imageWithActions: ^(UIGraphicsImageRendererContext* context) {
373
+ [image drawInRect: CGRectMake(0, 0, size.width, size.height)];
374
+ }];
375
+ }
376
+
348
377
  // Shows the image, tinted as React Native's Image does it: UIKit draws a
349
378
  // template image in the view's tintColor, so no tinted copy of the image is
350
379
  // made. SDAnimatedImageView draws the frames of an animated image itself,
@@ -372,6 +401,11 @@ static UIImage* FFFBlurredImage(UIImage* image, CGFloat scale, CGFloat radius, B
372
401
  if (tint) {
373
402
  if (animated) {
374
403
  image = [UIImage imageWithCGImage: image.CGImage scale: image.scale orientation: image.imageOrientation];
404
+ } else if (!image.CGImage && !repeats) {
405
+ // A vector image (an SVG), which UIKit doesn't draw as a
406
+ // template: drawn into a bitmap at the size it's shown at
407
+ // (tiledImage: draws one for repeat).
408
+ image = [self rasterizedImage: image];
375
409
  }
376
410
  image = [image imageWithRenderingMode: UIImageRenderingModeAlwaysTemplate];
377
411
  }
@@ -389,6 +423,18 @@ static UIImage* FFFBlurredImage(UIImage* image, CGFloat scale, CGFloat radius, B
389
423
  // animated image's first frame, scaled down to fit the view if it's larger,
390
424
  // as React Native's Image does (and Android here).
391
425
  - (UIImage*) tiledImage: (UIImage*)image {
426
+ if (!image.CGImage && image.size.width > 0 && image.size.height > 0) {
427
+ // A vector image (an SVG): drawn at its own size in pixels (its width
428
+ // and height), as Android tiles it and as any other image's size is.
429
+ UIImage* vector = image;
430
+ UIGraphicsImageRendererFormat* format = [UIGraphicsImageRendererFormat preferredFormat];
431
+ format.scale = 1;
432
+ format.opaque = NO;
433
+ UIGraphicsImageRenderer* renderer = [[UIGraphicsImageRenderer alloc] initWithSize: vector.size format: format];
434
+ image = [[renderer imageWithActions: ^(UIGraphicsImageRendererContext* context) {
435
+ [vector drawInRect: CGRectMake(0, 0, vector.size.width, vector.size.height)];
436
+ }] imageWithRenderingMode: vector.renderingMode];
437
+ }
392
438
  CGImageRef cgImage = image.CGImage;
393
439
  if (!cgImage) {
394
440
  return image;
@@ -643,12 +689,59 @@ NSString *FFFErrorMessage(NSError *error)
643
689
  }
644
690
 
645
691
  - (void) setSource: (FFFastImageSource*)source {
646
- if (_source != source) {
647
- _source = source;
692
+ if (self.propSource != source) {
693
+ self.propSource = source;
694
+ if (![self picksSource]) {
695
+ _source = source;
696
+ }
648
697
  _needsReload = YES;
649
698
  }
650
699
  }
651
700
 
701
+ - (void) setSources: (NSArray<FFFastImageSource*>*)sources {
702
+ if (_sources != sources) {
703
+ _sources = [sources copy];
704
+ if (![self picksSource]) {
705
+ _source = self.propSource;
706
+ }
707
+ _needsReload = YES;
708
+ }
709
+ }
710
+
711
+ // Whether the view picks one of several sources for its size.
712
+ - (BOOL) picksSource {
713
+ return _sources.count > 1;
714
+ }
715
+
716
+ // Of several sources, the one whose size in pixels is closest to the view's
717
+ // (by pixel count), or the largest while the view has no size (e.g. one sized
718
+ // from onLoad).
719
+ - (FFFastImageSource*) sourceForSize {
720
+ CGFloat scale = self.window.screen.scale ?: [UIScreen mainScreen].scale;
721
+ CGFloat viewPixels = self.bounds.size.width * self.bounds.size.height * scale * scale;
722
+ FFFastImageSource* best = nil;
723
+ CGFloat bestFit = CGFLOAT_MAX;
724
+ for (FFFastImageSource* source in _sources) {
725
+ CGFloat fit = viewPixels > 0 ? ABS(1 - source.pixelCount / viewPixels) : -source.pixelCount;
726
+ if (!best || fit < bestFit) {
727
+ best = source;
728
+ bestFit = fit;
729
+ }
730
+ }
731
+ return best;
732
+ }
733
+
734
+ // Several sources: another one fits the view's new size better. Loads it,
735
+ // keeping the image showing until then, without a fade (see switchesSource).
736
+ - (BOOL) switchSourceIfResized {
737
+ if (![self picksSource] || _needsReload || ![self hasSize] || [self sourceForSize] == _source) {
738
+ return NO;
739
+ }
740
+ self.switchesSource = YES;
741
+ [self reloadImage];
742
+ return YES;
743
+ }
744
+
652
745
  - (void) setRecyclingKey: (NSString*)recyclingKey {
653
746
  if (_recyclingKey == recyclingKey || [_recyclingKey isEqualToString: recyclingKey]) {
654
747
  return;
@@ -674,12 +767,13 @@ NSString *FFFErrorMessage(NSError *error)
674
767
 
675
768
  - (void) didSetProps: (NSArray<NSString*>*)changedProps {
676
769
  if (_needsReload) {
677
- // With downsample on, the image is decoded for the view's size, so
678
- // a view that hasn't been laid out yet loads once it has. Props and
679
- // layout are applied in the same update, so that's before the next
680
- // frame (in layoutSubviews). A view that still has no size then
681
- // (e.g. one sized from onLoad) loads at full size.
682
- if ([self downsamples] && ![self hasSize]) {
770
+ // With downsample on, the image is decoded for the view's size, and
771
+ // with several sources one is picked for it, so a view that hasn't
772
+ // been laid out yet loads once it has. Props and layout are applied
773
+ // in the same update, so that's before the next frame (in
774
+ // layoutSubviews). A view that still has no size then (e.g. one sized
775
+ // from onLoad) loads at full size, or the largest source.
776
+ if (([self downsamples] || [self picksSource]) && ![self hasSize]) {
683
777
  if (!self.waitsForSize) {
684
778
  self.waitsForSize = YES;
685
779
  __weak typeof(self) weakSelf = self;
@@ -766,6 +860,11 @@ NSString *FFFErrorMessage(NSError *error)
766
860
  - (void) reloadImage {
767
861
  _needsReload = NO;
768
862
  self.waitsForSize = NO;
863
+ self.loadsSwitchedSource = self.switchesSource;
864
+ self.switchesSource = NO;
865
+ if ([self picksSource]) {
866
+ _source = [self sourceForSize];
867
+ }
769
868
  self.decodedBox = CGSizeZero;
770
869
  // The previous load, if it's still running, is for a source the view no
771
870
  // longer shows: cancel it (a new download would, but a data uri or no
@@ -1002,7 +1101,7 @@ NSString *FFFErrorMessage(NSError *error)
1002
1101
  if (error) {
1003
1102
  // SDWebImage shows the placeholder (defaultSource or nothing).
1004
1103
  weakSelf.showsLoadedImage = NO;
1005
- [weakSelf sendOnError: FFFErrorMessage(error)];
1104
+ [weakSelf sendOnError: [source errorMessage: error]];
1006
1105
  if (weakSelf.onFastImageLoadEnd) {
1007
1106
  weakSelf.onFastImageLoadEnd([weakSelf loadEndEvent: NO]);
1008
1107
  }
@@ -101,6 +101,7 @@ RCT_EXPORT_MODULE(FastImageView)
101
101
  }
102
102
 
103
103
  RCT_EXPORT_VIEW_PROPERTY(source, FFFastImageSource)
104
+ RCT_EXPORT_VIEW_PROPERTY(sources, FFFastImageSourceArray)
104
105
  RCT_EXPORT_VIEW_PROPERTY(defaultSource, UIImage)
105
106
  RCT_EXPORT_VIEW_PROPERTY(resizeMode, RCTResizeMode)
106
107
  RCT_EXPORT_VIEW_PROPERTY(recyclingKey, NSString)
@@ -292,7 +293,7 @@ RCT_EXPORT_METHOD(preload:(nonnull NSArray<FFFastImageSource *> *)sources
292
293
  FFFQueueLoad(source, options, [context copy], ^(UIImage *image, NSError *error) {
293
294
  results[idx] = image
294
295
  ? @{@"ok": @YES, @"width": @(image.size.width), @"height": @(image.size.height)}
295
- : FFFFailure(FFFErrorMessage(error));
296
+ : FFFFailure([source errorMessage:error]);
296
297
  finishOne();
297
298
  });
298
299
  }];
@@ -350,7 +351,7 @@ RCT_EXPORT_METHOD(getCachePath:(FFFastImageSource *)source
350
351
  context[SDWebImageContextStoreCacheType] = @(SDImageCacheTypeDisk);
351
352
  FFFQueueLoad(source, options, [context copy], ^(UIImage *image, NSError *error) {
352
353
  if (!image) {
353
- resolve(FFFFailure(FFFErrorMessage(error)));
354
+ resolve(FFFFailure([source errorMessage:error]));
354
355
  return;
355
356
  }
356
357
  FFFFindCachedFile(key, resolveFile, ^{
@@ -5,5 +5,6 @@
5
5
  @interface RCTConvert (FFFastImage)
6
6
 
7
7
  + (FFFastImageSource *)FFFastImageSource:(id)json;
8
+ + (NSArray<FFFastImageSource *> *)FFFastImageSourceArray:(id)json;
8
9
 
9
10
  @end
@@ -51,6 +51,8 @@ RCT_ENUM_CONVERTER(FFFCacheControl, (@{
51
51
  imageSource.cacheKey = cacheKey.length > 0 ? cacheKey : nil;
52
52
  id memoryCache = json[@"memoryCache"];
53
53
  imageSource.memoryCache = memoryCache && memoryCache != (id)kCFNull ? [self BOOL:memoryCache] : YES;
54
+ CGFloat scale = json[@"scale"] && json[@"scale"] != (id)kCFNull ? [self CGFloat:json[@"scale"]] : 1;
55
+ imageSource.pixelCount = [self CGFloat:json[@"width"]] * [self CGFloat:json[@"height"]] * scale * scale;
54
56
 
55
57
  return imageSource;
56
58
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-fast-image",
3
- "version": "8.25.0",
3
+ "version": "8.27.0",
4
4
  "description": "🚩 FastImage, performant React Native image component.",
5
5
  "keywords": [
6
6
  "cache",