react-native-fast-image 9.2.2 → 9.3.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 +36 -7
- package/android/src/main/java/com/dylanvann/fastimage/FastImageBlur.java +18 -5
- package/android/src/main/java/com/dylanvann/fastimage/FastImageRequestListener.java +5 -1
- package/android/src/main/java/com/dylanvann/fastimage/FastImageSourceSize.java +22 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageSvg.java +27 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageSvgRenderer.java +10 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageViewConverter.java +5 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageViewManager.java +1 -1
- package/android/src/main/java/com/dylanvann/fastimage/FastImageViewWithUrl.java +157 -14
- package/dist/index.cjs.js +50 -18
- package/dist/index.cjs.js.flow +3 -0
- package/dist/index.cjs.web.js +241 -130
- package/dist/index.cjs.web.js.flow +3 -0
- package/dist/index.d.ts +38 -6
- package/dist/index.js +50 -18
- package/dist/index.js.flow +3 -0
- package/dist/index.web.js +243 -132
- package/dist/index.web.js.flow +3 -0
- package/dist/objectFit.d.ts +3 -0
- package/ios/FastImage/FFFDownsampledImage.h +8 -0
- package/ios/FastImage/FFFDownsampledImage.m +16 -0
- package/ios/FastImage/FFFastImageView.h +7 -1
- package/ios/FastImage/FFFastImageView.m +10 -5
- package/ios/FastImage/FFFastImageViewManager.m +3 -1
- package/ios/FastImage/RCTConvert+FFFastImage.h +3 -0
- package/ios/FastImage/RCTConvert+FFFastImage.m +11 -0
- package/package.json +1 -1
package/dist/index.cjs.js
CHANGED
|
@@ -46,30 +46,61 @@ const cacheControl = {
|
|
|
46
46
|
cacheOnly: "cacheOnly"
|
|
47
47
|
};
|
|
48
48
|
//#endregion
|
|
49
|
-
//#region src/
|
|
50
|
-
|
|
51
|
-
if (!defaultSource) return null;
|
|
52
|
-
if (react_native.Platform.OS === "android") {
|
|
53
|
-
const resolved = react_native.Image.resolveAssetSource(defaultSource);
|
|
54
|
-
if (resolved) return resolved.uri;
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
return defaultSource;
|
|
58
|
-
};
|
|
59
|
-
function tintColorFromStyle(style) {
|
|
49
|
+
//#region src/objectFit.ts
|
|
50
|
+
function fromStyle(style, key) {
|
|
60
51
|
if (Array.isArray(style)) {
|
|
61
52
|
for (let i = style.length - 1; i >= 0; i--) {
|
|
62
|
-
const found =
|
|
53
|
+
const found = fromStyle(style[i], key);
|
|
63
54
|
if (found !== void 0) return found;
|
|
64
55
|
}
|
|
65
56
|
return;
|
|
66
57
|
}
|
|
67
58
|
if (typeof style === "number") {
|
|
68
59
|
const flattened = react_native.StyleSheet.flatten(style);
|
|
69
|
-
return flattened ? flattened
|
|
60
|
+
return flattened ? flattened[key] : void 0;
|
|
70
61
|
}
|
|
71
|
-
return style && typeof style === "object" ? style
|
|
62
|
+
return style && typeof style === "object" ? style[key] : void 0;
|
|
63
|
+
}
|
|
64
|
+
const RESIZE_MODE_FIT = {
|
|
65
|
+
contain: "contain",
|
|
66
|
+
cover: "cover",
|
|
67
|
+
stretch: "fill",
|
|
68
|
+
center: "center",
|
|
69
|
+
repeat: "repeat"
|
|
70
|
+
};
|
|
71
|
+
const FITS = {
|
|
72
|
+
fill: true,
|
|
73
|
+
contain: true,
|
|
74
|
+
cover: true,
|
|
75
|
+
none: true,
|
|
76
|
+
"scale-down": true
|
|
77
|
+
};
|
|
78
|
+
function resolveObjectFit(objectFit, style, resizeMode) {
|
|
79
|
+
var _RESIZE_MODE_FIT$resi;
|
|
80
|
+
const fit = objectFit != null ? objectFit : fromStyle(style, "objectFit");
|
|
81
|
+
if (fit && FITS[fit] === true) return fit;
|
|
82
|
+
return (_RESIZE_MODE_FIT$resi = RESIZE_MODE_FIT[resizeMode]) !== null && _RESIZE_MODE_FIT$resi !== void 0 ? _RESIZE_MODE_FIT$resi : "cover";
|
|
72
83
|
}
|
|
84
|
+
//#endregion
|
|
85
|
+
//#region src/index.tsx
|
|
86
|
+
const resolveDefaultSource = (defaultSource) => {
|
|
87
|
+
if (!defaultSource) return null;
|
|
88
|
+
if (react_native.Platform.OS === "android") {
|
|
89
|
+
const resolved = react_native.Image.resolveAssetSource(defaultSource);
|
|
90
|
+
if (resolved) return resolved.uri;
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
return defaultSource;
|
|
94
|
+
};
|
|
95
|
+
const NATIVE_RESIZE_MODE = {
|
|
96
|
+
fill: "stretch",
|
|
97
|
+
contain: "contain",
|
|
98
|
+
cover: "cover",
|
|
99
|
+
none: "none",
|
|
100
|
+
"scale-down": "scale-down",
|
|
101
|
+
center: "center",
|
|
102
|
+
repeat: "repeat"
|
|
103
|
+
};
|
|
73
104
|
function loopCount(loop) {
|
|
74
105
|
if (loop === void 0) return -1;
|
|
75
106
|
if (loop === true) return 0;
|
|
@@ -111,7 +142,7 @@ function withoutCache(source) {
|
|
|
111
142
|
const { cache: _cache, ...rest } = source || {};
|
|
112
143
|
return rest;
|
|
113
144
|
}
|
|
114
|
-
function FastImageBase({ source, defaultSource, tintColor, onLoadStart, onProgress, onLoad, onError, onLoadEnd, style, fallback, children, resizeMode = "cover", loop, transition, forwardedRef, onLayout, pointerEvents, hitSlop, onStartShouldSetResponder, onStartShouldSetResponderCapture, onMoveShouldSetResponder, onMoveShouldSetResponderCapture, onResponderGrant, onResponderReject, onResponderStart, onResponderMove, onResponderEnd, onResponderRelease, onResponderTerminationRequest, onResponderTerminate, onTouchStart, onTouchMove, onTouchEnd, onTouchCancel, onTouchEndCapture, ...viewProps }) {
|
|
145
|
+
function FastImageBase({ source, defaultSource, tintColor, onLoadStart, onProgress, onLoad, onError, onLoadEnd, style, fallback, children, objectFit, resizeMode = "cover", loop, transition, forwardedRef, onLayout, pointerEvents, hitSlop, onStartShouldSetResponder, onStartShouldSetResponderCapture, onMoveShouldSetResponder, onMoveShouldSetResponderCapture, onResponderGrant, onResponderReject, onResponderStart, onResponderMove, onResponderEnd, onResponderRelease, onResponderTerminationRequest, onResponderTerminate, onTouchStart, onTouchMove, onTouchEnd, onTouchCancel, onTouchEndCapture, ...viewProps }) {
|
|
115
146
|
const { onClick, ...props } = viewProps;
|
|
116
147
|
const fallbackResult = (0, react.useRef)(void 0);
|
|
117
148
|
const wrapperProps = {
|
|
@@ -141,7 +172,8 @@ function FastImageBase({ source, defaultSource, tintColor, onLoadStart, onProgre
|
|
|
141
172
|
...props,
|
|
142
173
|
pointerEvents: pointerEvents === "box-none" ? "none" : void 0
|
|
143
174
|
};
|
|
144
|
-
const resolvedTintColor = tintColor != null ? tintColor :
|
|
175
|
+
const resolvedTintColor = tintColor != null ? tintColor : fromStyle(style, "tintColor");
|
|
176
|
+
const mode = NATIVE_RESIZE_MODE[resolveObjectFit(objectFit, style, resizeMode)];
|
|
145
177
|
if (fallback) {
|
|
146
178
|
const cleanedSource = typeof source === "number" ? source : Array.isArray(source) ? source.map(withoutCache) : withoutCache(source);
|
|
147
179
|
const resolvedSource = react_native.Image.resolveAssetSource(cleanedSource);
|
|
@@ -179,7 +211,7 @@ function FastImageBase({ source, defaultSource, tintColor, onLoadStart, onProgre
|
|
|
179
211
|
error: "Failed to load the image"
|
|
180
212
|
});
|
|
181
213
|
}),
|
|
182
|
-
resizeMode
|
|
214
|
+
resizeMode: mode === "none" || mode === "scale-down" ? "center" : mode
|
|
183
215
|
}), children);
|
|
184
216
|
}
|
|
185
217
|
const sources = Array.isArray(source) ? source.length === 1 ? void 0 : source : void 0;
|
|
@@ -205,7 +237,7 @@ function FastImageBase({ source, defaultSource, tintColor, onLoadStart, onProgre
|
|
|
205
237
|
onFastImageLoad: onLoad,
|
|
206
238
|
onFastImageError: onError,
|
|
207
239
|
onFastImageLoadEnd: onLoadEnd && ((event) => onLoadEnd(loadResult(event.nativeEvent))),
|
|
208
|
-
resizeMode
|
|
240
|
+
resizeMode: mode
|
|
209
241
|
}), children);
|
|
210
242
|
}
|
|
211
243
|
const FastImageMemo = (0, react.memo)(FastImageBase);
|
package/dist/index.cjs.js.flow
CHANGED
|
@@ -49,6 +49,7 @@ export type CacheControl = $ReadOnly<{|
|
|
|
49
49
|
|}>
|
|
50
50
|
|
|
51
51
|
export type ResizeModes = $Values<ResizeMode>
|
|
52
|
+
export type ObjectFit = 'fill' | 'contain' | 'cover' | 'none' | 'scale-down'
|
|
52
53
|
export type Priorities = $Values<Priority>
|
|
53
54
|
export type CacheControls = $Values<CacheControl>
|
|
54
55
|
|
|
@@ -123,6 +124,8 @@ export type FastImageProps = $ReadOnly<{|
|
|
|
123
124
|
defaultSource?: ?number,
|
|
124
125
|
|
|
125
126
|
tintColor?: number | string,
|
|
127
|
+
objectFit?: ?ObjectFit,
|
|
128
|
+
// Deprecated: objectFit, except for 'repeat'.
|
|
126
129
|
resizeMode?: ?ResizeModes,
|
|
127
130
|
fallback?: ?boolean,
|
|
128
131
|
recyclingKey?: ?string,
|
package/dist/index.cjs.web.js
CHANGED
|
@@ -27,6 +27,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
27
27
|
let react = require("react");
|
|
28
28
|
react = __toESM(react);
|
|
29
29
|
let react_native = require("react-native");
|
|
30
|
+
let react_native_web_dist_modules_AssetRegistry = require("react-native-web/dist/modules/AssetRegistry");
|
|
30
31
|
//#region src/constants.ts
|
|
31
32
|
const resizeMode = {
|
|
32
33
|
contain: "contain",
|
|
@@ -46,142 +47,279 @@ const cacheControl = {
|
|
|
46
47
|
cacheOnly: "cacheOnly"
|
|
47
48
|
};
|
|
48
49
|
//#endregion
|
|
50
|
+
//#region src/objectFit.ts
|
|
51
|
+
function fromStyle(style, key) {
|
|
52
|
+
if (Array.isArray(style)) {
|
|
53
|
+
for (let i = style.length - 1; i >= 0; i--) {
|
|
54
|
+
const found = fromStyle(style[i], key);
|
|
55
|
+
if (found !== void 0) return found;
|
|
56
|
+
}
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (typeof style === "number") {
|
|
60
|
+
const flattened = react_native.StyleSheet.flatten(style);
|
|
61
|
+
return flattened ? flattened[key] : void 0;
|
|
62
|
+
}
|
|
63
|
+
return style && typeof style === "object" ? style[key] : void 0;
|
|
64
|
+
}
|
|
65
|
+
const RESIZE_MODE_FIT = {
|
|
66
|
+
contain: "contain",
|
|
67
|
+
cover: "cover",
|
|
68
|
+
stretch: "fill",
|
|
69
|
+
center: "center",
|
|
70
|
+
repeat: "repeat"
|
|
71
|
+
};
|
|
72
|
+
const FITS = {
|
|
73
|
+
fill: true,
|
|
74
|
+
contain: true,
|
|
75
|
+
cover: true,
|
|
76
|
+
none: true,
|
|
77
|
+
"scale-down": true
|
|
78
|
+
};
|
|
79
|
+
function resolveObjectFit(objectFit, style, resizeMode) {
|
|
80
|
+
var _RESIZE_MODE_FIT$resi;
|
|
81
|
+
const fit = objectFit != null ? objectFit : fromStyle(style, "objectFit");
|
|
82
|
+
if (fit && FITS[fit] === true) return fit;
|
|
83
|
+
return (_RESIZE_MODE_FIT$resi = RESIZE_MODE_FIT[resizeMode]) !== null && _RESIZE_MODE_FIT$resi !== void 0 ? _RESIZE_MODE_FIT$resi : "cover";
|
|
84
|
+
}
|
|
85
|
+
//#endregion
|
|
49
86
|
//#region src/index.web.tsx
|
|
87
|
+
var _useId;
|
|
50
88
|
const notSupported = {
|
|
51
89
|
ok: false,
|
|
52
90
|
error: "Not supported on the web"
|
|
53
91
|
};
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
const
|
|
61
|
-
const
|
|
92
|
+
let ids = 0;
|
|
93
|
+
const useCounterId = () => (0, react.useState)(() => `${++ids}`)[0];
|
|
94
|
+
const useSvgId = (_useId = react.default.useId) !== null && _useId !== void 0 ? _useId : useCounterId;
|
|
95
|
+
const useClientLayoutEffect = globalThis.document === void 0 ? react.useEffect : react.useLayoutEffect;
|
|
96
|
+
function FastImageBase({ source, defaultSource, tintColor, blurRadius, onLoadStart, onLoad, onError, onLoadEnd, style, children, objectFit, resizeMode: mode = "cover", forwardedRef, onLayout, pointerEvents, fallback: _fallback, recyclingKey: _recyclingKey, loop: _loop, imageRendering: _imageRendering, paused: _paused, transition: _transition, downsample: _downsample, onProgress: _onProgress, ...props }) {
|
|
97
|
+
const { accessibilityLabel, "aria-label": ariaLabel, ...viewProps } = props;
|
|
98
|
+
const label = ariaLabel || accessibilityLabel || void 0;
|
|
99
|
+
const several = Array.isArray(source) && source.length > 1 ? source : void 0;
|
|
100
|
+
const sized = several ? sizedSources(several) : void 0;
|
|
101
|
+
const src = sized ? sized.src : resolveUri(source);
|
|
102
|
+
const srcSet = sized === null || sized === void 0 ? void 0 : sized.srcSet;
|
|
103
|
+
const key = srcSet !== null && srcSet !== void 0 ? srcSet : src;
|
|
104
|
+
const defaultUri = resolveUri(defaultSource);
|
|
105
|
+
const [result, setResult] = (0, react.useState)();
|
|
106
|
+
const ok = result && result.key === key ? result.ok : void 0;
|
|
107
|
+
const tintId = `fast-image-tint-${useSvgId().replace(/[^\w-]/g, "")}`;
|
|
108
|
+
const resolved = resolveObjectFit(objectFit, style, mode);
|
|
109
|
+
const fit = resolved === "center" ? "scale-down" : several && resolved === "repeat" ? "cover" : resolved;
|
|
110
|
+
const filter = [blurRadius ? `blur(${blurRadius}px)` : "", tintColor ? `url(#${tintId})` : ""].filter(Boolean).join(" ") || void 0;
|
|
111
|
+
const showDefault = !!defaultUri && ok !== true;
|
|
112
|
+
useClientLayoutEffect(() => {
|
|
113
|
+
if (key) onLoadStart === null || onLoadStart === void 0 || onLoadStart();
|
|
114
|
+
}, [key]);
|
|
62
115
|
return /* @__PURE__ */ react.default.createElement(react_native.View, {
|
|
116
|
+
...viewProps,
|
|
117
|
+
"aria-label": label,
|
|
63
118
|
style: [styles.container, style],
|
|
64
119
|
onLayout,
|
|
65
120
|
pointerEvents,
|
|
66
121
|
ref: forwardedRef
|
|
67
|
-
},
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
onLoad === null || onLoad === void 0 || onLoad({ nativeEvent: {
|
|
97
|
-
width,
|
|
98
|
-
height
|
|
99
|
-
} });
|
|
122
|
+
}, defaultUri && showDefault ? /* @__PURE__ */ react.default.createElement(Picture, {
|
|
123
|
+
key: `default ${defaultUri}`,
|
|
124
|
+
src: defaultUri,
|
|
125
|
+
fit,
|
|
126
|
+
filter,
|
|
127
|
+
alt: key ? "" : label !== null && label !== void 0 ? label : ""
|
|
128
|
+
}) : null, key && src ? /* @__PURE__ */ react.default.createElement(Picture, {
|
|
129
|
+
key: several ? "sizes" : key,
|
|
130
|
+
src,
|
|
131
|
+
srcSet,
|
|
132
|
+
lazy: !!several,
|
|
133
|
+
fit,
|
|
134
|
+
filter,
|
|
135
|
+
hidden: ok === false,
|
|
136
|
+
alt: label !== null && label !== void 0 ? label : "",
|
|
137
|
+
onLoad: (image) => {
|
|
138
|
+
const { width, height } = loadedSize(image, sized);
|
|
139
|
+
setResult({
|
|
140
|
+
key,
|
|
141
|
+
ok: true
|
|
142
|
+
});
|
|
143
|
+
onLoad === null || onLoad === void 0 || onLoad({ nativeEvent: {
|
|
144
|
+
width,
|
|
145
|
+
height
|
|
146
|
+
} });
|
|
147
|
+
onLoadEnd === null || onLoadEnd === void 0 || onLoadEnd({
|
|
148
|
+
ok: true,
|
|
149
|
+
width,
|
|
150
|
+
height
|
|
100
151
|
});
|
|
101
152
|
},
|
|
102
|
-
onError: (
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
153
|
+
onError: (uri) => {
|
|
154
|
+
const error = `Failed to load resource ${uri}`;
|
|
155
|
+
setResult({
|
|
156
|
+
key,
|
|
157
|
+
ok: false
|
|
158
|
+
});
|
|
159
|
+
onError === null || onError === void 0 || onError({ nativeEvent: { error } });
|
|
160
|
+
onLoadEnd === null || onLoadEnd === void 0 || onLoadEnd({
|
|
106
161
|
ok: false,
|
|
107
162
|
error
|
|
108
|
-
};
|
|
109
|
-
sent.current = Promise.resolve();
|
|
110
|
-
onError === null || onError === void 0 || onError({ nativeEvent: { error } });
|
|
111
|
-
},
|
|
112
|
-
onLoadEnd: () => {
|
|
113
|
-
sent.current.then(() => {
|
|
114
|
-
var _result$current;
|
|
115
|
-
onLoadEnd === null || onLoadEnd === void 0 || onLoadEnd((_result$current = result.current) !== null && _result$current !== void 0 ? _result$current : {
|
|
116
|
-
ok: false,
|
|
117
|
-
error: "Failed to load the image"
|
|
118
|
-
});
|
|
119
|
-
result.current = void 0;
|
|
120
163
|
});
|
|
121
164
|
}
|
|
122
|
-
}),
|
|
165
|
+
}) : null, tintColor ? /* @__PURE__ */ react.default.createElement(TintFilter, {
|
|
166
|
+
id: tintId,
|
|
167
|
+
color: tintColor
|
|
168
|
+
}) : null, children);
|
|
169
|
+
}
|
|
170
|
+
function resolveUri(source) {
|
|
171
|
+
let uri;
|
|
172
|
+
if (typeof source === "number") {
|
|
173
|
+
var _asset$scales$;
|
|
174
|
+
const asset = (0, react_native_web_dist_modules_AssetRegistry.getAssetByID)(source);
|
|
175
|
+
if (!asset) return void 0;
|
|
176
|
+
const scale = asset.scales.reduce((best, scale) => Math.abs(scale - react_native.PixelRatio.get()) < Math.abs(best - react_native.PixelRatio.get()) ? scale : best, (_asset$scales$ = asset.scales[0]) !== null && _asset$scales$ !== void 0 ? _asset$scales$ : 1);
|
|
177
|
+
const suffix = scale !== 1 ? `@${scale}x` : "";
|
|
178
|
+
uri = `${asset.httpServerLocation}/${asset.name}${suffix}.${asset.type}`;
|
|
179
|
+
} else if (typeof source === "string") uri = source;
|
|
180
|
+
else if (Array.isArray(source)) return resolveUri(source[0]);
|
|
181
|
+
else if (source && typeof source.uri === "string") uri = source.uri;
|
|
182
|
+
const svg = uri === null || uri === void 0 ? void 0 : uri.match(/^(data:image\/svg\+xml;utf8,)(.*)/);
|
|
183
|
+
return svg ? svg[1] + encodeURIComponent(svg[2]) : uri || void 0;
|
|
123
184
|
}
|
|
124
|
-
function
|
|
185
|
+
function sizedSources(sources) {
|
|
125
186
|
var _largest$uri, _sources$find;
|
|
126
|
-
const
|
|
187
|
+
const widths = sources.filter((source) => (source === null || source === void 0 ? void 0 : source.uri) && source.width).map((source) => {
|
|
127
188
|
var _source$width, _source$scale;
|
|
128
189
|
return {
|
|
129
190
|
uri: source.uri,
|
|
130
191
|
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
192
|
};
|
|
132
193
|
});
|
|
133
|
-
const largest =
|
|
134
|
-
|
|
135
|
-
|
|
194
|
+
const largest = widths.reduce((best, source) => !best || source.width > best.width ? source : best, void 0);
|
|
195
|
+
return {
|
|
196
|
+
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,
|
|
197
|
+
srcSet: widths.length > 0 ? widths.map((source) => `${source.uri} ${source.width}w`).join(", ") : void 0,
|
|
198
|
+
widths
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
function loadedSize(image, sized) {
|
|
202
|
+
var _picked$width;
|
|
203
|
+
const picked = (sized === null || sized === void 0 ? void 0 : sized.srcSet) ? sized.widths.find((source) => new URL(source.uri, image.currentSrc).href === image.currentSrc) : void 0;
|
|
204
|
+
return {
|
|
205
|
+
width: (_picked$width = picked === null || picked === void 0 ? void 0 : picked.width) !== null && _picked$width !== void 0 ? _picked$width : image.naturalWidth,
|
|
206
|
+
height: picked ? Math.round(image.naturalHeight * picked.width / image.naturalWidth) : image.naturalHeight
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
const fill = {
|
|
210
|
+
position: "absolute",
|
|
211
|
+
top: 0,
|
|
212
|
+
left: 0,
|
|
213
|
+
width: "100%",
|
|
214
|
+
height: "100%"
|
|
215
|
+
};
|
|
216
|
+
function Picture({ src, srcSet, lazy, fit, filter, hidden, alt, onLoad, onError }) {
|
|
217
|
+
const repeat = fit === "repeat";
|
|
218
|
+
const image = (0, react.useRef)(null);
|
|
219
|
+
const tiles = (0, react.useRef)(null);
|
|
220
|
+
const mounted = (0, react.useRef)(true);
|
|
221
|
+
const handled = (0, react.useRef)(void 0);
|
|
222
|
+
const [natural, setNatural] = (0, react.useState)();
|
|
223
|
+
const [box, setBox] = (0, react.useState)();
|
|
224
|
+
const loaded = (target) => {
|
|
225
|
+
var _handled$current, _target$decode;
|
|
226
|
+
const file = target.currentSrc || src;
|
|
227
|
+
const sources = srcSet !== null && srcSet !== void 0 ? srcSet : src;
|
|
228
|
+
if (!mounted.current || ((_handled$current = handled.current) === null || _handled$current === void 0 ? void 0 : _handled$current.sources) === sources && handled.current.file === file) return;
|
|
229
|
+
handled.current = {
|
|
230
|
+
sources,
|
|
231
|
+
file
|
|
232
|
+
};
|
|
233
|
+
setNatural({
|
|
234
|
+
width: target.naturalWidth,
|
|
235
|
+
height: target.naturalHeight
|
|
236
|
+
});
|
|
237
|
+
const done = () => {
|
|
238
|
+
if (mounted.current) onLoad === null || onLoad === void 0 || onLoad(target);
|
|
239
|
+
};
|
|
240
|
+
Promise.resolve((_target$decode = target.decode) === null || _target$decode === void 0 ? void 0 : _target$decode.call(target)).then(done, done);
|
|
241
|
+
};
|
|
136
242
|
(0, react.useEffect)(() => {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
243
|
+
mounted.current = true;
|
|
244
|
+
return () => {
|
|
245
|
+
mounted.current = false;
|
|
246
|
+
};
|
|
247
|
+
}, []);
|
|
248
|
+
useClientLayoutEffect(() => {
|
|
249
|
+
var _target$decode2;
|
|
250
|
+
const target = image.current;
|
|
251
|
+
if (!(target === null || target === void 0 ? void 0 : target.complete)) return;
|
|
252
|
+
if (target.naturalWidth > 0) return loaded(target);
|
|
253
|
+
Promise.resolve((_target$decode2 = target.decode) === null || _target$decode2 === void 0 ? void 0 : _target$decode2.call(target)).then(() => loaded(target), () => {
|
|
254
|
+
if (mounted.current) onError === null || onError === void 0 || onError(target.currentSrc || src);
|
|
255
|
+
});
|
|
256
|
+
}, []);
|
|
257
|
+
useClientLayoutEffect(() => {
|
|
258
|
+
const Observer = globalThis.ResizeObserver;
|
|
259
|
+
const node = tiles.current;
|
|
260
|
+
if (!repeat || !node || !Observer) return;
|
|
261
|
+
const observer = new Observer((entries) => {
|
|
262
|
+
const { width, height } = entries[0].contentRect;
|
|
263
|
+
setBox({
|
|
264
|
+
width,
|
|
265
|
+
height
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
observer.observe(node);
|
|
269
|
+
return () => observer.disconnect();
|
|
270
|
+
}, [repeat]);
|
|
271
|
+
const img = /* @__PURE__ */ react.default.createElement("img", {
|
|
272
|
+
ref: image,
|
|
140
273
|
src,
|
|
141
274
|
srcSet,
|
|
142
275
|
sizes: srcSet ? "auto, 100vw" : void 0,
|
|
143
|
-
loading: "lazy",
|
|
144
|
-
alt
|
|
276
|
+
loading: lazy ? "lazy" : void 0,
|
|
277
|
+
alt,
|
|
145
278
|
draggable: false,
|
|
146
279
|
style: {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
filter: blurRadius ? `blur(${blurRadius}px)` : void 0
|
|
280
|
+
...fill,
|
|
281
|
+
objectFit: repeat ? "cover" : fit,
|
|
282
|
+
filter: repeat ? void 0 : filter,
|
|
283
|
+
opacity: repeat || hidden ? 0 : void 0
|
|
152
284
|
},
|
|
153
|
-
onLoad: (event) =>
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
|
|
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
|
-
});
|
|
285
|
+
onLoad: (event) => loaded(event.currentTarget),
|
|
286
|
+
onError: (event) => {
|
|
287
|
+
const target = event.currentTarget;
|
|
288
|
+
if (mounted.current) onError === null || onError === void 0 || onError((target === null || target === void 0 ? void 0 : target.currentSrc) || src);
|
|
176
289
|
}
|
|
177
290
|
});
|
|
291
|
+
return /* @__PURE__ */ react.default.createElement(react.default.Fragment, null, repeat ? /* @__PURE__ */ react.default.createElement("div", {
|
|
292
|
+
ref: tiles,
|
|
293
|
+
style: {
|
|
294
|
+
...fill,
|
|
295
|
+
backgroundImage: `url("${src}")`,
|
|
296
|
+
backgroundRepeat: "repeat",
|
|
297
|
+
backgroundPosition: "0",
|
|
298
|
+
backgroundSize: tileSize(natural, box),
|
|
299
|
+
filter,
|
|
300
|
+
opacity: hidden ? 0 : void 0
|
|
301
|
+
}
|
|
302
|
+
}) : null, img);
|
|
178
303
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
304
|
+
function tileSize(natural, box) {
|
|
305
|
+
if (!(natural === null || natural === void 0 ? void 0 : natural.width) || !natural.height || !(box === null || box === void 0 ? void 0 : box.width) || !box.height) return;
|
|
306
|
+
const scale = Math.min(1, box.width / natural.width, box.height / natural.height);
|
|
307
|
+
return `${Math.ceil(scale * natural.width)}px ${Math.ceil(scale * natural.height)}px`;
|
|
308
|
+
}
|
|
309
|
+
function TintFilter({ id, color }) {
|
|
310
|
+
return /* @__PURE__ */ react.default.createElement("svg", { style: tintStyle }, /* @__PURE__ */ react.default.createElement("defs", null, /* @__PURE__ */ react.default.createElement("filter", { id }, /* @__PURE__ */ react.default.createElement("feFlood", {
|
|
311
|
+
floodColor: String(color),
|
|
312
|
+
key: String(color)
|
|
313
|
+
}), /* @__PURE__ */ react.default.createElement("feComposite", {
|
|
314
|
+
in2: "SourceAlpha",
|
|
315
|
+
operator: "in"
|
|
316
|
+
}))));
|
|
317
|
+
}
|
|
318
|
+
const tintStyle = {
|
|
319
|
+
position: "absolute",
|
|
320
|
+
width: 0,
|
|
321
|
+
height: 0,
|
|
322
|
+
visibility: "hidden"
|
|
185
323
|
};
|
|
186
324
|
const FastImageMemo = (0, react.memo)(FastImageBase);
|
|
187
325
|
const FastImageComponent = (0, react.forwardRef)((props, ref) => /* @__PURE__ */ react.default.createElement(FastImageMemo, {
|
|
@@ -189,24 +327,6 @@ const FastImageComponent = (0, react.forwardRef)((props, ref) => /* @__PURE__ */
|
|
|
189
327
|
...props
|
|
190
328
|
}));
|
|
191
329
|
FastImageComponent.displayName = "FastImage";
|
|
192
|
-
async function loadedSize(loaded, view, uri) {
|
|
193
|
-
var _view$querySelector;
|
|
194
|
-
if (loaded === null || loaded === void 0 ? void 0 : loaded.naturalWidth) return {
|
|
195
|
-
width: loaded.naturalWidth,
|
|
196
|
-
height: loaded.naturalHeight
|
|
197
|
-
};
|
|
198
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
199
|
-
const shown = view === null || view === void 0 || (_view$querySelector = view.querySelector) === null || _view$querySelector === void 0 ? void 0 : _view$querySelector.call(view, "img");
|
|
200
|
-
if (shown === null || shown === void 0 ? void 0 : shown.naturalWidth) return {
|
|
201
|
-
width: shown.naturalWidth,
|
|
202
|
-
height: shown.naturalHeight
|
|
203
|
-
};
|
|
204
|
-
const src = uri !== null && uri !== void 0 ? uri : shown === null || shown === void 0 ? void 0 : shown.src;
|
|
205
|
-
return src ? getSize(src) : {
|
|
206
|
-
width: 0,
|
|
207
|
-
height: 0
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
330
|
function getSize(uri) {
|
|
211
331
|
return new Promise((resolve) => {
|
|
212
332
|
react_native.Image.getSize(uri, (width, height) => resolve({
|
|
@@ -244,16 +364,7 @@ FastImage.clearDiskCache = () => Promise.resolve(notSupported);
|
|
|
244
364
|
FastImage.configureCache = () => Promise.resolve({});
|
|
245
365
|
FastImage.writeToCache = () => Promise.resolve(notSupported);
|
|
246
366
|
FastImage.getCachePath = () => Promise.resolve(notSupported);
|
|
247
|
-
const styles = react_native.StyleSheet.create({
|
|
248
|
-
container: { overflow: "hidden" },
|
|
249
|
-
image: {
|
|
250
|
-
position: "absolute",
|
|
251
|
-
top: 0,
|
|
252
|
-
left: 0,
|
|
253
|
-
width: "100%",
|
|
254
|
-
height: "100%"
|
|
255
|
-
}
|
|
256
|
-
});
|
|
367
|
+
const styles = react_native.StyleSheet.create({ container: { overflow: "hidden" } });
|
|
257
368
|
const FastImageWithRef = FastImageComponent;
|
|
258
369
|
const FastImageBackground = (0, react.forwardRef)(({ style, imageStyle, imageRef, children, ...props }, ref) => /* @__PURE__ */ react.default.createElement(react_native.View, {
|
|
259
370
|
style,
|
|
@@ -49,6 +49,7 @@ export type CacheControl = $ReadOnly<{|
|
|
|
49
49
|
|}>
|
|
50
50
|
|
|
51
51
|
export type ResizeModes = $Values<ResizeMode>
|
|
52
|
+
export type ObjectFit = 'fill' | 'contain' | 'cover' | 'none' | 'scale-down'
|
|
52
53
|
export type Priorities = $Values<Priority>
|
|
53
54
|
export type CacheControls = $Values<CacheControl>
|
|
54
55
|
|
|
@@ -123,6 +124,8 @@ export type FastImageProps = $ReadOnly<{|
|
|
|
123
124
|
defaultSource?: ?number,
|
|
124
125
|
|
|
125
126
|
tintColor?: number | string,
|
|
127
|
+
objectFit?: ?ObjectFit,
|
|
128
|
+
// Deprecated: objectFit, except for 'repeat'.
|
|
126
129
|
resizeMode?: ?ResizeModes,
|
|
127
130
|
fallback?: ?boolean,
|
|
128
131
|
recyclingKey?: ?string,
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { ColorValue, View, StyleProp, ViewStyle, ImageRequireSource, AccessibilityProps, ViewProps } from 'react-native';
|
|
3
3
|
import { cacheControl, priority, resizeMode } from './constants';
|
|
4
|
+
/** How the image fits the view: see [`objectFit`](#objectfit). */
|
|
5
|
+
export type ObjectFit = 'fill' | 'contain' | 'cover' | 'none' | 'scale-down';
|
|
4
6
|
/** How the image fits the view: see [`resizeMode`](#resizemode). */
|
|
5
7
|
export type ResizeMode = 'contain' | 'cover' | 'stretch' | 'center' | 'repeat';
|
|
6
8
|
/** An image's load priority: see [`source.priority`](#sourcepriority). */
|
|
@@ -234,6 +236,7 @@ export interface OnProgressEvent {
|
|
|
234
236
|
export interface ImageStyle extends ViewStyle {
|
|
235
237
|
overlayColor?: ColorValue;
|
|
236
238
|
tintColor?: ColorValue;
|
|
239
|
+
objectFit?: ObjectFit;
|
|
237
240
|
}
|
|
238
241
|
export interface FastImageProps extends AccessibilityProps, ViewProps {
|
|
239
242
|
/**
|
|
@@ -287,22 +290,47 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
|
|
|
287
290
|
*/
|
|
288
291
|
defaultSource?: ImageRequireSource;
|
|
289
292
|
/**
|
|
290
|
-
* How the image
|
|
293
|
+
* How the image fits the view, as CSS's `object-fit` does.
|
|
291
294
|
*
|
|
292
|
-
* - `'contain'`: scales it uniformly (keeping its aspect ratio) so all of
|
|
293
|
-
* it fits in the view (minus padding).
|
|
294
295
|
* - `'cover'`: scales it uniformly (keeping its aspect ratio) so it covers
|
|
295
296
|
* the view (minus padding), cropping what doesn't fit.
|
|
296
|
-
* - `'
|
|
297
|
+
* - `'contain'`: scales it uniformly (keeping its aspect ratio) so all of
|
|
298
|
+
* it fits in the view (minus padding).
|
|
299
|
+
* - `'fill'`: scales its width and height separately to fill the view,
|
|
297
300
|
* which can change its aspect ratio.
|
|
298
|
-
* - `'
|
|
299
|
-
*
|
|
301
|
+
* - `'none'`: shows it at its own size, centered, cropped if it's larger
|
|
302
|
+
* than the view.
|
|
303
|
+
* - `'scale-down'`: shows it at its own size, centered, or scaled down
|
|
304
|
+
* uniformly to fit if it's larger than the view (the smaller of `'none'`
|
|
305
|
+
* and `'contain'`).
|
|
306
|
+
*
|
|
307
|
+
* An image's own size is its size in pixels, as points (dp on Android),
|
|
308
|
+
* as CSS counts an image's pixels: a 300 × 200 image is 300 × 200 points.
|
|
309
|
+
* A bundled image (`require()`) is its size in points.
|
|
310
|
+
*
|
|
311
|
+
* It can also be set in `style`; the prop wins. Either one overrides
|
|
312
|
+
* `resizeMode`.
|
|
313
|
+
*
|
|
314
|
+
* @default 'cover'
|
|
315
|
+
*/
|
|
316
|
+
objectFit?: ObjectFit;
|
|
317
|
+
/**
|
|
318
|
+
* How the image fills the view.
|
|
319
|
+
*
|
|
320
|
+
* - `'contain'`: as `objectFit="contain"`.
|
|
321
|
+
* - `'cover'`: as `objectFit="cover"`.
|
|
322
|
+
* - `'stretch'`: as `objectFit="fill"`.
|
|
323
|
+
* - `'center'`: as `objectFit="scale-down"`, except that on Android an
|
|
324
|
+
* image smaller than the view is shown at its size in pixels on the
|
|
325
|
+
* screen, so smaller than on iOS and the web.
|
|
300
326
|
* - `'repeat'`: repeats it to cover the view, from its top-left corner, at
|
|
301
327
|
* the image's own size in pixels (a bundled image at its size in
|
|
302
328
|
* points), scaled down to fit if it's larger than the view. An animated
|
|
303
329
|
* image repeats its first frame, and `defaultSource` repeats too.
|
|
304
330
|
*
|
|
305
331
|
* @default 'cover'
|
|
332
|
+
* @deprecated Use `objectFit` instead, except for `'repeat'`, which has no
|
|
333
|
+
* `objectFit` value and isn't deprecated.
|
|
306
334
|
*/
|
|
307
335
|
resizeMode?: ResizeMode;
|
|
308
336
|
/**
|
|
@@ -539,6 +567,10 @@ export interface PreloadFailure {
|
|
|
539
567
|
*/
|
|
540
568
|
export type PreloadResult = PreloadSuccess | PreloadFailure;
|
|
541
569
|
export interface FastImageStaticProperties {
|
|
570
|
+
/**
|
|
571
|
+
* @deprecated Use the `objectFit` prop instead of `resizeMode`, except for
|
|
572
|
+
* `'repeat'`.
|
|
573
|
+
*/
|
|
542
574
|
resizeMode: typeof resizeMode;
|
|
543
575
|
priority: typeof priority;
|
|
544
576
|
cacheControl: typeof cacheControl;
|