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 +29 -1
- package/android/src/main/java/com/dylanvann/fastimage/FastImageViewManager.java +6 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageViewWithUrl.java +112 -3
- package/dist/index.cjs.js +20 -5
- package/dist/index.cjs.js.flow +4 -1
- package/dist/index.cjs.web.js +78 -3
- package/dist/index.cjs.web.js.flow +4 -1
- package/dist/index.d.ts +14 -1
- package/dist/index.js +20 -5
- package/dist/index.js.flow +4 -1
- package/dist/index.web.js +79 -4
- package/dist/index.web.js.flow +4 -1
- package/ios/FastImage/FFFastImageSource.h +3 -0
- package/ios/FastImage/FFFastImageView.h +3 -0
- package/ios/FastImage/FFFastImageView.m +74 -11
- package/ios/FastImage/FFFastImageViewManager.m +1 -0
- package/ios/FastImage/RCTConvert+FFFastImage.h +1 -0
- package/ios/FastImage/RCTConvert+FFFastImage.m +2 -0
- package/package.json +1 -1
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`,
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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) =>
|
|
224
|
-
|
|
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",
|
package/dist/index.cjs.js.flow
CHANGED
|
@@ -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.cjs.web.js
CHANGED
|
@@ -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
|
|
59
|
-
const
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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) =>
|
|
197
|
-
|
|
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",
|
package/dist/index.js.flow
CHANGED
|
@@ -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
|
|
32
|
-
const
|
|
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(
|
|
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,
|
package/dist/index.web.js.flow
CHANGED
|
@@ -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
|
|
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 ||
|
|
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 (
|
|
647
|
-
|
|
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,
|
|
678
|
-
//
|
|
679
|
-
//
|
|
680
|
-
//
|
|
681
|
-
//
|
|
682
|
-
|
|
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)
|
|
@@ -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
|
}
|