react-native-fast-image
Version:
FastImage, performant React Native image component.
271 lines (270 loc) • 9.75 kB
JavaScript
import React, { forwardRef, memo, useRef } from "react";
import { Image, NativeModules, Platform, StyleSheet, View, requireNativeComponent } from "react-native";
//#region src/constants.ts
const resizeMode = {
contain: "contain",
cover: "cover",
stretch: "stretch",
center: "center",
repeat: "repeat"
};
const priority = {
low: "low",
normal: "normal",
high: "high"
};
const cacheControl = {
immutable: "immutable",
web: "web",
cacheOnly: "cacheOnly"
};
//#endregion
//#region src/index.tsx
const resolveDefaultSource = (defaultSource) => {
if (!defaultSource) return null;
if (Platform.OS === "android") {
const resolved = Image.resolveAssetSource(defaultSource);
if (resolved) return resolved.uri;
return null;
}
return defaultSource;
};
function tintColorFromStyle(style) {
if (Array.isArray(style)) {
for (let i = style.length - 1; i >= 0; i--) {
const found = tintColorFromStyle(style[i]);
if (found !== void 0) return found;
}
return;
}
if (typeof style === "number") {
const flattened = StyleSheet.flatten(style);
return flattened ? flattened.tintColor : void 0;
}
return style && typeof style === "object" ? style.tintColor : void 0;
}
function loopCount(loop) {
if (loop === void 0) return -1;
if (loop === true) return 0;
if (loop === false) return 1;
return Number.isFinite(loop) ? Math.max(1, Math.floor(loop)) : 0;
}
function loadResult(event) {
var _event$width, _event$height, _event$error;
return event.ok ? {
ok: true,
width: (_event$width = event.width) !== null && _event$width !== void 0 ? _event$width : 0,
height: (_event$height = event.height) !== null && _event$height !== void 0 ? _event$height : 0
} : {
ok: false,
error: (_event$error = event.error) !== null && _event$error !== void 0 ? _event$error : "Failed to load the image"
};
}
function withProgress(onProgress) {
return onProgress && ((event) => {
const { loaded, total } = event.nativeEvent;
event.nativeEvent.progress = total > 0 ? Math.min(1, Math.max(0, loaded / total)) : 0;
onProgress(event);
});
}
const DEFAULT_FADE_MS = Platform.OS === "ios" ? 250 : 300;
function transitionProps(transition) {
var _transition$duration;
const { duration, betweenImages, skipOnCacheHit } = typeof transition === "number" ? { duration: transition } : typeof transition === "object" && transition ? {
...transition,
duration: (_transition$duration = transition.duration) !== null && _transition$duration !== void 0 ? _transition$duration : DEFAULT_FADE_MS
} : { duration: transition ? DEFAULT_FADE_MS : 0 };
return {
transitionDuration: typeof duration === "number" && duration > 0 ? duration : 0,
transitionBetweenImages: !!betweenImages,
transitionSkipOnCacheHit: skipOnCacheHit || "memory"
};
}
function withoutCache(source) {
const { cache: _cache, ...rest } = source || {};
return rest;
}
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 }) {
const { onClick, ...props } = viewProps;
const fallbackResult = useRef(void 0);
const wrapperProps = {
onLayout,
onClick,
pointerEvents,
hitSlop,
onStartShouldSetResponder,
onStartShouldSetResponderCapture,
onMoveShouldSetResponder,
onMoveShouldSetResponderCapture,
onResponderGrant,
onResponderReject,
onResponderStart,
onResponderMove,
onResponderEnd,
onResponderRelease,
onResponderTerminationRequest,
onResponderTerminate,
onTouchStart,
onTouchMove,
onTouchEnd,
onTouchCancel,
onTouchEndCapture
};
const imageProps = {
...props,
pointerEvents: pointerEvents === "box-none" ? "none" : void 0
};
const resolvedTintColor = tintColor != null ? tintColor : tintColorFromStyle(style);
if (fallback) {
const cleanedSource = typeof source === "number" ? source : Array.isArray(source) ? source.map(withoutCache) : withoutCache(source);
const resolvedSource = Image.resolveAssetSource(cleanedSource);
return /* @__PURE__ */ React.createElement(View, {
style: [styles.imageContainer, style],
...wrapperProps,
ref: forwardedRef
}, /* @__PURE__ */ React.createElement(Image, {
...imageProps,
style: [styles.fallbackImage, { tintColor: resolvedTintColor }],
source: resolvedSource,
defaultSource,
onLoadStart,
onProgress: withProgress(onProgress),
onLoad: onLoadEnd ? (event) => {
const { width, height } = event.nativeEvent.source;
fallbackResult.current = {
ok: true,
width,
height
};
onLoad === null || onLoad === void 0 || onLoad(event);
} : onLoad,
onError: onLoadEnd ? (event) => {
fallbackResult.current = {
ok: false,
error: String(event.nativeEvent.error)
};
onError === null || onError === void 0 || onError(event);
} : onError,
onLoadEnd: onLoadEnd && (() => {
var _fallbackResult$curre;
return onLoadEnd((_fallbackResult$curre = fallbackResult.current) !== null && _fallbackResult$curre !== void 0 ? _fallbackResult$curre : {
ok: false,
error: "Failed to load the image"
});
}),
resizeMode
}), children);
}
const sources = Array.isArray(source) ? source.length === 1 ? void 0 : source : void 0;
const single = Array.isArray(source) ? source[0] : source;
const resolvedSource = sources ? void 0 : Image.resolveAssetSource(single);
const resolvedDefaultSource = resolveDefaultSource(defaultSource);
return /* @__PURE__ */ React.createElement(View, {
style: [styles.imageContainer, style],
...wrapperProps,
ref: forwardedRef
}, /* @__PURE__ */ React.createElement(FastImageView, {
...imageProps,
tintColor: resolvedTintColor,
loopCount: loopCount(loop),
...transitionProps(transition),
style: StyleSheet.absoluteFill,
source: resolvedSource,
sources,
defaultSource: resolvedDefaultSource,
onFastImageLoadStart: onLoadStart,
onFastImageProgress: withProgress(onProgress),
trackProgress: !!onProgress,
onFastImageLoad: onLoad,
onFastImageError: onError,
onFastImageLoadEnd: onLoadEnd && ((event) => onLoadEnd(loadResult(event.nativeEvent))),
resizeMode
}), children);
}
const FastImageMemo = memo(FastImageBase);
const FastImageComponent = forwardRef((props, ref) => /* @__PURE__ */ React.createElement(FastImageMemo, {
forwardedRef: ref,
...props
}));
FastImageComponent.displayName = "FastImage";
const noResult = {
ok: false,
error: "No result"
};
const FastImage = FastImageComponent;
FastImage.resizeMode = resizeMode;
FastImage.cacheControl = cacheControl;
FastImage.priority = priority;
const ONE_SOURCE = "Takes one source, not an array of sizes: pass the size to use";
FastImage.preload = (sources) => Promise.resolve(NativeModules.FastImageView.preload(sources.map((s) => s && !Array.isArray(s) ? s : {}))).then((results) => sources.map((source, i) => {
var _results$i;
if (Array.isArray(source)) return {
ok: false,
error: ONE_SOURCE,
uri: void 0
};
const uri = source ? source.uri : void 0;
const result = (_results$i = results === null || results === void 0 ? void 0 : results[i]) !== null && _results$i !== void 0 ? _results$i : noResult;
if (!result.ok) return {
...result,
uri
};
return typeof uri === "string" ? {
...result,
uri
} : {
ok: false,
error: "Invalid source: no uri",
uri
};
}));
FastImage.clearMemoryCache = () => Promise.resolve(NativeModules.FastImageView.clearMemoryCache());
FastImage.clearDiskCache = () => Promise.resolve(NativeModules.FastImageView.clearDiskCache());
FastImage.configureCache = (limits = {}) => Promise.resolve(NativeModules.FastImageView.configureCache(limits));
FastImage.writeToCache = (source, file) => Array.isArray(source) ? Promise.resolve({
ok: false,
error: ONE_SOURCE
}) : Promise.resolve(NativeModules.FastImageView.writeToCache(source || {}, file));
FastImage.getCachePath = (source) => Array.isArray(source) ? Promise.resolve({
ok: false,
error: ONE_SOURCE
}) : Promise.resolve(NativeModules.FastImageView.getCachePath(source || {}));
const styles = StyleSheet.create({
fallbackImage: {
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: "100%"
},
imageContainer: { overflow: "hidden" }
});
/**
* An image with content on top of it, like React Native's `ImageBackground`: a
* view that the image fills, with the children on top. Use it rather than
* giving `FastImage` children, which it won't render in the next major version
* (the image will be a single native view). The other props go to the image;
* the ref is the view's.
*/
const FastImageBackground = forwardRef(({ style, imageStyle, imageRef, children, importantForAccessibility, ...props }, ref) => /* @__PURE__ */ React.createElement(View, {
accessibilityIgnoresInvertColors: true,
importantForAccessibility,
style,
ref
}, /* @__PURE__ */ React.createElement(FastImageComponent, {
...props,
importantForAccessibility,
style: [StyleSheet.absoluteFill, imageStyle],
ref: imageRef
}), children));
FastImageBackground.displayName = "FastImageBackground";
const FastImageView = requireNativeComponent("FastImageView", FastImage, { nativeOnly: {
onFastImageLoadStart: true,
onFastImageProgress: true,
onFastImageLoad: true,
onFastImageError: true,
onFastImageLoadEnd: true,
trackProgress: true
} });
//#endregion
export { FastImageBackground, FastImage as default };