react-native-fast-image 10.5.0 → 10.7.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 +26 -4
- package/android/src/main/java/com/fastimage/FastImageViewManager.java +19 -14
- package/android/src/main/java/com/fastimage/FastImageViewWithUrl.java +97 -29
- package/dist/composeRefs.d.ts +2 -0
- package/dist/index.cjs.js +33 -25
- package/dist/index.cjs.js.flow +3 -1
- package/dist/index.cjs.web.js +52 -13
- package/dist/index.cjs.web.js.flow +3 -1
- package/dist/index.d.ts +18 -4
- package/dist/index.js +34 -26
- package/dist/index.js.flow +3 -1
- package/dist/index.web.d.ts +5 -3
- package/dist/index.web.js +53 -14
- package/dist/index.web.js.flow +3 -1
- package/dist/specs/FastImageViewNativeComponent.d.ts +5 -2
- package/ios/FastImage/FFFThumbHash.h +3 -0
- package/ios/FastImage/FFFThumbHash.m +5 -1
- package/ios/FastImage/FFFastImageView.h +13 -7
- package/ios/FastImage/FFFastImageView.m +31 -19
- package/ios/FastImage/FFFastImageViewComponentView.mm +72 -14
- package/package.json +1 -1
- package/src/composeRefs.ts +29 -0
- package/src/index.js.flow +3 -1
- package/src/index.tsx +64 -81
- package/src/index.web.tsx +99 -53
- package/src/specs/FastImageViewNativeComponent.ts +25 -16
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { Children, forwardRef, memo, useMemo } from "react";
|
|
2
|
-
import { Image, Platform, StyleSheet, TurboModuleRegistry, View, codegenNativeComponent } from "react-native";
|
|
2
|
+
import { Image, Platform, StyleSheet, TurboModuleRegistry, View, codegenNativeCommands, codegenNativeComponent } from "react-native";
|
|
3
3
|
//#region src/constants.ts
|
|
4
4
|
const resizeMode = {
|
|
5
5
|
contain: "contain",
|
|
@@ -228,6 +228,7 @@ function objectPositionOrCentered(position) {
|
|
|
228
228
|
}
|
|
229
229
|
//#endregion
|
|
230
230
|
//#region src/specs/FastImageViewNativeComponent.ts
|
|
231
|
+
const Commands = codegenNativeCommands({ supportedCommands: ["reload"] });
|
|
231
232
|
var FastImageViewNativeComponent_default = codegenNativeComponent("FastImageView");
|
|
232
233
|
//#endregion
|
|
233
234
|
//#region src/specs/NativeFastImageModule.ts
|
|
@@ -252,17 +253,28 @@ function dataUri(value) {
|
|
|
252
253
|
if (typeof __DEV__ !== "undefined" && __DEV__) warnOnce("placeholder-url", `react-native-fast-image: \`blurDataUri\` takes a data uri (data:image/…): "${value.slice(0, 60)}" is ignored. For a placeholder loaded from a url, use \`placeholder\`.`, "warn");
|
|
253
254
|
}
|
|
254
255
|
//#endregion
|
|
255
|
-
//#region src/
|
|
256
|
-
function
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
function
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
256
|
+
//#region src/composeRefs.ts
|
|
257
|
+
function composeRefs(...refs) {
|
|
258
|
+
return (value) => {
|
|
259
|
+
const cleanups = refs.map((ref) => {
|
|
260
|
+
if (typeof ref === "function") {
|
|
261
|
+
const cleanup = ref(value);
|
|
262
|
+
return typeof cleanup === "function" ? cleanup : () => {
|
|
263
|
+
ref(null);
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
if (ref) {
|
|
267
|
+
ref.current = value;
|
|
268
|
+
return () => {
|
|
269
|
+
ref.current = null;
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
return () => cleanups.forEach((cleanup) => cleanup === null || cleanup === void 0 ? void 0 : cleanup());
|
|
274
|
+
};
|
|
265
275
|
}
|
|
276
|
+
//#endregion
|
|
277
|
+
//#region src/index.tsx
|
|
266
278
|
const resolvePlaceholder = (placeholder) => {
|
|
267
279
|
if (typeof placeholder === "string") {
|
|
268
280
|
if (isImageName(placeholder)) return Platform.OS === "android" ? placeholder : { uri: placeholder };
|
|
@@ -334,28 +346,21 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, onLoadSt
|
|
|
334
346
|
const resolvedTintColor = tintColor != null ? tintColor : fromStyle(style, "tintColor");
|
|
335
347
|
const fit = resolveObjectFit(objectFit, style);
|
|
336
348
|
const position = objectPositionOrCentered(objectPosition);
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
sources: (sources === null || sources === void 0 ? void 0 : sources.some(hasSourcePlaceholder)) ? sources.map(withoutPlaceholders) : sources,
|
|
345
|
-
sourcePlaceholders: placeholders.some(Boolean) ? placeholders : void 0
|
|
346
|
-
};
|
|
347
|
-
}, [source, shared]);
|
|
348
|
-
const resolvedPlaceholder = (_placeholderSource = placeholderSource(placeholder)) !== null && _placeholderSource !== void 0 ? _placeholderSource : resolvePlaceholder(hasSourcePlaceholder(placeholder) ? defaultSource : placeholder !== null && placeholder !== void 0 ? placeholder : defaultSource);
|
|
349
|
+
const resolvedSource = useMemo(() => {
|
|
350
|
+
for (const entry of [source].flat()) dataUri(entry === null || entry === void 0 ? void 0 : entry.blurDataUri);
|
|
351
|
+
const single = Array.isArray(source) && source.length === 1 ? source[0] : source;
|
|
352
|
+
return Array.isArray(single) ? single.map((entry) => Image.resolveAssetSource(entry)) : Image.resolveAssetSource(single);
|
|
353
|
+
}, [source]);
|
|
354
|
+
const resolvedPlaceholder = hasSourcePlaceholder(placeholder) ? placeholder.thumbhash || dataUri(placeholder.blurDataUri) ? placeholder : resolvePlaceholder(defaultSource) : (_placeholderSource = placeholderSource(placeholder)) !== null && _placeholderSource !== void 0 ? _placeholderSource : resolvePlaceholder(placeholder !== null && placeholder !== void 0 ? placeholder : defaultSource);
|
|
355
|
+
const ref = useMemo(() => composeRefs(addReload, forwardedRef), [forwardedRef]);
|
|
349
356
|
return /* @__PURE__ */ React.createElement(FastImageViewNativeComponent_default, {
|
|
350
357
|
...props,
|
|
351
|
-
ref
|
|
358
|
+
ref,
|
|
352
359
|
style: [styles.image, style],
|
|
353
360
|
recyclingKey: (_props$recyclingKey = props.recyclingKey) !== null && _props$recyclingKey !== void 0 ? _props$recyclingKey : void 0,
|
|
354
361
|
loopCount: loopCount(loop),
|
|
355
362
|
...resolveTransition(transition, (_defaultTransition = defaultTransition) !== null && _defaultTransition !== void 0 ? _defaultTransition : builtInTransition()),
|
|
356
363
|
source: resolvedSource,
|
|
357
|
-
sources,
|
|
358
|
-
sourcePlaceholders,
|
|
359
364
|
placeholder: resolvedPlaceholder,
|
|
360
365
|
onFastImageLoadStart: onLoadStart,
|
|
361
366
|
onFastImageProgress: onProgress,
|
|
@@ -377,6 +382,9 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, onLoadSt
|
|
|
377
382
|
objectPositionYOffset: position.y.offset
|
|
378
383
|
});
|
|
379
384
|
}
|
|
385
|
+
function addReload(view) {
|
|
386
|
+
if (view) view.reload = () => Commands.reload(view);
|
|
387
|
+
}
|
|
380
388
|
const FastImageMemo = memo(FastImageBase);
|
|
381
389
|
const FastImageComponent = forwardRef((props, ref) => /* @__PURE__ */ React.createElement(FastImageMemo, {
|
|
382
390
|
forwardedRef: ref,
|
package/dist/index.js.flow
CHANGED
|
@@ -145,7 +145,7 @@ export type FastImageProps = $ReadOnly<{|
|
|
|
145
145
|
onLoadStart?: ?() => void,
|
|
146
146
|
onProgress?: ?(event: OnProgressEvent) => void,
|
|
147
147
|
|
|
148
|
-
source?: ?(FastImageSource | Array<FastImageSource> | number),
|
|
148
|
+
source?: ?(FastImageSource | Array<FastImageSource | number> | number),
|
|
149
149
|
placeholder?: ?(number | string | SourcePlaceholder | PlaceholderSource),
|
|
150
150
|
// Deprecated: use placeholder, which is the same.
|
|
151
151
|
defaultSource?: ?number,
|
|
@@ -196,4 +196,6 @@ declare export default class FastImage extends React$Component<FastImageProps> {
|
|
|
196
196
|
source: FastImageSource,
|
|
197
197
|
file: string,
|
|
198
198
|
) => Promise<CachePathResult>;
|
|
199
|
+
// On a ref: loads the image again, e.g. to try again after onError.
|
|
200
|
+
reload(): void;
|
|
199
201
|
}
|
package/dist/index.web.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
import type { FastImageBackgroundProps, FastImageProps, FastImageRef, FastImageStaticProperties } from './index';
|
|
3
4
|
export type * from './index';
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
type ViewRef = React.ComponentRef<typeof View>;
|
|
6
|
+
declare const FastImage: React.ForwardRefExoticComponent<FastImageProps & React.RefAttributes<FastImageRef>> & FastImageStaticProperties;
|
|
7
|
+
export declare const FastImageBackground: React.ForwardRefExoticComponent<FastImageBackgroundProps & React.RefAttributes<ViewRef>>;
|
|
6
8
|
export default FastImage;
|
package/dist/index.web.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { Children, forwardRef, memo, useEffect, useId, useLayoutEffect, useRef, useState } from "react";
|
|
1
|
+
import React, { Children, forwardRef, memo, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import { Image, PixelRatio, StyleSheet, View } from "react-native";
|
|
3
3
|
import { getAssetByID } from "react-native-web/dist/modules/AssetRegistry";
|
|
4
4
|
//#region src/constants.ts
|
|
@@ -255,6 +255,27 @@ function dataUri(value) {
|
|
|
255
255
|
if (typeof __DEV__ !== "undefined" && __DEV__) warnOnce("placeholder-url", `react-native-fast-image: \`blurDataUri\` takes a data uri (data:image/…): "${value.slice(0, 60)}" is ignored. For a placeholder loaded from a url, use \`placeholder\`.`, "warn");
|
|
256
256
|
}
|
|
257
257
|
//#endregion
|
|
258
|
+
//#region src/composeRefs.ts
|
|
259
|
+
function composeRefs(...refs) {
|
|
260
|
+
return (value) => {
|
|
261
|
+
const cleanups = refs.map((ref) => {
|
|
262
|
+
if (typeof ref === "function") {
|
|
263
|
+
const cleanup = ref(value);
|
|
264
|
+
return typeof cleanup === "function" ? cleanup : () => {
|
|
265
|
+
ref(null);
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
if (ref) {
|
|
269
|
+
ref.current = value;
|
|
270
|
+
return () => {
|
|
271
|
+
ref.current = null;
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
return () => cleanups.forEach((cleanup) => cleanup === null || cleanup === void 0 ? void 0 : cleanup());
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
//#endregion
|
|
258
279
|
//#region src/index.web.tsx
|
|
259
280
|
const notSupported = {
|
|
260
281
|
ok: false,
|
|
@@ -271,12 +292,17 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
|
|
|
271
292
|
const src = sized ? sized.src : resolveUri(source);
|
|
272
293
|
const srcSet = sized === null || sized === void 0 ? void 0 : sized.srcSet;
|
|
273
294
|
const key = srcSet !== null && srcSet !== void 0 ? srcSet : src;
|
|
295
|
+
const [reloads, setReloads] = useState(0);
|
|
296
|
+
const ref = useMemo(() => composeRefs((view) => {
|
|
297
|
+
if (view) view.reload = () => setReloads((count) => count + 1);
|
|
298
|
+
}, forwardedRef), [forwardedRef]);
|
|
274
299
|
const priority = sourcePriority(source);
|
|
275
300
|
const preview = hasSourcePlaceholder(placeholder) ? placeholder : void 0;
|
|
276
301
|
const blurUri = (_resolveUri = resolveUri(dataUri(shared(source, "blurDataUri", preview)))) !== null && _resolveUri !== void 0 ? _resolveUri : thumbhashUri(shared(source, "thumbhash", preview));
|
|
277
302
|
const urlPlaceholder = (_placeholderSource = placeholderSource(placeholder)) === null || _placeholderSource === void 0 ? void 0 : _placeholderSource.uri;
|
|
303
|
+
const placeholderKey = `${urlPlaceholder} ${reloads}`;
|
|
278
304
|
const [failedPlaceholder, setFailedPlaceholder] = useState();
|
|
279
|
-
const defaultUri = blurUri !== null && blurUri !== void 0 ? blurUri : resolveUri(preview
|
|
305
|
+
const defaultUri = blurUri !== null && blurUri !== void 0 ? blurUri : resolveUri(preview ? defaultSource : urlPlaceholder ? void 0 : placeholder !== null && placeholder !== void 0 ? placeholder : defaultSource);
|
|
280
306
|
const [result, setResult] = useState();
|
|
281
307
|
const ok = result && result.key === key ? result.ok : void 0;
|
|
282
308
|
const svgId = useId().replace(/[^\w-]/g, "");
|
|
@@ -288,14 +314,14 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
|
|
|
288
314
|
const showDefault = ok !== true;
|
|
289
315
|
useClientLayoutEffect(() => {
|
|
290
316
|
if (key) onLoadStart === null || onLoadStart === void 0 || onLoadStart();
|
|
291
|
-
}, [key]);
|
|
317
|
+
}, [key, reloads]);
|
|
292
318
|
return /* @__PURE__ */ React.createElement(View, {
|
|
293
319
|
...viewProps,
|
|
294
320
|
"aria-label": label,
|
|
295
321
|
style: [styles.container, style],
|
|
296
322
|
onLayout,
|
|
297
323
|
pointerEvents,
|
|
298
|
-
ref
|
|
324
|
+
ref
|
|
299
325
|
}, defaultUri && showDefault ? /* @__PURE__ */ React.createElement(Picture, {
|
|
300
326
|
key: `default ${defaultUri}`,
|
|
301
327
|
src: defaultUri,
|
|
@@ -307,7 +333,7 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
|
|
|
307
333
|
filter: blurUri ? [`url(#${blurId})`, tintColor ? `url(#${tintId})` : ""].filter(Boolean).join(" ") : filter,
|
|
308
334
|
alt: key ? "" : label !== null && label !== void 0 ? label : ""
|
|
309
335
|
}) : null, urlPlaceholder && showDefault ? /* @__PURE__ */ React.createElement(Picture, {
|
|
310
|
-
key: `placeholder ${
|
|
336
|
+
key: `placeholder ${placeholderKey}`,
|
|
311
337
|
src: urlPlaceholder,
|
|
312
338
|
loading,
|
|
313
339
|
priority: "high",
|
|
@@ -315,11 +341,11 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
|
|
|
315
341
|
fit: fit === "scale-down" || fit === "none" ? "contain" : fit,
|
|
316
342
|
position,
|
|
317
343
|
filter: tintColor ? `url(#${tintId})` : void 0,
|
|
318
|
-
hidden: failedPlaceholder ===
|
|
319
|
-
onError: () => setFailedPlaceholder(
|
|
344
|
+
hidden: failedPlaceholder === placeholderKey,
|
|
345
|
+
onError: () => setFailedPlaceholder(placeholderKey),
|
|
320
346
|
alt: ""
|
|
321
347
|
}) : null, key && src ? /* @__PURE__ */ React.createElement(Picture, {
|
|
322
|
-
key: several ? "sizes" : key
|
|
348
|
+
key: `${several ? "sizes" : key} ${reloads}`,
|
|
323
349
|
src,
|
|
324
350
|
srcSet,
|
|
325
351
|
sizes,
|
|
@@ -389,10 +415,9 @@ function shared(source, key, preview) {
|
|
|
389
415
|
function resolveUri(source) {
|
|
390
416
|
let uri;
|
|
391
417
|
if (typeof source === "number") {
|
|
392
|
-
var _asset$scales$;
|
|
393
418
|
const asset = getAssetByID(source);
|
|
394
419
|
if (!asset) return void 0;
|
|
395
|
-
const scale =
|
|
420
|
+
const scale = assetScale(asset);
|
|
396
421
|
const suffix = scale !== 1 ? `@${scale}x` : "";
|
|
397
422
|
uri = `${asset.httpServerLocation}/${asset.name}${suffix}.${asset.type}`;
|
|
398
423
|
} else if (typeof source === "string") uri = source;
|
|
@@ -401,9 +426,24 @@ function resolveUri(source) {
|
|
|
401
426
|
const svg = uri === null || uri === void 0 ? void 0 : uri.match(/^(data:image\/svg\+xml;utf8,)(.*)/);
|
|
402
427
|
return svg ? svg[1] + encodeURIComponent(svg[2]) : uri || void 0;
|
|
403
428
|
}
|
|
404
|
-
function
|
|
429
|
+
function assetScale(asset) {
|
|
430
|
+
var _asset$scales$;
|
|
431
|
+
return asset.scales.reduce((best, scale) => Math.abs(scale - PixelRatio.get()) < Math.abs(best - PixelRatio.get()) ? scale : best, (_asset$scales$ = asset.scales[0]) !== null && _asset$scales$ !== void 0 ? _asset$scales$ : 1);
|
|
432
|
+
}
|
|
433
|
+
function sizedSource(source) {
|
|
434
|
+
if (typeof source !== "number") return source;
|
|
435
|
+
const asset = getAssetByID(source);
|
|
436
|
+
return asset ? {
|
|
437
|
+
uri: resolveUri(source),
|
|
438
|
+
width: asset.width,
|
|
439
|
+
height: asset.height,
|
|
440
|
+
scale: assetScale(asset)
|
|
441
|
+
} : void 0;
|
|
442
|
+
}
|
|
443
|
+
function sizedSources(entries) {
|
|
405
444
|
var _largest$uri, _sources$find;
|
|
406
|
-
const
|
|
445
|
+
const sources = entries.map(sizedSource);
|
|
446
|
+
const widths = sources.filter((source) => !!(source === null || source === void 0 ? void 0 : source.uri) && !!source.width).map((source) => {
|
|
407
447
|
var _source$width, _source$scale;
|
|
408
448
|
return {
|
|
409
449
|
uri: source.uri,
|
|
@@ -563,11 +603,10 @@ FastImage.setDefaults = () => {};
|
|
|
563
603
|
FastImage.writeToCache = () => Promise.resolve(notSupported);
|
|
564
604
|
FastImage.getCachePath = () => Promise.resolve(notSupported);
|
|
565
605
|
const styles = StyleSheet.create({ container: { overflow: "hidden" } });
|
|
566
|
-
const FastImageWithRef = FastImageComponent;
|
|
567
606
|
const FastImageBackground = forwardRef(({ style, imageStyle, imageRef, children, ...props }, ref) => /* @__PURE__ */ React.createElement(View, {
|
|
568
607
|
style,
|
|
569
608
|
ref
|
|
570
|
-
}, /* @__PURE__ */ React.createElement(
|
|
609
|
+
}, /* @__PURE__ */ React.createElement(FastImageComponent, {
|
|
571
610
|
...props,
|
|
572
611
|
style: [StyleSheet.absoluteFill, imageStyle],
|
|
573
612
|
ref: imageRef
|
package/dist/index.web.js.flow
CHANGED
|
@@ -145,7 +145,7 @@ export type FastImageProps = $ReadOnly<{|
|
|
|
145
145
|
onLoadStart?: ?() => void,
|
|
146
146
|
onProgress?: ?(event: OnProgressEvent) => void,
|
|
147
147
|
|
|
148
|
-
source?: ?(FastImageSource | Array<FastImageSource> | number),
|
|
148
|
+
source?: ?(FastImageSource | Array<FastImageSource | number> | number),
|
|
149
149
|
placeholder?: ?(number | string | SourcePlaceholder | PlaceholderSource),
|
|
150
150
|
// Deprecated: use placeholder, which is the same.
|
|
151
151
|
defaultSource?: ?number,
|
|
@@ -196,4 +196,6 @@ declare export default class FastImage extends React$Component<FastImageProps> {
|
|
|
196
196
|
source: FastImageSource,
|
|
197
197
|
file: string,
|
|
198
198
|
) => Promise<CachePathResult>;
|
|
199
|
+
// On a ref: loads the image again, e.g. to try again after onError.
|
|
200
|
+
reload(): void;
|
|
199
201
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type * as React from 'react';
|
|
1
2
|
import type { CodegenTypes, ColorValue, HostComponent, ViewProps } from 'react-native';
|
|
2
3
|
type LoadStartEvent = Readonly<{}>;
|
|
3
4
|
type ProgressEvent = Readonly<{
|
|
@@ -20,9 +21,7 @@ type LoadEndEvent = Readonly<{
|
|
|
20
21
|
}>;
|
|
21
22
|
export interface NativeProps extends ViewProps {
|
|
22
23
|
source?: CodegenTypes.UnsafeMixed;
|
|
23
|
-
sources?: CodegenTypes.UnsafeMixed;
|
|
24
24
|
placeholder?: CodegenTypes.UnsafeMixed;
|
|
25
|
-
sourcePlaceholders?: ReadonlyArray<string>;
|
|
26
25
|
objectFit?: CodegenTypes.WithDefault<string, 'cover'>;
|
|
27
26
|
objectPositionX?: CodegenTypes.WithDefault<CodegenTypes.Float, 0.5>;
|
|
28
27
|
objectPositionXOffset?: CodegenTypes.Float;
|
|
@@ -46,5 +45,9 @@ export interface NativeProps extends ViewProps {
|
|
|
46
45
|
onFastImageError?: CodegenTypes.DirectEventHandler<ErrorEvent>;
|
|
47
46
|
onFastImageLoadEnd?: CodegenTypes.DirectEventHandler<LoadEndEvent>;
|
|
48
47
|
}
|
|
48
|
+
interface NativeCommands {
|
|
49
|
+
reload: (viewRef: React.ElementRef<HostComponent<NativeProps>>) => void;
|
|
50
|
+
}
|
|
51
|
+
export declare const Commands: NativeCommands;
|
|
49
52
|
declare const _default: HostComponent<NativeProps>;
|
|
50
53
|
export default _default;
|
|
@@ -15,6 +15,9 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
15
15
|
// Main thread.
|
|
16
16
|
+ (nullable UIImage*) imageForPlaceholder: (nullable NSString*)placeholder;
|
|
17
17
|
|
|
18
|
+
// Whether a placeholder is a data uri (a blurDataUri) rather than a ThumbHash.
|
|
19
|
+
+ (BOOL) isDataUri: (nullable NSString*)placeholder;
|
|
20
|
+
|
|
18
21
|
// The placeholder for a base64 ThumbHash (standard or url-safe, with or
|
|
19
22
|
// without padding), or nil for an empty or invalid one. Main thread.
|
|
20
23
|
+ (nullable UIImage*) imageForThumbHash: (nullable NSString*)thumbhash;
|
|
@@ -200,8 +200,12 @@ static UIImage* FFFBlurredPreview(UIImage* image) {
|
|
|
200
200
|
return cache;
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
+
+ (BOOL) isDataUri: (NSString*)placeholder {
|
|
204
|
+
return placeholder && [placeholder rangeOfString: @"data:" options: NSCaseInsensitiveSearch | NSAnchoredSearch].location != NSNotFound;
|
|
205
|
+
}
|
|
206
|
+
|
|
203
207
|
+ (UIImage*) imageForPlaceholder: (NSString*)placeholder {
|
|
204
|
-
if ([
|
|
208
|
+
if (![self isDataUri: placeholder]) {
|
|
205
209
|
return [self imageForThumbHash: placeholder];
|
|
206
210
|
}
|
|
207
211
|
UIImage* cached = [[self cache] objectForKey: placeholder];
|
|
@@ -28,9 +28,8 @@ typedef NS_ENUM(NSInteger, FFFObjectFit) {
|
|
|
28
28
|
@property (nonatomic, copy) RCTDirectEventBlock onFastImageLoad;
|
|
29
29
|
@property (nonatomic, copy) RCTDirectEventBlock onFastImageLoadEnd;
|
|
30
30
|
@property (nonatomic, assign) FFFObjectFit objectFit;
|
|
31
|
-
|
|
32
|
-
//
|
|
33
|
-
// loads the one whose size is closest to its own.
|
|
31
|
+
// The `source` prop: one source, or several (sizes of one image), of which
|
|
32
|
+
// the view loads the one whose size is closest to its own.
|
|
34
33
|
@property (nonatomic, copy) NSArray<FFFastImageSource *> *sources;
|
|
35
34
|
// The `placeholder` prop.
|
|
36
35
|
@property (nonatomic, strong) UIImage *placeholder;
|
|
@@ -38,11 +37,14 @@ typedef NS_ENUM(NSInteger, FFFObjectFit) {
|
|
|
38
37
|
// image, at a high priority (or only from the cache, with cache cacheOnly),
|
|
39
38
|
// it shows over the other placeholders once it has.
|
|
40
39
|
@property (nonatomic, strong) FFFastImageSource *placeholderSource;
|
|
41
|
-
// The
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
40
|
+
// The sources' ThumbHashes or blurDataUris (data uris), by the source's index
|
|
41
|
+
// (an empty string for none): the picked source's shows instead of
|
|
42
|
+
// placeholder. Set apart from the sources, so a new one for the same image
|
|
43
|
+
// doesn't load it again.
|
|
45
44
|
@property (nonatomic, copy) NSArray<NSString *> *sourcePlaceholders;
|
|
45
|
+
// `placeholder` as a thumbhash or blurDataUri: shows for the sources without
|
|
46
|
+
// their own.
|
|
47
|
+
@property (nonatomic, copy) NSString *placeholderHash;
|
|
46
48
|
@property (nonatomic, strong) UIColor *imageColor;
|
|
47
49
|
// When it changes, the next image doesn't replace the current one: the view
|
|
48
50
|
// clears first (for views reused for other content, like list rows).
|
|
@@ -80,6 +82,10 @@ typedef NS_ENUM(NSInteger, FFFObjectFit) {
|
|
|
80
82
|
// the view has a size, when it needs one).
|
|
81
83
|
- (void) didSetProps;
|
|
82
84
|
|
|
85
|
+
// Loads the image again, as a new source would (once the view has a size,
|
|
86
|
+
// when it needs one): the ref's reload().
|
|
87
|
+
- (void) reload;
|
|
88
|
+
|
|
83
89
|
// Keeps the view's mask (React Native's, for its rounded corners) on the box
|
|
84
90
|
// the view was laid out at, while objectPosition moves the view: after React
|
|
85
91
|
// Native sets or removes it.
|
|
@@ -44,9 +44,9 @@
|
|
|
44
44
|
// downsample, or several sources: a load waits for the view's size (see
|
|
45
45
|
// didSetProps).
|
|
46
46
|
@property(nonatomic, assign) BOOL waitsForSize;
|
|
47
|
-
// The `source` prop
|
|
48
|
-
// the view's size
|
|
49
|
-
@property(nonatomic, strong) FFFastImageSource*
|
|
47
|
+
// The source that loads: the `source` prop's, or with several the one
|
|
48
|
+
// picked for the view's size.
|
|
49
|
+
@property(nonatomic, strong) FFFastImageSource* source;
|
|
50
50
|
// Several sources: the next load switches to another one for a new view
|
|
51
51
|
// size, and the one after it is loading. It's the same picture at another
|
|
52
52
|
// size, so it doesn't fade in.
|
|
@@ -749,21 +749,11 @@ NSString *FFFErrorMessage(NSError *error)
|
|
|
749
749
|
return event;
|
|
750
750
|
}
|
|
751
751
|
|
|
752
|
-
- (void) setSource: (FFFastImageSource*)source {
|
|
753
|
-
if (self.propSource != source) {
|
|
754
|
-
self.propSource = source;
|
|
755
|
-
if (![self picksSource]) {
|
|
756
|
-
_source = source;
|
|
757
|
-
}
|
|
758
|
-
_needsReload = YES;
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
|
|
762
752
|
- (void) setSources: (NSArray<FFFastImageSource*>*)sources {
|
|
763
753
|
if (_sources != sources) {
|
|
764
754
|
_sources = [sources copy];
|
|
765
755
|
if (![self picksSource]) {
|
|
766
|
-
_source =
|
|
756
|
+
_source = _sources.firstObject;
|
|
767
757
|
}
|
|
768
758
|
_needsReload = YES;
|
|
769
759
|
}
|
|
@@ -847,11 +837,16 @@ NSString *FFFErrorMessage(NSError *error)
|
|
|
847
837
|
return [_dynamicRangeLimit isEqualToString: @"constrained"] || [_dynamicRangeLimit isEqualToString: @"no-limit"];
|
|
848
838
|
}
|
|
849
839
|
|
|
840
|
+
// Shows at once, as a new source placeholder does (see updateHashImage),
|
|
841
|
+
// without loading the image again.
|
|
850
842
|
- (void) setPlaceholder: (UIImage*)placeholder {
|
|
851
|
-
if (_placeholder
|
|
852
|
-
|
|
853
|
-
_needsReload = YES;
|
|
843
|
+
if (_placeholder == placeholder) {
|
|
844
|
+
return;
|
|
854
845
|
}
|
|
846
|
+
UIImage* previous = [self shownPlaceholder];
|
|
847
|
+
BOOL showing = self.showsPlaceholder;
|
|
848
|
+
_placeholder = placeholder;
|
|
849
|
+
[self showChangedPlaceholder: previous showing: showing];
|
|
855
850
|
}
|
|
856
851
|
|
|
857
852
|
- (void) setSourcePlaceholders: (NSArray<NSString*>*)sourcePlaceholders {
|
|
@@ -859,6 +854,11 @@ NSString *FFFErrorMessage(NSError *error)
|
|
|
859
854
|
[self updateHashImage];
|
|
860
855
|
}
|
|
861
856
|
|
|
857
|
+
- (void) setPlaceholderHash: (NSString*)placeholderHash {
|
|
858
|
+
_placeholderHash = [placeholderHash copy];
|
|
859
|
+
[self updateHashImage];
|
|
860
|
+
}
|
|
861
|
+
|
|
862
862
|
// What shows while the image loads and if it fails: placeholderSource's
|
|
863
863
|
// image once it has loaded, the picked source's placeholder (its ThumbHash or
|
|
864
864
|
// blurDataUri), or the placeholder prop.
|
|
@@ -912,6 +912,9 @@ NSString *FFFErrorMessage(NSError *error)
|
|
|
912
912
|
}
|
|
913
913
|
|
|
914
914
|
- (void) setPlaceholderSource: (FFFastImageSource*)placeholderSource {
|
|
915
|
+
if (!placeholderSource && !_placeholderSource) {
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
915
918
|
_placeholderSource = placeholderSource;
|
|
916
919
|
[self cancelUrlPlaceholderLoad];
|
|
917
920
|
[self showUrlPlaceholderImage: nil];
|
|
@@ -979,14 +982,18 @@ NSString *FFFErrorMessage(NSError *error)
|
|
|
979
982
|
[self showChangedPlaceholder: previous showing: showing];
|
|
980
983
|
}
|
|
981
984
|
|
|
982
|
-
// The picked source's ThumbHash or blurDataUri, or nil.
|
|
985
|
+
// The picked source's ThumbHash or blurDataUri, or placeholderHash, or nil.
|
|
983
986
|
- (NSString*) pickedSourcePlaceholder {
|
|
984
987
|
NSUInteger index = 0;
|
|
985
988
|
if ([self picksSource]) {
|
|
986
989
|
index = _source ? [_sources indexOfObjectIdenticalTo: _source] : NSNotFound;
|
|
990
|
+
// None until one is picked.
|
|
991
|
+
if (index == NSNotFound) {
|
|
992
|
+
return nil;
|
|
993
|
+
}
|
|
987
994
|
}
|
|
988
995
|
NSString* hash = index < _sourcePlaceholders.count ? _sourcePlaceholders[index] : nil;
|
|
989
|
-
return hash.length > 0 ? hash : nil;
|
|
996
|
+
return hash.length > 0 ? hash : _placeholderHash.length > 0 ? _placeholderHash : nil;
|
|
990
997
|
}
|
|
991
998
|
|
|
992
999
|
// Decodes the picked source's placeholder. A view showing the previous
|
|
@@ -1040,6 +1047,11 @@ NSString *FFFErrorMessage(NSError *error)
|
|
|
1040
1047
|
}
|
|
1041
1048
|
}
|
|
1042
1049
|
|
|
1050
|
+
- (void) reload {
|
|
1051
|
+
_needsReload = YES;
|
|
1052
|
+
[self didSetProps];
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1043
1055
|
// The same image decoded again for a dynamicRangeLimit between HDR and SDR,
|
|
1044
1056
|
// as a resize is (reloadIfResized): without events once it has loaded, and
|
|
1045
1057
|
// the image showing stays if the load fails. A display setting shouldn't
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
#import <react/renderer/components/RNFastImageSpec/RCTComponentViewHelpers.h>
|
|
9
9
|
|
|
10
10
|
#import "FFFastImageView.h"
|
|
11
|
+
#import "FFFThumbHash.h"
|
|
11
12
|
#import "RCTConvert+FFFastImage.h"
|
|
12
13
|
|
|
13
14
|
using namespace facebook::react;
|
|
@@ -102,6 +103,41 @@ static id _Nullable FFFJSON(const folly::dynamic &value)
|
|
|
102
103
|
return value.isNull() ? nil : FFFObjectFromDynamic(value);
|
|
103
104
|
}
|
|
104
105
|
|
|
106
|
+
// A source (or `placeholder`) without its thumbhash and blurDataUri, to
|
|
107
|
+
// compare: one source, or each of several.
|
|
108
|
+
static folly::dynamic FFFWithoutPlaceholders(const folly::dynamic &value)
|
|
109
|
+
{
|
|
110
|
+
if (value.isArray()) {
|
|
111
|
+
folly::dynamic result = folly::dynamic::array;
|
|
112
|
+
for (const auto &entry : value) {
|
|
113
|
+
result.push_back(FFFWithoutPlaceholders(entry));
|
|
114
|
+
}
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
if (!value.isObject()) {
|
|
118
|
+
return value;
|
|
119
|
+
}
|
|
120
|
+
folly::dynamic result = value;
|
|
121
|
+
result.erase("thumbhash");
|
|
122
|
+
result.erase("blurDataUri");
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// A source's (or `placeholder`'s) ThumbHash, or its blurDataUri if it's a data
|
|
127
|
+
// uri (JS warns about another), or nil.
|
|
128
|
+
static NSString *_Nullable FFFSourcePlaceholder(NSDictionary *source)
|
|
129
|
+
{
|
|
130
|
+
id thumbhash = source[@"thumbhash"];
|
|
131
|
+
if ([thumbhash isKindOfClass:[NSString class]] && [thumbhash length] > 0) {
|
|
132
|
+
return thumbhash;
|
|
133
|
+
}
|
|
134
|
+
id blurDataUri = source[@"blurDataUri"];
|
|
135
|
+
if ([blurDataUri isKindOfClass:[NSString class]] && [FFFThumbHash isDataUri:blurDataUri]) {
|
|
136
|
+
return blurDataUri;
|
|
137
|
+
}
|
|
138
|
+
return nil;
|
|
139
|
+
}
|
|
140
|
+
|
|
105
141
|
// Whether the placeholder prop is a url to load apart from the image (a
|
|
106
142
|
// source with an http or https uri), rather than an image to show at once: a
|
|
107
143
|
// bundled image (served by the packager in development), a file or a data uri.
|
|
@@ -242,25 +278,36 @@ typedef NS_OPTIONS(int32_t, FFFEvent) {
|
|
|
242
278
|
FFFastImageView *view = _imageView;
|
|
243
279
|
|
|
244
280
|
if (all || oldViewProps.source != newViewProps.source) {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
281
|
+
// One source, or several (an array). Only a change besides their
|
|
282
|
+
// thumbhashes and blurDataUris loads again: a new one for the same
|
|
283
|
+
// image (e.g. one that arrives later) shows at once.
|
|
284
|
+
BOOL loads = all || FFFWithoutPlaceholders(oldViewProps.source) != FFFWithoutPlaceholders(newViewProps.source);
|
|
285
|
+
id json = FFFJSON(newViewProps.source);
|
|
286
|
+
NSArray *entries = [json isKindOfClass:[NSArray class]] ? json : json ? @[ json ] : @[];
|
|
287
|
+
NSMutableArray<FFFastImageSource *> *sources = [NSMutableArray arrayWithCapacity:entries.count];
|
|
288
|
+
NSMutableArray<NSString *> *placeholders = [NSMutableArray arrayWithCapacity:entries.count];
|
|
289
|
+
for (id entry in entries) {
|
|
290
|
+
if ([entry isKindOfClass:[NSDictionary class]]) {
|
|
291
|
+
if (loads) {
|
|
292
|
+
[sources addObject:[RCTConvert FFFastImageSource:entry]];
|
|
293
|
+
}
|
|
294
|
+
[placeholders addObject:FFFSourcePlaceholder(entry) ?: @""];
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (loads) {
|
|
298
|
+
view.sources = sources.count > 0 ? sources : nil;
|
|
299
|
+
}
|
|
300
|
+
view.sourcePlaceholders = placeholders;
|
|
250
301
|
}
|
|
251
302
|
if (all || oldViewProps.placeholder != newViewProps.placeholder) {
|
|
303
|
+
// A thumbhash or blurDataUri, a url to load, or an image to show.
|
|
252
304
|
id placeholder = FFFJSON(newViewProps.placeholder);
|
|
253
|
-
BOOL
|
|
254
|
-
|
|
305
|
+
BOOL hash = [placeholder isKindOfClass:[NSDictionary class]] && (placeholder[@"thumbhash"] || placeholder[@"blurDataUri"]);
|
|
306
|
+
BOOL loads = !hash && FFFIsUrlPlaceholder(placeholder);
|
|
307
|
+
view.placeholderHash = hash ? FFFSourcePlaceholder(placeholder) : nil;
|
|
308
|
+
view.placeholder = placeholder && !hash && !loads ? [RCTConvert UIImage:placeholder] : nil;
|
|
255
309
|
view.placeholderSource = loads ? [RCTConvert FFFastImageSource:placeholder] : nil;
|
|
256
310
|
}
|
|
257
|
-
if (all || oldViewProps.sourcePlaceholders != newViewProps.sourcePlaceholders) {
|
|
258
|
-
NSMutableArray<NSString *> *placeholders = [NSMutableArray arrayWithCapacity:newViewProps.sourcePlaceholders.size()];
|
|
259
|
-
for (const auto &placeholder : newViewProps.sourcePlaceholders) {
|
|
260
|
-
[placeholders addObject:[NSString stringWithUTF8String:placeholder.c_str()]];
|
|
261
|
-
}
|
|
262
|
-
view.sourcePlaceholders = placeholders.count > 0 ? placeholders : nil;
|
|
263
|
-
}
|
|
264
311
|
if (all || oldViewProps.objectFit != newViewProps.objectFit) {
|
|
265
312
|
view.objectFit = [RCTConvert FFFObjectFit:FFFString(newViewProps.objectFit)];
|
|
266
313
|
}
|
|
@@ -341,6 +388,17 @@ typedef NS_OPTIONS(int32_t, FFFEvent) {
|
|
|
341
388
|
}
|
|
342
389
|
}
|
|
343
390
|
|
|
391
|
+
// The view's commands (reload), from the ref's methods.
|
|
392
|
+
- (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args
|
|
393
|
+
{
|
|
394
|
+
RCTFastImageViewHandleCommand(self, commandName, args);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
- (void)reload
|
|
398
|
+
{
|
|
399
|
+
[_imageView reload];
|
|
400
|
+
}
|
|
401
|
+
|
|
344
402
|
// React Native makes the image view's mask again when the appearance changes
|
|
345
403
|
// (as after an update): where objectPosition moved the view, it moves back.
|
|
346
404
|
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
|
package/package.json
CHANGED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Shared by the native (index.tsx) and web (index.web.tsx) versions.
|
|
2
|
+
import type React from 'react'
|
|
3
|
+
|
|
4
|
+
// One ref that sets each of these refs (as React Native's useMergeRefs does),
|
|
5
|
+
// and clears them when it's cleared.
|
|
6
|
+
export function composeRefs<T>(
|
|
7
|
+
...refs: (React.Ref<T> | undefined)[]
|
|
8
|
+
): React.RefCallback<T> {
|
|
9
|
+
return (value) => {
|
|
10
|
+
const cleanups = refs.map((ref) => {
|
|
11
|
+
if (typeof ref === 'function') {
|
|
12
|
+
const cleanup = ref(value)
|
|
13
|
+
return typeof cleanup === 'function'
|
|
14
|
+
? cleanup
|
|
15
|
+
: () => {
|
|
16
|
+
ref(null)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
if (ref) {
|
|
20
|
+
ref.current = value
|
|
21
|
+
return () => {
|
|
22
|
+
ref.current = null
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return undefined
|
|
26
|
+
})
|
|
27
|
+
return () => cleanups.forEach((cleanup) => cleanup?.())
|
|
28
|
+
}
|
|
29
|
+
}
|