src/index.tsx
import React, { useRef, useCallback, useState, ReactNode } from 'react';
import {
View,
Image,
ImageBackground,
Animated,
StyleSheet,
ImageProps,
ViewStyle,
StyleProp,
ImageSourcePropType,
NativeSyntheticEvent,
ImageErrorEventData,
} from 'react-native';
import { useDeepCompareEffectNoCheck } from 'use-deep-compare-effect';
export interface BetterImageProps extends ImageProps {
viewStyle?: StyleProp<ViewStyle>;
thumbnailFadeDuration?: number;
imageFadeDuration?: number;
thumbnailSource?: ImageSourcePropType;
thumbnailBlurRadius?: number;
fallbackSource?: ImageSourcePropType;
children?: ReactNode;
}
const { Value, createAnimatedComponent, timing } = Animated;
const AnimatedImage = createAnimatedComponent(Image);
const AnimatedImageBackground = createAnimatedComponent(ImageBackground);
const BetterImage = ({
viewStyle,
thumbnailFadeDuration = 250,
imageFadeDuration = 250,
thumbnailSource,
source,
onLoadEnd,
resizeMethod,
resizeMode,
thumbnailBlurRadius = 1,
style,
fallbackSource = { uri: '' },
onError,
children,
...otherProps
}: BetterImageProps) => {
const imageOpacity = useRef(new Value(0)).current;
const thumbnailOpacity = useRef(new Value(0)).current;
const thumbnailAnimationProgress = useRef<
Animated.CompositeAnimation | undefined
>();
const [hasError, setHasError] = useState(false);
const [hasLoaded, setHasLoaded] = useState(false);
const onImageLoad = () => {
setHasLoaded(true);
timing(imageOpacity, {
toValue: 1,
duration: imageFadeDuration,
useNativeDriver: true,
}).start(() => {
thumbnailAnimationProgress.current?.stop();
timing(thumbnailOpacity, {
toValue: 0,
duration: thumbnailFadeDuration,
useNativeDriver: true,
}).start();
});
onLoadEnd && onLoadEnd();
};
const onThumbnailLoad = () => {
if (!hasLoaded) {
const progress = timing(thumbnailOpacity, {
toValue: 1,
duration: thumbnailFadeDuration,
useNativeDriver: true,
});
thumbnailAnimationProgress.current = progress;
thumbnailAnimationProgress.current.start();
}
};
const onImageLoadError = (
event: NativeSyntheticEvent<ImageErrorEventData>
) => {
setHasError(true);
onError && onError(event);
};
useDeepCompareEffectNoCheck(
useCallback(() => {
imageOpacity.setValue(0);
thumbnailOpacity.setValue(0);
setHasError(false);
setHasLoaded(false);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []),
[source, thumbnailSource]
);
const ImageComponent = children ? AnimatedImageBackground : AnimatedImage;
return (
<View style={[styles.imageContainerStyle, viewStyle]}>
{thumbnailSource ? (
<ImageComponent
children={children}
onLoadEnd={onThumbnailLoad}
style={[
styles.thumbnailImageStyle,
{ opacity: thumbnailOpacity },
style,
]}
source={thumbnailSource}
blurRadius={thumbnailBlurRadius}
resizeMethod={resizeMethod}
resizeMode={resizeMode}
/>
) : null}
<ImageComponent
children={children}
resizeMethod={resizeMethod}
resizeMode={resizeMode}
onLoadEnd={onImageLoad}
onError={hasError ? () => null : onImageLoadError}
source={hasError ? fallbackSource : source}
style={[styles.imageStyle, { opacity: imageOpacity }, style]}
{...otherProps}
/>
</View>
);
};
const styles = StyleSheet.create({
imageContainerStyle: {
overflow: 'hidden',
},
thumbnailImageStyle: {
...StyleSheet.absoluteFillObject,
},
imageStyle: {
...StyleSheet.absoluteFillObject,
},
});
export default BetterImage;