react-native-fast-image 8.25.0 → 8.26.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 CHANGED
@@ -175,6 +175,33 @@ Whether the decoded image is kept in the memory cache. **Default: true.** With `
175
175
 
176
176
  ---
177
177
 
178
+ ### Several sizes of an image
179
+
180
+ `source` can also be an array of the same image at different sizes, each with its `width` and `height` in pixels (times `scale`, if it has one). The view loads the one whose size is closest to its own, in pixels, so a small view downloads a small image:
181
+
182
+ ```jsx
183
+ <FastImage
184
+ style={{ width: 120, height: 120 }}
185
+ source={[
186
+ { uri: 'https://example.com/photo-200.jpg', width: 200, height: 200 },
187
+ { uri: 'https://example.com/photo-800.jpg', width: 800, height: 800 },
188
+ {
189
+ uri: 'https://example.com/photo-2000.jpg',
190
+ width: 2000,
191
+ height: 2000,
192
+ },
193
+ ]}
194
+ />
195
+ ```
196
+
197
+ - The view picks once it has been laid out. One that has no size (e.g. sized from `onLoad`) loads the largest.
198
+ - When the view's size changes so that another size fits better, it loads that one (with its load events), and keeps showing the current image until then, without a `transition`.
199
+ - Each size is cached separately. A `cacheKey` applies to its own entry: give each size its own key (or none), or the sizes would replace each other in the cache.
200
+ - `FastImage.preload`, `getCachePath` and `writeToCache` take one size: pass the one a view will show. An array fails with `{ ok: false, error }`.
201
+ - An array of one is the same as that source.
202
+
203
+ ---
204
+
178
205
  ### `defaultSource?: number`
179
206
 
180
207
  - An asset loaded with `require(...)`.
@@ -512,8 +539,9 @@ With [react-native-web](https://necolas.github.io/react-native-web/), FastImage
512
539
 
513
540
  Support is minimal:
514
541
 
515
- - Props that work: `source` (a `uri`, or a `require()`d image), `defaultSource`, `resizeMode`, `tintColor`, `blurRadius`, `style`, children, `onLoadStart`, `onLoad`, `onError`, `onLoadEnd`, and View props such as `testID`, accessibility props, `onLayout` and `pointerEvents`.
542
+ - Props that work: `source` (a `uri`, a `require()`d image, or several sizes), `defaultSource`, `resizeMode`, `tintColor`, `blurRadius`, `style`, children, `onLoadStart`, `onLoad`, `onError`, `onLoadEnd`, and View props such as `testID`, accessibility props, `onLayout` and `pointerEvents`.
516
543
  - Ignored: `source.headers` (a browser can't send them for an image), `source.priority`, `source.cache`, `source.cacheKey`, `source.memoryCache`, `recyclingKey`, `loop`, `imageRendering`, `paused`, `transition`, `downsample`, `fallback` and `onProgress`.
544
+ - Several sizes are shown with a lazily loaded `<img>` whose `srcset` lists them, with `sizes="auto, 100vw"`: the browser loads the one for the width the image is shown at, in device pixels (usually the smallest that's at least as wide). Browsers that don't support `sizes="auto"` use the viewport's width. `tintColor`, `defaultSource` and `resizeMode="repeat"` don't apply to them.
517
545
  - `FastImage.preload` loads the images into the browser's cache and resolves with a result per source. `clearMemoryCache`, `clearDiskCache` and `configureCache` resolve without doing anything (the browser manages its cache), and `getCachePath` and `writeToCache` resolve with `{ ok: false, error: 'Not supported on the web' }`.
518
546
 
519
547
  ## Troubleshooting
@@ -14,6 +14,7 @@ import androidx.annotation.NonNull;
14
14
 
15
15
  import com.bumptech.glide.Glide;
16
16
  import com.bumptech.glide.RequestManager;
17
+ import com.facebook.react.bridge.ReadableArray;
17
18
  import com.facebook.react.bridge.ReadableMap;
18
19
  import com.facebook.react.bridge.WritableMap;
19
20
  import com.facebook.react.bridge.WritableNativeMap;
@@ -69,6 +70,11 @@ class FastImageViewManager extends SimpleViewManager<FastImageViewWithUrl> imple
69
70
  view.setSource(source);
70
71
  }
71
72
 
73
+ @ReactProp(name = "sources")
74
+ public void setSources(FastImageViewWithUrl view, @Nullable ReadableArray sources) {
75
+ view.setSources(sources);
76
+ }
77
+
72
78
  @ReactProp(name = "defaultSource")
73
79
  public void setDefaultSource(FastImageViewWithUrl view, @Nullable String source) {
74
80
  view.setDefaultSource(
@@ -35,6 +35,7 @@ import com.bumptech.glide.request.target.SizeReadyCallback;
35
35
  import com.bumptech.glide.request.transition.NoTransition;
36
36
  import com.bumptech.glide.request.transition.Transition;
37
37
  import com.bumptech.glide.request.transition.TransitionFactory;
38
+ import com.facebook.react.bridge.ReadableArray;
38
39
  import com.facebook.react.bridge.ReadableMap;
39
40
  import com.facebook.react.bridge.WritableMap;
40
41
  import com.facebook.react.bridge.WritableNativeMap;
@@ -63,7 +64,89 @@ class FastImageViewWithUrl extends AppCompatImageView {
63
64
 
64
65
  public void setSource(@Nullable ReadableMap source) {
65
66
  mNeedsReload = true;
66
- mSource = source;
67
+ mPropSource = source;
68
+ if (mSources == null) mSource = source;
69
+ }
70
+
71
+ // The `source` prop. With several `sources`, mSource is the one picked for
72
+ // the view's size instead.
73
+ @Nullable
74
+ private ReadableMap mPropSource = null;
75
+ // Several sources (2 or more) of the same image at different sizes, or
76
+ // null: the view loads the one whose size is closest to its own.
77
+ @Nullable
78
+ private ReadableArray mSources = null;
79
+ // Several sources: the index of the one picked (-1 before one is), and
80
+ // whether the load waits for the view's size (after layout), or has
81
+ // waited (then it loads the largest if the view still has no size).
82
+ private int mSourceIndex = -1;
83
+ private boolean mWaitsForSize = false;
84
+ private boolean mWaitedForSize = false;
85
+ // Several sources: the next load switches to another one for a new view
86
+ // size. It's the same picture at another size, so it doesn't fade in.
87
+ private boolean mSwitchesSource = false;
88
+ // What the last update was called with, to load once the view has a size.
89
+ @Nullable
90
+ private FastImageViewManager mManager;
91
+ @Nullable
92
+ private Map<String, List<FastImageViewWithUrl>> mViewsForUrlsMap;
93
+
94
+ public void setSources(@Nullable ReadableArray sources) {
95
+ mNeedsReload = true;
96
+ mSources = sources != null && sources.size() > 1 ? sources : null;
97
+ mSourceIndex = -1;
98
+ if (mSources == null) mSource = mPropSource;
99
+ }
100
+
101
+ // Of several sources, the index of the one whose size in pixels (width ×
102
+ // height × scale²) is closest to the view's (by pixel count), or of the
103
+ // largest while the view has no size (e.g. one sized from onLoad).
104
+ private int sourceIndexForSize() {
105
+ ReadableArray sources = mSources;
106
+ if (sources == null) return -1;
107
+ double viewPixels = (double) getWidth() * getHeight();
108
+ int best = -1;
109
+ double bestFit = Double.MAX_VALUE;
110
+ for (int i = 0; i < sources.size(); i++) {
111
+ ReadableMap source = sources.getMap(i);
112
+ if (source == null) continue;
113
+ double scale = number(source, "scale", 1);
114
+ double pixels = number(source, "width", 0) * number(source, "height", 0) * scale * scale;
115
+ double fit = viewPixels > 0 ? Math.abs(1 - pixels / viewPixels) : -pixels;
116
+ if (best < 0 || fit < bestFit) {
117
+ best = i;
118
+ bestFit = fit;
119
+ }
120
+ }
121
+ return best;
122
+ }
123
+
124
+ // Picks the source for the view's size (several sources).
125
+ private void pickSource() {
126
+ mSourceIndex = sourceIndexForSize();
127
+ mSource = mSourceIndex >= 0 && mSources != null ? mSources.getMap(mSourceIndex) : null;
128
+ }
129
+
130
+ private static double number(ReadableMap map, String key, double fallback) {
131
+ return map.hasKey(key) && !map.isNull(key) ? map.getDouble(key) : fallback;
132
+ }
133
+
134
+ // Several sources: another one fits the view's new size better. Loads it
135
+ // (after layout), keeping the image showing until then, without a fade.
136
+ private boolean switchSourceIfResized() {
137
+ if (mSources == null || mNeedsReload || mWaitsForSize || getWidth() <= 0 || getHeight() <= 0
138
+ || mManager == null || sourceIndexForSize() == mSourceIndex) {
139
+ return false;
140
+ }
141
+ mSwitchesSource = true;
142
+ mNeedsReload = true;
143
+ post(new Runnable() {
144
+ @Override
145
+ public void run() {
146
+ if (mNeedsReload && mManager != null) onAfterUpdate(mManager, requestManager, mViewsForUrlsMap);
147
+ }
148
+ });
149
+ return true;
67
150
  }
68
151
 
69
152
  public void setDefaultSource(@Nullable Drawable source) {
@@ -520,7 +603,7 @@ class FastImageViewWithUrl extends AppCompatImageView {
520
603
  mFadeFrom = null;
521
604
  invalidate();
522
605
  }
523
- reloadIfResized();
606
+ if (!switchSourceIfResized()) reloadIfResized();
524
607
  }
525
608
 
526
609
  private void reloadIfResized() {
@@ -671,16 +754,42 @@ class FastImageViewWithUrl extends AppCompatImageView {
671
754
  @Nonnull FastImageViewManager manager,
672
755
  @Nullable RequestManager requestManager,
673
756
  @Nonnull Map<String, List<FastImageViewWithUrl>> viewsForUrlsMap) {
757
+ mManager = manager;
758
+ mViewsForUrlsMap = viewsForUrlsMap;
674
759
  if (mBlurChanged && !mNeedsReload) {
675
760
  mBlurChanged = false;
676
761
  reblur();
677
762
  }
678
763
  if (!mNeedsReload)
679
764
  return;
765
+ // Several sources: one is picked for the view's size, so a view that
766
+ // hasn't been laid out yet loads after layout (props and layout are
767
+ // applied in the same batch). A view that still has no size then
768
+ // (e.g. one sized from onLoad) loads the largest source.
769
+ if (mSources != null) {
770
+ if (mWaitsForSize) return;
771
+ if ((getWidth() <= 0 || getHeight() <= 0) && !mWaitedForSize) {
772
+ mWaitsForSize = true;
773
+ post(new Runnable() {
774
+ @Override
775
+ public void run() {
776
+ if (!mWaitsForSize) return;
777
+ mWaitsForSize = false;
778
+ mWaitedForSize = true;
779
+ onAfterUpdate(manager, requestManager, viewsForUrlsMap);
780
+ }
781
+ });
782
+ return;
783
+ }
784
+ mWaitedForSize = false;
785
+ pickSource();
786
+ }
680
787
  // Only reload for changes that affect the request (source,
681
788
  // defaultSource, resizeMode), not for every prop update.
682
789
  mNeedsReload = false;
683
790
  mBlurChanged = false;
791
+ boolean switching = mSwitchesSource;
792
+ mSwitchesSource = false;
684
793
  boolean restarting = mRestarting;
685
794
  mRestarting = false;
686
795
  mLoadCount++;
@@ -791,7 +900,7 @@ class FastImageViewWithUrl extends AppCompatImageView {
791
900
  // replaces it at once, as Glide, Coil and Fresco fade an image in
792
901
  // from nothing and never between images.
793
902
  boolean replacing = shownRequest != null && model != null;
794
- if (mTransitionDuration > 0 && (!replacing || mTransitionBetweenImages)) {
903
+ if (mTransitionDuration > 0 && !switching && (!replacing || mTransitionBetweenImages)) {
795
904
  builder = builder.transition(DrawableTransitionOptions.with(
796
905
  new FadeFactory(mTransitionDuration, mTransitionSkipOnCacheHit)));
797
906
  }
package/dist/index.cjs.js CHANGED
@@ -125,7 +125,7 @@ function FastImageBase({ source, defaultSource, tintColor, onLoadStart, onProgre
125
125
  };
126
126
  const resolvedTintColor = tintColor != null ? tintColor : tintColorFromStyle(style);
127
127
  if (fallback) {
128
- const cleanedSource = typeof source === "number" ? source : withoutCache(source);
128
+ const cleanedSource = typeof source === "number" ? source : Array.isArray(source) ? source.map(withoutCache) : withoutCache(source);
129
129
  const resolvedSource = react_native.Image.resolveAssetSource(cleanedSource);
130
130
  return /* @__PURE__ */ react.default.createElement(react_native.View, {
131
131
  style: [styles.imageContainer, style],
@@ -164,7 +164,9 @@ function FastImageBase({ source, defaultSource, tintColor, onLoadStart, onProgre
164
164
  resizeMode
165
165
  }), children);
166
166
  }
167
- const resolvedSource = react_native.Image.resolveAssetSource(source);
167
+ const sources = Array.isArray(source) ? source.length === 1 ? void 0 : source : void 0;
168
+ const single = Array.isArray(source) ? source[0] : source;
169
+ const resolvedSource = sources ? void 0 : react_native.Image.resolveAssetSource(single);
168
170
  const resolvedDefaultSource = resolveDefaultSource(defaultSource);
169
171
  return /* @__PURE__ */ react.default.createElement(react_native.View, {
170
172
  style: [styles.imageContainer, style],
@@ -177,6 +179,7 @@ function FastImageBase({ source, defaultSource, tintColor, onLoadStart, onProgre
177
179
  ...transitionProps(transition),
178
180
  style: react_native.StyleSheet.absoluteFill,
179
181
  source: resolvedSource,
182
+ sources,
180
183
  defaultSource: resolvedDefaultSource,
181
184
  onFastImageLoadStart: onLoadStart,
182
185
  onFastImageProgress: withProgress(onProgress),
@@ -200,8 +203,14 @@ const FastImage = FastImageComponent;
200
203
  FastImage.resizeMode = resizeMode;
201
204
  FastImage.cacheControl = cacheControl;
202
205
  FastImage.priority = priority;
203
- FastImage.preload = (sources) => Promise.resolve(react_native.NativeModules.FastImageView.preload(sources.map((s) => s || {}))).then((results) => sources.map((source, i) => {
206
+ const ONE_SOURCE = "Takes one source, not an array of sizes: pass the size to use";
207
+ FastImage.preload = (sources) => Promise.resolve(react_native.NativeModules.FastImageView.preload(sources.map((s) => s && !Array.isArray(s) ? s : {}))).then((results) => sources.map((source, i) => {
204
208
  var _results$i;
209
+ if (Array.isArray(source)) return {
210
+ ok: false,
211
+ error: ONE_SOURCE,
212
+ uri: void 0
213
+ };
205
214
  const uri = source ? source.uri : void 0;
206
215
  const result = (_results$i = results === null || results === void 0 ? void 0 : results[i]) !== null && _results$i !== void 0 ? _results$i : noResult;
207
216
  if (!result.ok) return {
@@ -220,8 +229,14 @@ FastImage.preload = (sources) => Promise.resolve(react_native.NativeModules.Fast
220
229
  FastImage.clearMemoryCache = () => react_native.NativeModules.FastImageView.clearMemoryCache();
221
230
  FastImage.clearDiskCache = () => react_native.NativeModules.FastImageView.clearDiskCache();
222
231
  FastImage.configureCache = (limits = {}) => Promise.resolve(react_native.NativeModules.FastImageView.configureCache(limits));
223
- FastImage.writeToCache = (source, file) => Promise.resolve(react_native.NativeModules.FastImageView.writeToCache(source || {}, file));
224
- FastImage.getCachePath = (source) => Promise.resolve(react_native.NativeModules.FastImageView.getCachePath(source || {}));
232
+ FastImage.writeToCache = (source, file) => Array.isArray(source) ? Promise.resolve({
233
+ ok: false,
234
+ error: ONE_SOURCE
235
+ }) : Promise.resolve(react_native.NativeModules.FastImageView.writeToCache(source || {}, file));
236
+ FastImage.getCachePath = (source) => Array.isArray(source) ? Promise.resolve({
237
+ ok: false,
238
+ error: ONE_SOURCE
239
+ }) : Promise.resolve(react_native.NativeModules.FastImageView.getCachePath(source || {}));
225
240
  const styles = react_native.StyleSheet.create({
226
241
  fallbackImage: {
227
242
  position: "absolute",
@@ -104,6 +104,9 @@ export type FastImageSource = {
104
104
  cache?: CacheControls,
105
105
  cacheKey?: string,
106
106
  memoryCache?: boolean,
107
+ width?: number,
108
+ height?: number,
109
+ scale?: number,
107
110
  }
108
111
 
109
112
  export type FastImageProps = $ReadOnly<{|
@@ -114,7 +117,7 @@ export type FastImageProps = $ReadOnly<{|
114
117
  onLoadStart?: ?() => void,
115
118
  onProgress?: ?(event: OnProgressEvent) => void,
116
119
 
117
- source?: ?(FastImageSource | number),
120
+ source?: ?(FastImageSource | Array<FastImageSource> | number),
118
121
  defaultSource?: ?number,
119
122
 
120
123
  tintColor?: number | string,
@@ -55,14 +55,27 @@ function FastImageBase({ source, defaultSource, tintColor, blurRadius, onLoadSta
55
55
  const result = (0, react.useRef)(void 0);
56
56
  const sent = (0, react.useRef)(Promise.resolve());
57
57
  const image = (0, react.useRef)(null);
58
- const uri = typeof source === "object" && source ? source.uri : void 0;
59
- const webSource = typeof source === "number" ? source : uri ? { uri } : void 0;
58
+ const sizes = Array.isArray(source) && source.length > 1 ? source : void 0;
59
+ const single = Array.isArray(source) ? source[0] : source;
60
+ const uri = typeof single === "object" && single ? single.uri : void 0;
61
+ const webSource = typeof single === "number" ? single : uri ? { uri } : void 0;
60
62
  return /* @__PURE__ */ react.default.createElement(react_native.View, {
61
63
  style: [styles.container, style],
62
64
  onLayout,
63
65
  pointerEvents,
64
66
  ref: forwardedRef
65
- }, /* @__PURE__ */ react.default.createElement(react_native.Image, {
67
+ }, sizes ? /* @__PURE__ */ react.default.createElement(react_native.View, {
68
+ ...props,
69
+ style: styles.image
70
+ }, /* @__PURE__ */ react.default.createElement(ImageSizes, {
71
+ sources: sizes,
72
+ resizeMode: mode,
73
+ blurRadius,
74
+ onLoadStart,
75
+ onLoad,
76
+ onError,
77
+ onLoadEnd
78
+ })) : /* @__PURE__ */ react.default.createElement(react_native.Image, {
66
79
  ...props,
67
80
  style: styles.image,
68
81
  source: webSource,
@@ -108,6 +121,68 @@ function FastImageBase({ source, defaultSource, tintColor, blurRadius, onLoadSta
108
121
  }
109
122
  }), children);
110
123
  }
124
+ function ImageSizes({ sources, resizeMode: mode, blurRadius, onLoadStart, onLoad, onError, onLoadEnd }) {
125
+ var _largest$uri, _sources$find;
126
+ const sized = sources.filter((source) => (source === null || source === void 0 ? void 0 : source.uri) && source.width).map((source) => {
127
+ var _source$width, _source$scale;
128
+ return {
129
+ uri: source.uri,
130
+ width: Math.round(((_source$width = source.width) !== null && _source$width !== void 0 ? _source$width : 0) * ((_source$scale = source.scale) !== null && _source$scale !== void 0 ? _source$scale : 1))
131
+ };
132
+ });
133
+ const largest = sized.reduce((best, source) => !best || source.width > best.width ? source : best, void 0);
134
+ const src = (_largest$uri = largest === null || largest === void 0 ? void 0 : largest.uri) !== null && _largest$uri !== void 0 ? _largest$uri : (_sources$find = sources.find((source) => source === null || source === void 0 ? void 0 : source.uri)) === null || _sources$find === void 0 ? void 0 : _sources$find.uri;
135
+ const srcSet = sized.length > 0 ? sized.map((source) => `${source.uri} ${source.width}w`).join(", ") : void 0;
136
+ (0, react.useEffect)(() => {
137
+ onLoadStart === null || onLoadStart === void 0 || onLoadStart();
138
+ }, [src, srcSet]);
139
+ return /* @__PURE__ */ react.default.createElement("img", {
140
+ src,
141
+ srcSet,
142
+ sizes: srcSet ? "auto, 100vw" : void 0,
143
+ loading: "lazy",
144
+ alt: "",
145
+ draggable: false,
146
+ style: {
147
+ position: "absolute",
148
+ width: "100%",
149
+ height: "100%",
150
+ objectFit: OBJECT_FIT[mode !== null && mode !== void 0 ? mode : "cover"],
151
+ filter: blurRadius ? `blur(${blurRadius}px)` : void 0
152
+ },
153
+ onLoad: (event) => {
154
+ var _picked$width;
155
+ const image = event.currentTarget;
156
+ const picked = srcSet ? sized.find((source) => new URL(source.uri, image.currentSrc).href === image.currentSrc) : void 0;
157
+ const loadedWidth = (_picked$width = picked === null || picked === void 0 ? void 0 : picked.width) !== null && _picked$width !== void 0 ? _picked$width : image.naturalWidth;
158
+ const loadedHeight = picked ? Math.round(image.naturalHeight * picked.width / image.naturalWidth) : image.naturalHeight;
159
+ onLoad === null || onLoad === void 0 || onLoad({ nativeEvent: {
160
+ width: loadedWidth,
161
+ height: loadedHeight
162
+ } });
163
+ onLoadEnd === null || onLoadEnd === void 0 || onLoadEnd({
164
+ ok: true,
165
+ width: loadedWidth,
166
+ height: loadedHeight
167
+ });
168
+ },
169
+ onError: () => {
170
+ const error = "Failed to load the image";
171
+ onError === null || onError === void 0 || onError({ nativeEvent: { error } });
172
+ onLoadEnd === null || onLoadEnd === void 0 || onLoadEnd({
173
+ ok: false,
174
+ error
175
+ });
176
+ }
177
+ });
178
+ }
179
+ const OBJECT_FIT = {
180
+ cover: "cover",
181
+ contain: "contain",
182
+ stretch: "fill",
183
+ center: "scale-down",
184
+ repeat: "cover"
185
+ };
111
186
  const FastImageMemo = (0, react.memo)(FastImageBase);
112
187
  const FastImageComponent = (0, react.forwardRef)((props, ref) => /* @__PURE__ */ react.default.createElement(FastImageMemo, {
113
188
  forwardedRef: ref,
@@ -104,6 +104,9 @@ export type FastImageSource = {
104
104
  cache?: CacheControls,
105
105
  cacheKey?: string,
106
106
  memoryCache?: boolean,
107
+ width?: number,
108
+ height?: number,
109
+ scale?: number,
107
110
  }
108
111
 
109
112
  export type FastImageProps = $ReadOnly<{|
@@ -114,7 +117,7 @@ export type FastImageProps = $ReadOnly<{|
114
117
  onLoadStart?: ?() => void,
115
118
  onProgress?: ?(event: OnProgressEvent) => void,
116
119
 
117
- source?: ?(FastImageSource | number),
120
+ source?: ?(FastImageSource | Array<FastImageSource> | number),
118
121
  defaultSource?: ?number,
119
122
 
120
123
  tintColor?: number | string,
package/dist/index.d.ts CHANGED
@@ -26,6 +26,14 @@ export type Source = {
26
26
  * list scrolled back decodes them again.
27
27
  */
28
28
  memoryCache?: boolean;
29
+ /**
30
+ * With several sources (`source` as an array), the image's size at this
31
+ * uri in pixels (width and height, times `scale` if given): the view
32
+ * loads the source closest to its own size.
33
+ */
34
+ width?: number;
35
+ height?: number;
36
+ scale?: number;
29
37
  };
30
38
  export type Transition = {
31
39
  /**
@@ -116,7 +124,12 @@ export interface ImageStyle extends ViewStyle {
116
124
  opacity?: number;
117
125
  }
118
126
  export interface FastImageProps extends AccessibilityProps, ViewProps {
119
- source?: Source | ImageRequireSource;
127
+ /**
128
+ * The image: a source, a `require()`d image, or several sources of the
129
+ * same image at different sizes (each with its `width` and `height` in
130
+ * pixels), of which the view loads the one closest to its size.
131
+ */
132
+ source?: Source | Source[] | ImageRequireSource;
120
133
  defaultSource?: ImageRequireSource;
121
134
  resizeMode?: ResizeMode;
122
135
  fallback?: boolean;
package/dist/index.js CHANGED
@@ -98,7 +98,7 @@ function FastImageBase({ source, defaultSource, tintColor, onLoadStart, onProgre
98
98
  };
99
99
  const resolvedTintColor = tintColor != null ? tintColor : tintColorFromStyle(style);
100
100
  if (fallback) {
101
- const cleanedSource = typeof source === "number" ? source : withoutCache(source);
101
+ const cleanedSource = typeof source === "number" ? source : Array.isArray(source) ? source.map(withoutCache) : withoutCache(source);
102
102
  const resolvedSource = Image.resolveAssetSource(cleanedSource);
103
103
  return /* @__PURE__ */ React.createElement(View, {
104
104
  style: [styles.imageContainer, style],
@@ -137,7 +137,9 @@ function FastImageBase({ source, defaultSource, tintColor, onLoadStart, onProgre
137
137
  resizeMode
138
138
  }), children);
139
139
  }
140
- const resolvedSource = Image.resolveAssetSource(source);
140
+ const sources = Array.isArray(source) ? source.length === 1 ? void 0 : source : void 0;
141
+ const single = Array.isArray(source) ? source[0] : source;
142
+ const resolvedSource = sources ? void 0 : Image.resolveAssetSource(single);
141
143
  const resolvedDefaultSource = resolveDefaultSource(defaultSource);
142
144
  return /* @__PURE__ */ React.createElement(View, {
143
145
  style: [styles.imageContainer, style],
@@ -150,6 +152,7 @@ function FastImageBase({ source, defaultSource, tintColor, onLoadStart, onProgre
150
152
  ...transitionProps(transition),
151
153
  style: StyleSheet.absoluteFill,
152
154
  source: resolvedSource,
155
+ sources,
153
156
  defaultSource: resolvedDefaultSource,
154
157
  onFastImageLoadStart: onLoadStart,
155
158
  onFastImageProgress: withProgress(onProgress),
@@ -173,8 +176,14 @@ const FastImage = FastImageComponent;
173
176
  FastImage.resizeMode = resizeMode;
174
177
  FastImage.cacheControl = cacheControl;
175
178
  FastImage.priority = priority;
176
- FastImage.preload = (sources) => Promise.resolve(NativeModules.FastImageView.preload(sources.map((s) => s || {}))).then((results) => sources.map((source, i) => {
179
+ const ONE_SOURCE = "Takes one source, not an array of sizes: pass the size to use";
180
+ FastImage.preload = (sources) => Promise.resolve(NativeModules.FastImageView.preload(sources.map((s) => s && !Array.isArray(s) ? s : {}))).then((results) => sources.map((source, i) => {
177
181
  var _results$i;
182
+ if (Array.isArray(source)) return {
183
+ ok: false,
184
+ error: ONE_SOURCE,
185
+ uri: void 0
186
+ };
178
187
  const uri = source ? source.uri : void 0;
179
188
  const result = (_results$i = results === null || results === void 0 ? void 0 : results[i]) !== null && _results$i !== void 0 ? _results$i : noResult;
180
189
  if (!result.ok) return {
@@ -193,8 +202,14 @@ FastImage.preload = (sources) => Promise.resolve(NativeModules.FastImageView.pre
193
202
  FastImage.clearMemoryCache = () => NativeModules.FastImageView.clearMemoryCache();
194
203
  FastImage.clearDiskCache = () => NativeModules.FastImageView.clearDiskCache();
195
204
  FastImage.configureCache = (limits = {}) => Promise.resolve(NativeModules.FastImageView.configureCache(limits));
196
- FastImage.writeToCache = (source, file) => Promise.resolve(NativeModules.FastImageView.writeToCache(source || {}, file));
197
- FastImage.getCachePath = (source) => Promise.resolve(NativeModules.FastImageView.getCachePath(source || {}));
205
+ FastImage.writeToCache = (source, file) => Array.isArray(source) ? Promise.resolve({
206
+ ok: false,
207
+ error: ONE_SOURCE
208
+ }) : Promise.resolve(NativeModules.FastImageView.writeToCache(source || {}, file));
209
+ FastImage.getCachePath = (source) => Array.isArray(source) ? Promise.resolve({
210
+ ok: false,
211
+ error: ONE_SOURCE
212
+ }) : Promise.resolve(NativeModules.FastImageView.getCachePath(source || {}));
198
213
  const styles = StyleSheet.create({
199
214
  fallbackImage: {
200
215
  position: "absolute",
@@ -104,6 +104,9 @@ export type FastImageSource = {
104
104
  cache?: CacheControls,
105
105
  cacheKey?: string,
106
106
  memoryCache?: boolean,
107
+ width?: number,
108
+ height?: number,
109
+ scale?: number,
107
110
  }
108
111
 
109
112
  export type FastImageProps = $ReadOnly<{|
@@ -114,7 +117,7 @@ export type FastImageProps = $ReadOnly<{|
114
117
  onLoadStart?: ?() => void,
115
118
  onProgress?: ?(event: OnProgressEvent) => void,
116
119
 
117
- source?: ?(FastImageSource | number),
120
+ source?: ?(FastImageSource | Array<FastImageSource> | number),
118
121
  defaultSource?: ?number,
119
122
 
120
123
  tintColor?: number | string,
package/dist/index.web.js CHANGED
@@ -1,4 +1,4 @@
1
- import React, { forwardRef, memo, useRef } from "react";
1
+ import React, { forwardRef, memo, useEffect, useRef } from "react";
2
2
  import { Image, StyleSheet, View } from "react-native";
3
3
  //#region src/constants.ts
4
4
  const resizeMode = {
@@ -28,14 +28,27 @@ function FastImageBase({ source, defaultSource, tintColor, blurRadius, onLoadSta
28
28
  const result = useRef(void 0);
29
29
  const sent = useRef(Promise.resolve());
30
30
  const image = useRef(null);
31
- const uri = typeof source === "object" && source ? source.uri : void 0;
32
- const webSource = typeof source === "number" ? source : uri ? { uri } : void 0;
31
+ const sizes = Array.isArray(source) && source.length > 1 ? source : void 0;
32
+ const single = Array.isArray(source) ? source[0] : source;
33
+ const uri = typeof single === "object" && single ? single.uri : void 0;
34
+ const webSource = typeof single === "number" ? single : uri ? { uri } : void 0;
33
35
  return /* @__PURE__ */ React.createElement(View, {
34
36
  style: [styles.container, style],
35
37
  onLayout,
36
38
  pointerEvents,
37
39
  ref: forwardedRef
38
- }, /* @__PURE__ */ React.createElement(Image, {
40
+ }, sizes ? /* @__PURE__ */ React.createElement(View, {
41
+ ...props,
42
+ style: styles.image
43
+ }, /* @__PURE__ */ React.createElement(ImageSizes, {
44
+ sources: sizes,
45
+ resizeMode: mode,
46
+ blurRadius,
47
+ onLoadStart,
48
+ onLoad,
49
+ onError,
50
+ onLoadEnd
51
+ })) : /* @__PURE__ */ React.createElement(Image, {
39
52
  ...props,
40
53
  style: styles.image,
41
54
  source: webSource,
@@ -81,6 +94,68 @@ function FastImageBase({ source, defaultSource, tintColor, blurRadius, onLoadSta
81
94
  }
82
95
  }), children);
83
96
  }
97
+ function ImageSizes({ sources, resizeMode: mode, blurRadius, onLoadStart, onLoad, onError, onLoadEnd }) {
98
+ var _largest$uri, _sources$find;
99
+ const sized = sources.filter((source) => (source === null || source === void 0 ? void 0 : source.uri) && source.width).map((source) => {
100
+ var _source$width, _source$scale;
101
+ return {
102
+ uri: source.uri,
103
+ width: Math.round(((_source$width = source.width) !== null && _source$width !== void 0 ? _source$width : 0) * ((_source$scale = source.scale) !== null && _source$scale !== void 0 ? _source$scale : 1))
104
+ };
105
+ });
106
+ const largest = sized.reduce((best, source) => !best || source.width > best.width ? source : best, void 0);
107
+ const src = (_largest$uri = largest === null || largest === void 0 ? void 0 : largest.uri) !== null && _largest$uri !== void 0 ? _largest$uri : (_sources$find = sources.find((source) => source === null || source === void 0 ? void 0 : source.uri)) === null || _sources$find === void 0 ? void 0 : _sources$find.uri;
108
+ const srcSet = sized.length > 0 ? sized.map((source) => `${source.uri} ${source.width}w`).join(", ") : void 0;
109
+ useEffect(() => {
110
+ onLoadStart === null || onLoadStart === void 0 || onLoadStart();
111
+ }, [src, srcSet]);
112
+ return /* @__PURE__ */ React.createElement("img", {
113
+ src,
114
+ srcSet,
115
+ sizes: srcSet ? "auto, 100vw" : void 0,
116
+ loading: "lazy",
117
+ alt: "",
118
+ draggable: false,
119
+ style: {
120
+ position: "absolute",
121
+ width: "100%",
122
+ height: "100%",
123
+ objectFit: OBJECT_FIT[mode !== null && mode !== void 0 ? mode : "cover"],
124
+ filter: blurRadius ? `blur(${blurRadius}px)` : void 0
125
+ },
126
+ onLoad: (event) => {
127
+ var _picked$width;
128
+ const image = event.currentTarget;
129
+ const picked = srcSet ? sized.find((source) => new URL(source.uri, image.currentSrc).href === image.currentSrc) : void 0;
130
+ const loadedWidth = (_picked$width = picked === null || picked === void 0 ? void 0 : picked.width) !== null && _picked$width !== void 0 ? _picked$width : image.naturalWidth;
131
+ const loadedHeight = picked ? Math.round(image.naturalHeight * picked.width / image.naturalWidth) : image.naturalHeight;
132
+ onLoad === null || onLoad === void 0 || onLoad({ nativeEvent: {
133
+ width: loadedWidth,
134
+ height: loadedHeight
135
+ } });
136
+ onLoadEnd === null || onLoadEnd === void 0 || onLoadEnd({
137
+ ok: true,
138
+ width: loadedWidth,
139
+ height: loadedHeight
140
+ });
141
+ },
142
+ onError: () => {
143
+ const error = "Failed to load the image";
144
+ onError === null || onError === void 0 || onError({ nativeEvent: { error } });
145
+ onLoadEnd === null || onLoadEnd === void 0 || onLoadEnd({
146
+ ok: false,
147
+ error
148
+ });
149
+ }
150
+ });
151
+ }
152
+ const OBJECT_FIT = {
153
+ cover: "cover",
154
+ contain: "contain",
155
+ stretch: "fill",
156
+ center: "scale-down",
157
+ repeat: "cover"
158
+ };
84
159
  const FastImageMemo = memo(FastImageBase);
85
160
  const FastImageComponent = forwardRef((props, ref) => /* @__PURE__ */ React.createElement(FastImageMemo, {
86
161
  forwardedRef: ref,
@@ -104,6 +104,9 @@ export type FastImageSource = {
104
104
  cache?: CacheControls,
105
105
  cacheKey?: string,
106
106
  memoryCache?: boolean,
107
+ width?: number,
108
+ height?: number,
109
+ scale?: number,
107
110
  }
108
111
 
109
112
  export type FastImageProps = $ReadOnly<{|
@@ -114,7 +117,7 @@ export type FastImageProps = $ReadOnly<{|
114
117
  onLoadStart?: ?() => void,
115
118
  onProgress?: ?(event: OnProgressEvent) => void,
116
119
 
117
- source?: ?(FastImageSource | number),
120
+ source?: ?(FastImageSource | Array<FastImageSource> | number),
118
121
  defaultSource?: ?number,
119
122
 
120
123
  tintColor?: number | string,
@@ -32,6 +32,9 @@ typedef NS_ENUM(NSInteger, FFFCacheControl) {
32
32
  @property (nonatomic) FFFCacheControl cacheControl;
33
33
  // The key to cache the image under instead of its url, or nil.
34
34
  @property (nonatomic, copy) NSString *cacheKey;
35
+ // With several sources, the image's size at this url in pixels (width ×
36
+ // height × scale²), to pick the one closest to the view's size; 0 if unknown.
37
+ @property (nonatomic) CGFloat pixelCount;
35
38
  // Whether the decoded image is kept in the memory cache (`memoryCache`,
36
39
  // default YES). Without it, it's only kept on disk: views don't leave it in
37
40
  // memory, and preload doesn't decode it.
@@ -17,6 +17,9 @@
17
17
  @property (nonatomic, copy) RCTDirectEventBlock onFastImageLoadEnd;
18
18
  @property (nonatomic, assign) RCTResizeMode resizeMode;
19
19
  @property (nonatomic, strong) FFFastImageSource *source;
20
+ // Several sources (2 or more) of the same image at different sizes: the view
21
+ // loads the one whose size is closest to its own.
22
+ @property (nonatomic, copy) NSArray<FFFastImageSource *> *sources;
20
23
  @property (nonatomic, strong) UIImage *defaultSource;
21
24
  @property (nonatomic, strong) UIColor *imageColor;
22
25
  // When it changes, the next image doesn't replace the current one: the view
@@ -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
  }
@@ -643,12 +653,59 @@ NSString *FFFErrorMessage(NSError *error)
643
653
  }
644
654
 
645
655
  - (void) setSource: (FFFastImageSource*)source {
646
- if (_source != source) {
647
- _source = source;
656
+ if (self.propSource != source) {
657
+ self.propSource = source;
658
+ if (![self picksSource]) {
659
+ _source = source;
660
+ }
661
+ _needsReload = YES;
662
+ }
663
+ }
664
+
665
+ - (void) setSources: (NSArray<FFFastImageSource*>*)sources {
666
+ if (_sources != sources) {
667
+ _sources = [sources copy];
668
+ if (![self picksSource]) {
669
+ _source = self.propSource;
670
+ }
648
671
  _needsReload = YES;
649
672
  }
650
673
  }
651
674
 
675
+ // Whether the view picks one of several sources for its size.
676
+ - (BOOL) picksSource {
677
+ return _sources.count > 1;
678
+ }
679
+
680
+ // Of several sources, the one whose size in pixels is closest to the view's
681
+ // (by pixel count), or the largest while the view has no size (e.g. one sized
682
+ // from onLoad).
683
+ - (FFFastImageSource*) sourceForSize {
684
+ CGFloat scale = self.window.screen.scale ?: [UIScreen mainScreen].scale;
685
+ CGFloat viewPixels = self.bounds.size.width * self.bounds.size.height * scale * scale;
686
+ FFFastImageSource* best = nil;
687
+ CGFloat bestFit = CGFLOAT_MAX;
688
+ for (FFFastImageSource* source in _sources) {
689
+ CGFloat fit = viewPixels > 0 ? ABS(1 - source.pixelCount / viewPixels) : -source.pixelCount;
690
+ if (!best || fit < bestFit) {
691
+ best = source;
692
+ bestFit = fit;
693
+ }
694
+ }
695
+ return best;
696
+ }
697
+
698
+ // Several sources: another one fits the view's new size better. Loads it,
699
+ // keeping the image showing until then, without a fade (see switchesSource).
700
+ - (BOOL) switchSourceIfResized {
701
+ if (![self picksSource] || _needsReload || ![self hasSize] || [self sourceForSize] == _source) {
702
+ return NO;
703
+ }
704
+ self.switchesSource = YES;
705
+ [self reloadImage];
706
+ return YES;
707
+ }
708
+
652
709
  - (void) setRecyclingKey: (NSString*)recyclingKey {
653
710
  if (_recyclingKey == recyclingKey || [_recyclingKey isEqualToString: recyclingKey]) {
654
711
  return;
@@ -674,12 +731,13 @@ NSString *FFFErrorMessage(NSError *error)
674
731
 
675
732
  - (void) didSetProps: (NSArray<NSString*>*)changedProps {
676
733
  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]) {
734
+ // With downsample on, the image is decoded for the view's size, and
735
+ // with several sources one is picked for it, so a view that hasn't
736
+ // been laid out yet loads once it has. Props and layout are applied
737
+ // in the same update, so that's before the next frame (in
738
+ // layoutSubviews). A view that still has no size then (e.g. one sized
739
+ // from onLoad) loads at full size, or the largest source.
740
+ if (([self downsamples] || [self picksSource]) && ![self hasSize]) {
683
741
  if (!self.waitsForSize) {
684
742
  self.waitsForSize = YES;
685
743
  __weak typeof(self) weakSelf = self;
@@ -766,6 +824,11 @@ NSString *FFFErrorMessage(NSError *error)
766
824
  - (void) reloadImage {
767
825
  _needsReload = NO;
768
826
  self.waitsForSize = NO;
827
+ self.loadsSwitchedSource = self.switchesSource;
828
+ self.switchesSource = NO;
829
+ if ([self picksSource]) {
830
+ _source = [self sourceForSize];
831
+ }
769
832
  self.decodedBox = CGSizeZero;
770
833
  // The previous load, if it's still running, is for a source the view no
771
834
  // longer shows: cancel it (a new download would, but a data uri or no
@@ -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)
@@ -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.26.0",
4
4
  "description": "🚩 FastImage, performant React Native image component.",
5
5
  "keywords": [
6
6
  "cache",