react-native-fast-image 10.1.1 → 10.2.0

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