141 lines
3.7 KiB
TypeScript
141 lines
3.7 KiB
TypeScript
import { Image as ExpoImage, type ImageProps as ExpoImageProps } from 'expo-image'
|
||
import React, { forwardRef, memo, useEffect, useMemo, useState } from 'react'
|
||
import { View } from 'react-native'
|
||
import tw from 'twrnc'
|
||
|
||
interface ImgProps extends ExpoImageProps {
|
||
className?: string
|
||
src?: string | number
|
||
width?: number
|
||
isWebP?: boolean
|
||
errorSource?: string | number
|
||
isCompression?: boolean
|
||
/** 自定义缓存键,用于需要重定向的 URL */
|
||
cacheKey?: string
|
||
/** 占位图 URL,在真实图片加载完成前显示 */
|
||
placeholderSrc?: string
|
||
|
||
autoPlay?: boolean
|
||
}
|
||
|
||
// ExpoImage.clearDiskCache()
|
||
// ExpoImage.clearMemoryCache()
|
||
|
||
// cloudflare 图片优化服务示例地址
|
||
const ImageOrigin = `https://bowong.cc/cdn-cgi/image/width=128,quality=75,format=webp/https://cdn.roasmax.cn/material/b59b75841c484d8bafec9c5636930b69.webp`
|
||
|
||
const Img = forwardRef<ExpoImage, ImgProps>((props, ref) => {
|
||
const {
|
||
className = '',
|
||
style = {},
|
||
src,
|
||
source: propSource,
|
||
cacheKey,
|
||
width = 256,
|
||
isCompression = false,
|
||
isWebP = true,
|
||
placeholderSrc,
|
||
onLoad,
|
||
...reset
|
||
} = props
|
||
|
||
const [isLoaded, setIsLoaded] = useState(false)
|
||
|
||
useEffect(() => {
|
||
setIsLoaded(false)
|
||
}, [src])
|
||
|
||
const imageStyle = tw`${className}`
|
||
|
||
// 静态图全部压缩jpg
|
||
const compressionUrl = useMemo(() => {
|
||
const f = isWebP ? 'webp' : 'jpg'
|
||
return `https://bowong.cc/cdn-cgi/image/width=${width},quality=75,format=${f}/${src}`
|
||
}, [width, isWebP, src])
|
||
|
||
// 判断是否为网络图片
|
||
const isNetworkImage = (uri: string | number): boolean => {
|
||
if (typeof uri !== 'string') return false
|
||
return uri.startsWith('http://') || uri.startsWith('https://')
|
||
}
|
||
|
||
// 构建图片源
|
||
const imageSource = useMemo(() => {
|
||
if (propSource) return propSource
|
||
if (!src) return undefined
|
||
|
||
if (typeof src === 'number') {
|
||
return src
|
||
} else {
|
||
if (isNetworkImage(src)) {
|
||
const finalUrl = isCompression ? compressionUrl : src
|
||
return {
|
||
uri: finalUrl,
|
||
cacheKey: cacheKey || finalUrl,
|
||
}
|
||
} else {
|
||
return { uri: src }
|
||
}
|
||
}
|
||
}, [src, propSource, cacheKey, isCompression, compressionUrl])
|
||
|
||
const handleLoad = (e: any) => {
|
||
// console.log('handleLoad--------------', e)
|
||
|
||
setIsLoaded(true)
|
||
onLoad?.(e)
|
||
}
|
||
|
||
// 无占位图时直接返回原图
|
||
if (!placeholderSrc) {
|
||
return (
|
||
<ExpoImage
|
||
ref={ref}
|
||
style={[style, imageStyle]}
|
||
source={imageSource}
|
||
cachePolicy="disk"
|
||
recyclingKey={typeof src === 'string' ? src : undefined}
|
||
transition={{ duration: 200, effect: 'cross-dissolve' }}
|
||
onLoad={onLoad}
|
||
priority="low"
|
||
{...reset}
|
||
/>
|
||
)
|
||
}
|
||
|
||
const showPlaceholder = placeholderSrc
|
||
|
||
// 有占位图时使用层叠布局
|
||
return (
|
||
<View style={[style, imageStyle, { overflow: 'hidden' }]}>
|
||
{/* 占位图层 - 加载完成后隐藏 */}
|
||
{showPlaceholder && (
|
||
<ExpoImage
|
||
style={{ position: 'absolute', width: '100%', height: '100%' }}
|
||
source={{ uri: placeholderSrc }}
|
||
cachePolicy="disk"
|
||
contentFit={reset.contentFit || 'cover'}
|
||
transition={{ duration: 100, effect: 'curl-up' }}
|
||
/>
|
||
)}
|
||
|
||
{/* 真实图片层 */}
|
||
<ExpoImage
|
||
ref={ref}
|
||
style={{ width: '100%', height: '100%', opacity: isLoaded ? 1 : 0 }}
|
||
source={imageSource}
|
||
cachePolicy="disk"
|
||
recyclingKey={typeof src === 'string' ? src : undefined}
|
||
transition={{ duration: 300, effect: 'cross-dissolve' }}
|
||
onLoad={handleLoad}
|
||
contentFit={reset.contentFit || 'cover'}
|
||
priority="low"
|
||
{...reset}
|
||
/>
|
||
</View>
|
||
)
|
||
})
|
||
|
||
Img.displayName = 'Img'
|
||
export default memo(Img)
|