react-native-fast-image 9.3.7 → 10.0.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 +87 -61
- package/RNFastImage.podspec +14 -9
- package/android/build.gradle +21 -47
- package/android/consumer-rules.pro +7 -7
- package/android/src/main/AndroidManifest.xml +1 -2
- package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageAnimatable.java +1 -1
- package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageAnimated.java +2 -2
- package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageApng.java +2 -2
- package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageBlur.java +4 -6
- package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageCacheControl.java +1 -1
- package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageCacheLimits.java +4 -1
- package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageCacheWrite.java +1 -1
- package/android/src/main/java/com/fastimage/FastImageCrop.java +92 -0
- package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageEvent.java +2 -3
- package/android/src/main/java/com/fastimage/FastImageEvents.java +74 -0
- package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageGif.java +1 -1
- package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageGlide.java +3 -3
- package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageGlideModule.java +1 -1
- package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageKeyedGlideUrl.java +1 -1
- package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageOkHttpProgressGlideModule.java +36 -9
- package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageRequestListener.java +6 -1
- package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageSharedDownloads.java +1 -1
- package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageSource.java +1 -1
- package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageSourceSize.java +12 -2
- package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageSvg.java +3 -3
- package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageSvgRenderer.java +1 -1
- package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageUrl.java +1 -1
- package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageViewConverter.java +7 -8
- package/android/src/main/java/com/fastimage/FastImageViewManager.java +591 -0
- package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageViewModule.java +31 -28
- package/android/src/main/java/com/fastimage/FastImageViewPackage.java +51 -0
- package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageViewWithUrl.java +352 -126
- package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageWebGlideUrl.java +1 -1
- package/app.plugin.js +2 -7
- package/dist/index.cjs.js +276 -157
- package/dist/index.cjs.js.flow +33 -17
- package/dist/index.cjs.web.js +211 -61
- package/dist/index.cjs.web.js.flow +33 -17
- package/dist/index.d.ts +83 -58
- package/dist/index.js +278 -159
- package/dist/index.js.flow +33 -17
- package/dist/index.web.js +212 -62
- package/dist/index.web.js.flow +33 -17
- package/dist/objectFit.d.ts +2 -2
- package/dist/objectPosition.d.ts +42 -0
- package/dist/specs/FastImageViewNativeComponent.d.ts +48 -0
- package/dist/specs/NativeFastImageModule.d.ts +11 -0
- package/dist/warnings.d.ts +3 -0
- package/ios/FastImage/FFFDownsampledImage.h +4 -8
- package/ios/FastImage/FFFDownsampledImage.m +0 -34
- package/ios/FastImage/FFFastImageModule.h +8 -0
- package/ios/FastImage/{FFFastImageViewManager.m → FFFastImageModule.mm} +70 -59
- package/ios/FastImage/FFFastImageView.h +36 -7
- package/ios/FastImage/FFFastImageView.m +201 -164
- package/ios/FastImage/FFFastImageViewComponentView.h +11 -0
- package/ios/FastImage/FFFastImageViewComponentView.mm +325 -0
- package/ios/FastImage/RCTConvert+FFFastImage.h +1 -1
- package/ios/FastImage/RCTConvert+FFFastImage.m +7 -10
- package/package.json +28 -9
- package/src/constants.ts +28 -0
- package/src/index.js.flow +179 -0
- package/src/index.tsx +1103 -0
- package/src/index.web.tsx +548 -0
- package/src/objectFit.ts +48 -0
- package/src/objectPosition.ts +243 -0
- package/src/specs/FastImageViewNativeComponent.ts +83 -0
- package/src/specs/NativeFastImageModule.ts +35 -0
- package/src/warnings.ts +37 -0
- package/android/src/main/AndroidManifestNew.xml +0 -3
- package/android/src/main/java/com/dylanvann/fastimage/FastImageCookieHandler.java +0 -63
- package/android/src/main/java/com/dylanvann/fastimage/FastImageEvents.java +0 -49
- package/android/src/main/java/com/dylanvann/fastimage/FastImageProgressListener.java +0 -18
- package/android/src/main/java/com/dylanvann/fastimage/FastImageShadowNode.java +0 -21
- package/android/src/main/java/com/dylanvann/fastimage/FastImageViewManager.java +0 -379
- package/android/src/main/java/com/dylanvann/fastimage/FastImageViewPackage.java +0 -27
- package/ios/FastImage/FFFastImageViewManager.h +0 -5
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
// FastImage on the web (react-native-web): a View with an <img> in it, with
|
|
2
|
+
// FastImage's props and events. Bundlers that resolve `.web` files, or the
|
|
3
|
+
// package's `browser` field, pick this file instead of index.tsx, which uses
|
|
4
|
+
// the native views.
|
|
5
|
+
//
|
|
6
|
+
// The View (a div) has FastImage's style, ref and View props. The <img> fills
|
|
7
|
+
// it inside its borders, with object-fit for objectFit (see Picture).
|
|
8
|
+
// defaultSource is a second <img> under it until the image has loaded (the
|
|
9
|
+
// browser draws the image over it as it arrives), and tintColor is an SVG
|
|
10
|
+
// filter next to them.
|
|
11
|
+
//
|
|
12
|
+
// Works: source (uri, require(), or several sizes: see sizedSources),
|
|
13
|
+
// defaultSource, objectFit, objectPosition, tintColor, blurRadius, style,
|
|
14
|
+
// onLoadStart, onLoad, onError, onLoadEnd, and View props (testID,
|
|
15
|
+
// accessibility, onLayout, pointerEvents). The native-only props (cache,
|
|
16
|
+
// priority, headers, transition, downsample, loop, paused, imageRendering,
|
|
17
|
+
// recyclingKey) and onProgress are ignored.
|
|
18
|
+
import React, {
|
|
19
|
+
forwardRef,
|
|
20
|
+
memo,
|
|
21
|
+
useEffect,
|
|
22
|
+
useLayoutEffect,
|
|
23
|
+
useRef,
|
|
24
|
+
useState,
|
|
25
|
+
} from 'react'
|
|
26
|
+
import { Image, PixelRatio, StyleSheet, View } from 'react-native'
|
|
27
|
+
// @ts-expect-error react-native-web has no type declarations.
|
|
28
|
+
import { getAssetByID } from 'react-native-web/dist/modules/AssetRegistry'
|
|
29
|
+
import { cacheControl, priority, resizeMode } from './constants'
|
|
30
|
+
import { resolveObjectFit } from './objectFit'
|
|
31
|
+
import { cssPosition, objectPositionOrCentered } from './objectPosition'
|
|
32
|
+
import { warnIfChildren } from './warnings'
|
|
33
|
+
import type {
|
|
34
|
+
CachePathResult,
|
|
35
|
+
CacheState,
|
|
36
|
+
FastImageBackgroundProps,
|
|
37
|
+
FastImageProps,
|
|
38
|
+
FastImageStaticProperties,
|
|
39
|
+
ObjectFit,
|
|
40
|
+
PreloadResult,
|
|
41
|
+
Source,
|
|
42
|
+
} from './index'
|
|
43
|
+
|
|
44
|
+
export type * from './index'
|
|
45
|
+
|
|
46
|
+
// The native versions never reject: these resolve with an error.
|
|
47
|
+
const notSupported: CachePathResult = {
|
|
48
|
+
ok: false,
|
|
49
|
+
error: 'Not supported on the web',
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// An id that's the same on the server and in the browser: React's useId,
|
|
53
|
+
// from React 18 (this package's React types are older), without the
|
|
54
|
+
// characters an SVG id can't have (:r0: in React 18, «r0» in React 19). On
|
|
55
|
+
// React 17, a counter's (which can differ from the server's).
|
|
56
|
+
let ids = 0
|
|
57
|
+
const useCounterId = () => useState(() => `${++ids}`)[0]
|
|
58
|
+
const useSvgId: () => string =
|
|
59
|
+
(React as unknown as { useId?: () => string }).useId ?? useCounterId
|
|
60
|
+
|
|
61
|
+
// A layout effect in the browser (it runs before the image can show), and an
|
|
62
|
+
// effect when rendering on a server, where layout effects warn.
|
|
63
|
+
const useClientLayoutEffect =
|
|
64
|
+
(globalThis as { document?: unknown }).document === undefined
|
|
65
|
+
? useEffect
|
|
66
|
+
: useLayoutEffect
|
|
67
|
+
|
|
68
|
+
// Metro's (and React Native's Jest preset's) development flag; other web
|
|
69
|
+
// bundlers may not define it.
|
|
70
|
+
declare const __DEV__: boolean | undefined
|
|
71
|
+
|
|
72
|
+
function FastImageBase({
|
|
73
|
+
source,
|
|
74
|
+
defaultSource,
|
|
75
|
+
tintColor,
|
|
76
|
+
blurRadius,
|
|
77
|
+
onLoadStart,
|
|
78
|
+
onLoad,
|
|
79
|
+
onError,
|
|
80
|
+
onLoadEnd,
|
|
81
|
+
style,
|
|
82
|
+
children,
|
|
83
|
+
objectFit,
|
|
84
|
+
objectPosition,
|
|
85
|
+
forwardedRef,
|
|
86
|
+
// On the wrapper, the view the layout and touches are for here (on
|
|
87
|
+
// native, FastImage's only view).
|
|
88
|
+
onLayout,
|
|
89
|
+
pointerEvents,
|
|
90
|
+
// Native only.
|
|
91
|
+
recyclingKey: _recyclingKey,
|
|
92
|
+
loop: _loop,
|
|
93
|
+
imageRendering: _imageRendering,
|
|
94
|
+
paused: _paused,
|
|
95
|
+
transition: _transition,
|
|
96
|
+
downsample: _downsample,
|
|
97
|
+
onProgress: _onProgress,
|
|
98
|
+
...props
|
|
99
|
+
}: FastImageProps & { forwardedRef: React.Ref<any> }) {
|
|
100
|
+
// As on native (a single view there): children aren't rendered.
|
|
101
|
+
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
102
|
+
warnIfChildren(children)
|
|
103
|
+
}
|
|
104
|
+
// The label is the wrapper's aria-label, as react-native-web's Image gave
|
|
105
|
+
// it (its View warns that accessibilityLabel is deprecated), and the
|
|
106
|
+
// <img>'s alt text.
|
|
107
|
+
const {
|
|
108
|
+
accessibilityLabel,
|
|
109
|
+
'aria-label': ariaLabel,
|
|
110
|
+
// Removed in 10.0 (objectFit instead): ignored.
|
|
111
|
+
resizeMode: _resizeMode,
|
|
112
|
+
...viewProps
|
|
113
|
+
} = props as typeof props & { 'aria-label'?: string; resizeMode?: unknown }
|
|
114
|
+
const label = ariaLabel || accessibilityLabel || undefined
|
|
115
|
+
const several =
|
|
116
|
+
Array.isArray(source) && source.length > 1 ? source : undefined
|
|
117
|
+
const sized = several ? sizedSources(several) : undefined
|
|
118
|
+
const src = sized ? sized.src : resolveUri(source)
|
|
119
|
+
const srcSet = sized?.srcSet
|
|
120
|
+
// What the <img> loads. Another image gets a new <img>, so the previous
|
|
121
|
+
// one doesn't stay while it loads; with several sizes, the <img> stays,
|
|
122
|
+
// and the browser shows the previous image until the new one has loaded.
|
|
123
|
+
const key = srcSet ?? src
|
|
124
|
+
const defaultUri = resolveUri(defaultSource)
|
|
125
|
+
// Whether the image for `key` loaded, or failed.
|
|
126
|
+
const [result, setResult] = useState<{ key: string; ok: boolean }>()
|
|
127
|
+
const ok = result && result.key === key ? result.ok : undefined
|
|
128
|
+
// The same on the server and in the browser. React's ids have characters
|
|
129
|
+
// an SVG id can't (:r0: in React 18, «r0» in React 19).
|
|
130
|
+
const tintId = `fast-image-tint-${useSvgId().replace(/[^\w-]/g, '')}`
|
|
131
|
+
// objectFit (the prop, then style's).
|
|
132
|
+
const fit = resolveObjectFit(objectFit, style)
|
|
133
|
+
const position = cssPosition(objectPositionOrCentered(objectPosition))
|
|
134
|
+
const filter =
|
|
135
|
+
[
|
|
136
|
+
blurRadius ? `blur(${blurRadius}px)` : '',
|
|
137
|
+
tintColor ? `url(#${tintId})` : '',
|
|
138
|
+
]
|
|
139
|
+
.filter(Boolean)
|
|
140
|
+
.join(' ') || undefined
|
|
141
|
+
// defaultSource shows until the image has loaded, and if it fails.
|
|
142
|
+
const showDefault = !!defaultUri && ok !== true
|
|
143
|
+
|
|
144
|
+
useClientLayoutEffect(() => {
|
|
145
|
+
if (key) onLoadStart?.()
|
|
146
|
+
// Once per image the browser is asked for.
|
|
147
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
148
|
+
}, [key])
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<View
|
|
152
|
+
{...viewProps}
|
|
153
|
+
{...({ 'aria-label': label } as {})}
|
|
154
|
+
style={[styles.container, style]}
|
|
155
|
+
onLayout={onLayout}
|
|
156
|
+
pointerEvents={pointerEvents}
|
|
157
|
+
ref={forwardedRef}
|
|
158
|
+
>
|
|
159
|
+
{defaultUri && showDefault ? (
|
|
160
|
+
<Picture
|
|
161
|
+
key={`default ${defaultUri}`}
|
|
162
|
+
src={defaultUri}
|
|
163
|
+
fit={fit}
|
|
164
|
+
position={position}
|
|
165
|
+
filter={filter}
|
|
166
|
+
// The image has the label, if there is one.
|
|
167
|
+
alt={key ? '' : (label ?? '')}
|
|
168
|
+
/>
|
|
169
|
+
) : null}
|
|
170
|
+
{key && src ? (
|
|
171
|
+
<Picture
|
|
172
|
+
key={several ? 'sizes' : key}
|
|
173
|
+
src={src}
|
|
174
|
+
srcSet={srcSet}
|
|
175
|
+
lazy={!!several}
|
|
176
|
+
fit={fit}
|
|
177
|
+
position={position}
|
|
178
|
+
filter={filter}
|
|
179
|
+
// Shown as it loads, over defaultSource, and not if it
|
|
180
|
+
// failed (no broken image icon or alt text).
|
|
181
|
+
hidden={ok === false}
|
|
182
|
+
alt={label ?? ''}
|
|
183
|
+
onLoad={(image) => {
|
|
184
|
+
const { width, height } = loadedSize(image, sized)
|
|
185
|
+
setResult({ key, ok: true })
|
|
186
|
+
onLoad?.({ nativeEvent: { width, height } })
|
|
187
|
+
onLoadEnd?.({ ok: true, width, height })
|
|
188
|
+
}}
|
|
189
|
+
onError={(uri) => {
|
|
190
|
+
const error = `Failed to load resource ${uri}`
|
|
191
|
+
setResult({ key, ok: false })
|
|
192
|
+
onError?.({ nativeEvent: { error } })
|
|
193
|
+
onLoadEnd?.({ ok: false, error })
|
|
194
|
+
}}
|
|
195
|
+
/>
|
|
196
|
+
) : null}
|
|
197
|
+
{tintColor ? <TintFilter id={tintId} color={tintColor} /> : null}
|
|
198
|
+
</View>
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// A source's (or defaultSource's) uri. Expo's web builds make a require()d
|
|
203
|
+
// image a { uri, width, height } object (or a string). Other setups can make
|
|
204
|
+
// it a number in react-native-web's asset registry, resolved as its Image
|
|
205
|
+
// does: the file for the scale closest to the screen's.
|
|
206
|
+
function resolveUri(source: unknown): string | undefined {
|
|
207
|
+
let uri: string | undefined
|
|
208
|
+
if (typeof source === 'number') {
|
|
209
|
+
const asset: PackagerAsset | undefined = getAssetByID(source)
|
|
210
|
+
if (!asset) return undefined
|
|
211
|
+
const scale = asset.scales.reduce(
|
|
212
|
+
(best, scale) =>
|
|
213
|
+
Math.abs(scale - PixelRatio.get()) <
|
|
214
|
+
Math.abs(best - PixelRatio.get())
|
|
215
|
+
? scale
|
|
216
|
+
: best,
|
|
217
|
+
asset.scales[0] ?? 1,
|
|
218
|
+
)
|
|
219
|
+
const suffix = scale !== 1 ? `@${scale}x` : ''
|
|
220
|
+
uri = `${asset.httpServerLocation}/${asset.name}${suffix}.${asset.type}`
|
|
221
|
+
} else if (typeof source === 'string') {
|
|
222
|
+
uri = source
|
|
223
|
+
} else if (Array.isArray(source)) {
|
|
224
|
+
// An array of one is that source.
|
|
225
|
+
return resolveUri(source[0])
|
|
226
|
+
} else if (source && typeof (source as Source).uri === 'string') {
|
|
227
|
+
uri = (source as Source).uri
|
|
228
|
+
}
|
|
229
|
+
// An SVG's markup in a data uri, escaped: a # in it would end the uri.
|
|
230
|
+
const svg = uri?.match(/^(data:image\/svg\+xml;utf8,)(.*)/)
|
|
231
|
+
return svg ? svg[1] + encodeURIComponent(svg[2]) : uri || undefined
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// An image in react-native-web's asset registry.
|
|
235
|
+
type PackagerAsset = {
|
|
236
|
+
httpServerLocation: string
|
|
237
|
+
name: string
|
|
238
|
+
type: string
|
|
239
|
+
scales: number[]
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Several sizes of an image, as a srcset (each size's width, times its
|
|
243
|
+
// scale), from which the browser loads the one for the image's width in
|
|
244
|
+
// device pixels, usually the smallest at least as wide, as expo-image does.
|
|
245
|
+
// sizes="auto" has the browser use the width the image is laid out at; it
|
|
246
|
+
// only applies to lazy images, and browsers without it use the next value,
|
|
247
|
+
// 100vw (the viewport's width). src is the largest, for browsers without
|
|
248
|
+
// srcset.
|
|
249
|
+
function sizedSources(sources: Source[]) {
|
|
250
|
+
const widths = sources
|
|
251
|
+
.filter((source) => source?.uri && source.width)
|
|
252
|
+
.map((source) => ({
|
|
253
|
+
uri: source.uri as string,
|
|
254
|
+
width: Math.round((source.width ?? 0) * (source.scale ?? 1)),
|
|
255
|
+
}))
|
|
256
|
+
const largest = widths.reduce<(typeof widths)[number] | undefined>(
|
|
257
|
+
(best, source) => (!best || source.width > best.width ? source : best),
|
|
258
|
+
undefined,
|
|
259
|
+
)
|
|
260
|
+
return {
|
|
261
|
+
src: largest?.uri ?? sources.find((source) => source?.uri)?.uri,
|
|
262
|
+
srcSet:
|
|
263
|
+
widths.length > 0
|
|
264
|
+
? widths
|
|
265
|
+
.map((source) => `${source.uri} ${source.width}w`)
|
|
266
|
+
.join(', ')
|
|
267
|
+
: undefined,
|
|
268
|
+
widths,
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// An <img> that loaded. The DOM's types aren't in this package's TypeScript
|
|
273
|
+
// setup.
|
|
274
|
+
type LoadedImage = {
|
|
275
|
+
naturalWidth: number
|
|
276
|
+
naturalHeight: number
|
|
277
|
+
currentSrc: string
|
|
278
|
+
complete?: boolean
|
|
279
|
+
decode?: () => Promise<void>
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// The size of the image that loaded, in pixels, as on native. With a srcset,
|
|
283
|
+
// the natural size is divided by the density the browser picked (the
|
|
284
|
+
// candidate's width over `sizes`): it's the picked candidate's size instead.
|
|
285
|
+
function loadedSize(
|
|
286
|
+
image: LoadedImage,
|
|
287
|
+
sized: ReturnType<typeof sizedSources> | undefined,
|
|
288
|
+
) {
|
|
289
|
+
const picked = sized?.srcSet
|
|
290
|
+
? sized.widths.find(
|
|
291
|
+
(source) =>
|
|
292
|
+
new URL(source.uri, image.currentSrc).href ===
|
|
293
|
+
image.currentSrc,
|
|
294
|
+
)
|
|
295
|
+
: undefined
|
|
296
|
+
return {
|
|
297
|
+
width: picked?.width ?? image.naturalWidth,
|
|
298
|
+
height: picked
|
|
299
|
+
? Math.round(
|
|
300
|
+
(image.naturalHeight * picked.width) / image.naturalWidth,
|
|
301
|
+
)
|
|
302
|
+
: image.naturalHeight,
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const fill = {
|
|
307
|
+
position: 'absolute',
|
|
308
|
+
top: 0,
|
|
309
|
+
left: 0,
|
|
310
|
+
width: '100%',
|
|
311
|
+
height: '100%',
|
|
312
|
+
} as const
|
|
313
|
+
|
|
314
|
+
// An image filling the view: an <img> with object-fit and object-position
|
|
315
|
+
// (objectFit's values are CSS's).
|
|
316
|
+
function Picture({
|
|
317
|
+
src,
|
|
318
|
+
srcSet,
|
|
319
|
+
lazy,
|
|
320
|
+
fit,
|
|
321
|
+
position,
|
|
322
|
+
filter,
|
|
323
|
+
hidden,
|
|
324
|
+
alt,
|
|
325
|
+
onLoad,
|
|
326
|
+
onError,
|
|
327
|
+
}: {
|
|
328
|
+
src: string
|
|
329
|
+
srcSet?: string
|
|
330
|
+
// Several sizes load lazily, for sizes="auto" (see sizedSources).
|
|
331
|
+
lazy?: boolean
|
|
332
|
+
fit: ObjectFit
|
|
333
|
+
// objectPosition, as CSS.
|
|
334
|
+
position: string
|
|
335
|
+
filter?: string
|
|
336
|
+
hidden?: boolean
|
|
337
|
+
alt: string
|
|
338
|
+
onLoad?: (image: LoadedImage) => void
|
|
339
|
+
onError?: (uri: string) => void
|
|
340
|
+
}) {
|
|
341
|
+
const image = useRef<HTMLImageElement>(null)
|
|
342
|
+
const mounted = useRef(true)
|
|
343
|
+
// The load that was handled: the file, for these sources. The image can
|
|
344
|
+
// have loaded when this mounts: from the memory cache (it also gets a load
|
|
345
|
+
// event), or before the page's JavaScript ran, in a page rendered on a
|
|
346
|
+
// server (it doesn't). The browser fires load for each new srcset, also
|
|
347
|
+
// when it keeps the same file, and can load another size later.
|
|
348
|
+
const handled = useRef<{ sources: string; file: string } | undefined>(
|
|
349
|
+
undefined,
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
const loaded = (target: LoadedImage) => {
|
|
353
|
+
const file = target.currentSrc || src
|
|
354
|
+
const sources = srcSet ?? src
|
|
355
|
+
if (
|
|
356
|
+
!mounted.current ||
|
|
357
|
+
(handled.current?.sources === sources &&
|
|
358
|
+
handled.current.file === file)
|
|
359
|
+
) {
|
|
360
|
+
return
|
|
361
|
+
}
|
|
362
|
+
handled.current = { sources, file }
|
|
363
|
+
// Once it's decoded, as react-native-web's Image did, so it shows at
|
|
364
|
+
// once (e.g. in place of defaultSource). Safari can fail to decode an
|
|
365
|
+
// SVG, which still shows.
|
|
366
|
+
const done = () => {
|
|
367
|
+
if (mounted.current) onLoad?.(target)
|
|
368
|
+
}
|
|
369
|
+
Promise.resolve(target.decode?.()).then(done, done)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
useEffect(() => {
|
|
373
|
+
mounted.current = true
|
|
374
|
+
return () => {
|
|
375
|
+
mounted.current = false
|
|
376
|
+
}
|
|
377
|
+
}, [])
|
|
378
|
+
|
|
379
|
+
// An image that loaded or failed before this mounted (in a page rendered
|
|
380
|
+
// on a server, before the page's JavaScript ran) had its event then. A
|
|
381
|
+
// broken image is complete with no size; so can an SVG without one be in
|
|
382
|
+
// some browsers, which decodes.
|
|
383
|
+
useClientLayoutEffect(() => {
|
|
384
|
+
const target = image.current as unknown as LoadedImage | null
|
|
385
|
+
if (!target?.complete) return
|
|
386
|
+
if (target.naturalWidth > 0) return loaded(target)
|
|
387
|
+
Promise.resolve(target.decode?.()).then(
|
|
388
|
+
() => loaded(target),
|
|
389
|
+
() => {
|
|
390
|
+
if (mounted.current) onError?.(target.currentSrc || src)
|
|
391
|
+
},
|
|
392
|
+
)
|
|
393
|
+
// When it mounts.
|
|
394
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
395
|
+
}, [])
|
|
396
|
+
|
|
397
|
+
return (
|
|
398
|
+
<img
|
|
399
|
+
ref={image}
|
|
400
|
+
src={src}
|
|
401
|
+
srcSet={srcSet}
|
|
402
|
+
sizes={srcSet ? 'auto, 100vw' : undefined}
|
|
403
|
+
loading={lazy ? 'lazy' : undefined}
|
|
404
|
+
alt={alt}
|
|
405
|
+
draggable={false}
|
|
406
|
+
style={{
|
|
407
|
+
...fill,
|
|
408
|
+
objectFit: fit,
|
|
409
|
+
objectPosition: position,
|
|
410
|
+
filter,
|
|
411
|
+
opacity: hidden ? 0 : undefined,
|
|
412
|
+
}}
|
|
413
|
+
onLoad={(event) =>
|
|
414
|
+
loaded(event.currentTarget as unknown as LoadedImage)
|
|
415
|
+
}
|
|
416
|
+
onError={(event) => {
|
|
417
|
+
const target = event.currentTarget as unknown as
|
|
418
|
+
| LoadedImage
|
|
419
|
+
| undefined
|
|
420
|
+
if (mounted.current) onError?.(target?.currentSrc || src)
|
|
421
|
+
}}
|
|
422
|
+
/>
|
|
423
|
+
)
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// tintColor: an SVG filter that fills the image's pixels with the color,
|
|
427
|
+
// keeping their alpha.
|
|
428
|
+
function TintFilter({ id, color }: { id: string; color: unknown }) {
|
|
429
|
+
return (
|
|
430
|
+
<svg style={tintStyle}>
|
|
431
|
+
<defs>
|
|
432
|
+
<filter id={id}>
|
|
433
|
+
{/* A new element for a new color, which browsers apply. */}
|
|
434
|
+
<feFlood floodColor={String(color)} key={String(color)} />
|
|
435
|
+
<feComposite in2="SourceAlpha" operator="in" />
|
|
436
|
+
</filter>
|
|
437
|
+
</defs>
|
|
438
|
+
</svg>
|
|
439
|
+
)
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const tintStyle = {
|
|
443
|
+
position: 'absolute',
|
|
444
|
+
width: 0,
|
|
445
|
+
height: 0,
|
|
446
|
+
visibility: 'hidden',
|
|
447
|
+
} as const
|
|
448
|
+
|
|
449
|
+
const FastImageMemo = memo(FastImageBase)
|
|
450
|
+
|
|
451
|
+
const FastImageComponent: React.ComponentType<FastImageProps> = forwardRef(
|
|
452
|
+
(props: FastImageProps, ref: React.Ref<any>) => (
|
|
453
|
+
<FastImageMemo forwardedRef={ref} {...props} />
|
|
454
|
+
),
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
FastImageComponent.displayName = 'FastImage'
|
|
458
|
+
|
|
459
|
+
// The image's size, or 0 by 0 if it can't be read.
|
|
460
|
+
function getSize(uri: string) {
|
|
461
|
+
return new Promise<{ width: number; height: number }>((resolve) => {
|
|
462
|
+
Image.getSize(
|
|
463
|
+
uri,
|
|
464
|
+
(width, height) => resolve({ width, height }),
|
|
465
|
+
() => resolve({ width: 0, height: 0 }),
|
|
466
|
+
)
|
|
467
|
+
})
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const FastImage: React.ComponentType<FastImageProps> &
|
|
471
|
+
FastImageStaticProperties = FastImageComponent as any
|
|
472
|
+
|
|
473
|
+
FastImage.cacheControl = cacheControl
|
|
474
|
+
|
|
475
|
+
FastImage.priority = priority
|
|
476
|
+
|
|
477
|
+
FastImage.resizeMode = resizeMode
|
|
478
|
+
|
|
479
|
+
// Loads each source into the browser's cache, with a result per source.
|
|
480
|
+
FastImage.preload = (sources: Source[]) =>
|
|
481
|
+
Promise.all(
|
|
482
|
+
sources.map((source): Promise<PreloadResult> => {
|
|
483
|
+
const uri = source ? source.uri : undefined
|
|
484
|
+
if (typeof uri !== 'string' || !uri) {
|
|
485
|
+
return Promise.resolve({
|
|
486
|
+
ok: false,
|
|
487
|
+
error: 'Invalid source: no uri',
|
|
488
|
+
uri,
|
|
489
|
+
})
|
|
490
|
+
}
|
|
491
|
+
return Image.prefetch(uri).then(
|
|
492
|
+
() => getSize(uri).then((size) => ({ ok: true, uri, ...size })),
|
|
493
|
+
() => ({ ok: false, error: 'Failed to load the image', uri }),
|
|
494
|
+
)
|
|
495
|
+
}),
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
// The browser manages its own cache: these don't clear it.
|
|
499
|
+
FastImage.clearMemoryCache = () => Promise.resolve(notSupported)
|
|
500
|
+
|
|
501
|
+
FastImage.clearDiskCache = () => Promise.resolve(notSupported)
|
|
502
|
+
|
|
503
|
+
FastImage.configureCache = (): Promise<CacheState> => Promise.resolve({})
|
|
504
|
+
|
|
505
|
+
// transition is native only.
|
|
506
|
+
FastImage.setDefaults = () => {}
|
|
507
|
+
|
|
508
|
+
FastImage.writeToCache = () => Promise.resolve(notSupported)
|
|
509
|
+
|
|
510
|
+
FastImage.getCachePath = () => Promise.resolve(notSupported)
|
|
511
|
+
|
|
512
|
+
const styles = StyleSheet.create({
|
|
513
|
+
container: {
|
|
514
|
+
overflow: 'hidden',
|
|
515
|
+
},
|
|
516
|
+
})
|
|
517
|
+
|
|
518
|
+
// FastImage forwards its ref, which its type doesn't say.
|
|
519
|
+
const FastImageWithRef = FastImageComponent as React.ComponentType<
|
|
520
|
+
FastImageProps & { ref?: React.Ref<any> }
|
|
521
|
+
>
|
|
522
|
+
|
|
523
|
+
export const FastImageBackground: React.ComponentType<FastImageBackgroundProps> =
|
|
524
|
+
forwardRef(
|
|
525
|
+
(
|
|
526
|
+
{
|
|
527
|
+
style,
|
|
528
|
+
imageStyle,
|
|
529
|
+
imageRef,
|
|
530
|
+
children,
|
|
531
|
+
...props
|
|
532
|
+
}: FastImageBackgroundProps,
|
|
533
|
+
ref: React.Ref<any>,
|
|
534
|
+
) => (
|
|
535
|
+
<View style={style} ref={ref}>
|
|
536
|
+
<FastImageWithRef
|
|
537
|
+
{...props}
|
|
538
|
+
style={[StyleSheet.absoluteFill, imageStyle]}
|
|
539
|
+
ref={imageRef}
|
|
540
|
+
/>
|
|
541
|
+
{children}
|
|
542
|
+
</View>
|
|
543
|
+
),
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
FastImageBackground.displayName = 'FastImageBackground'
|
|
547
|
+
|
|
548
|
+
export default FastImage
|
package/src/objectFit.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// objectFit, from the prop or the style, shared by the native (index.tsx) and
|
|
2
|
+
// web (index.web.tsx) versions.
|
|
3
|
+
import { StyleSheet } from 'react-native'
|
|
4
|
+
import type { ImageStyle, ObjectFit } from './index'
|
|
5
|
+
|
|
6
|
+
// Finds a key (tintColor, objectFit) in a style prop, where the last style
|
|
7
|
+
// that sets it wins, as with StyleSheet.flatten, but without flattening (which
|
|
8
|
+
// allocates a merged object for an array style on every render).
|
|
9
|
+
export function fromStyle<K extends 'tintColor' | 'objectFit'>(
|
|
10
|
+
style: unknown,
|
|
11
|
+
key: K,
|
|
12
|
+
): ImageStyle[K] {
|
|
13
|
+
if (Array.isArray(style)) {
|
|
14
|
+
for (let i = style.length - 1; i >= 0; i--) {
|
|
15
|
+
const found = fromStyle(style[i], key)
|
|
16
|
+
if (found !== undefined) return found
|
|
17
|
+
}
|
|
18
|
+
return undefined
|
|
19
|
+
}
|
|
20
|
+
if (typeof style === 'number') {
|
|
21
|
+
// A registered style from StyleSheet.create on older React Native.
|
|
22
|
+
const flattened = StyleSheet.flatten(style as any) as
|
|
23
|
+
| ImageStyle
|
|
24
|
+
| undefined
|
|
25
|
+
return flattened ? flattened[key] : undefined
|
|
26
|
+
}
|
|
27
|
+
return style && typeof style === 'object'
|
|
28
|
+
? (style as ImageStyle)[key]
|
|
29
|
+
: undefined
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const FITS: Record<ObjectFit, true> = {
|
|
33
|
+
fill: true,
|
|
34
|
+
contain: true,
|
|
35
|
+
cover: true,
|
|
36
|
+
none: true,
|
|
37
|
+
'scale-down': true,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// The objectFit prop, then style's, then cover (null, or a value that isn't
|
|
41
|
+
// one of objectFit's, counts as not set).
|
|
42
|
+
export function resolveObjectFit(
|
|
43
|
+
objectFit: ObjectFit | null | undefined,
|
|
44
|
+
style: unknown,
|
|
45
|
+
): ObjectFit {
|
|
46
|
+
const fit = objectFit != null ? objectFit : fromStyle(style, 'objectFit')
|
|
47
|
+
return fit && FITS[fit] === true ? fit : 'cover'
|
|
48
|
+
}
|