262 lines
8.3 KiB
TypeScript
262 lines
8.3 KiB
TypeScript
import React from 'react';
|
||
import { Image as RNImage, ImageProps as RNImageProps, View, Text } from 'react-native';
|
||
import { getImageSourceWithDefault, isLocalPath, convertLocalPathToUri } from '@/utils/image';
|
||
import { getGameImageSizeFromPath } from '@/constants/gameImages';
|
||
|
||
/**
|
||
* 自定义 Image 组件
|
||
*
|
||
* 功能:
|
||
* - 自动处理图片 URL 验证
|
||
* - 支持默认图片
|
||
* - 支持自适应宽度或高度
|
||
* - 支持自动测量图片尺寸(无需预知 aspectRatio)
|
||
* - 简化图片加载逻辑
|
||
*
|
||
* 使用方式:
|
||
* ```typescript
|
||
* // 基础用法
|
||
* <Image
|
||
* source="https://example.com/image.png"
|
||
* defaultSource="https://via.placeholder.com/200"
|
||
* style={{ width: 100, height: 100 }}
|
||
* />
|
||
*
|
||
* // 设置高度,宽度自适应(需要提供 aspectRatio)
|
||
* <Image
|
||
* source="https://example.com/image.png"
|
||
* style={{ height: 200 }} // 只设置高度
|
||
* aspectRatio={16 / 9}
|
||
* adaptiveMode="height"
|
||
* />
|
||
*
|
||
* // 设置宽度,高度自适应(需要提供 aspectRatio)
|
||
* <Image
|
||
* source="https://example.com/image.png"
|
||
* style={{ width: 300 }} // 只设置宽度
|
||
* aspectRatio={4 / 3}
|
||
* adaptiveMode="width"
|
||
* />
|
||
*
|
||
* // 自动测量图片尺寸(推荐用于未知尺寸的图片)
|
||
* <Image
|
||
* source="https://example.com/image.png"
|
||
* style={{ height: 50 }}
|
||
* adaptiveMode="height"
|
||
* autoMeasure={true}
|
||
* />
|
||
* ```
|
||
*/
|
||
|
||
interface CustomImageProps extends Omit<RNImageProps, 'source' | 'defaultSource'> {
|
||
/** 图片 URL(HTTP/HTTPS 或 Data URI) */
|
||
source?: string;
|
||
/** 默认图片 URL(当主图片无效时使用) */
|
||
defaultSource?: string;
|
||
/** 是否显示加载失败的占位符 */
|
||
showPlaceholder?: boolean;
|
||
/** 图片宽高比(用于自适应宽度或高度) */
|
||
aspectRatio?: number;
|
||
/** 自适应模式:
|
||
* - 'height': 高度固定,宽度自适应(style 中只需设置 height)
|
||
* - 'width': 宽度固定,高度自适应(style 中只需设置 width)
|
||
*/
|
||
adaptiveMode?: 'width' | 'height';
|
||
/** 是否自动测量图片尺寸(当 aspectRatio 未提供时) */
|
||
autoMeasure?: boolean;
|
||
}
|
||
|
||
const Image = React.forwardRef<RNImage, CustomImageProps>(
|
||
(
|
||
{
|
||
source,
|
||
defaultSource = 'https://via.placeholder.com/200',
|
||
showPlaceholder = false,
|
||
style,
|
||
aspectRatio,
|
||
adaptiveMode,
|
||
autoMeasure = false,
|
||
onLoad,
|
||
...props
|
||
},
|
||
ref
|
||
) => {
|
||
// 自动测量的宽高比状态
|
||
const [measuredAspectRatio, setMeasuredAspectRatio] = React.useState<number | null>(null);
|
||
|
||
// 获取有效的图片 URI
|
||
const imageUri = getImageSourceWithDefault(source, defaultSource);
|
||
|
||
// 处理图片加载完成事件
|
||
const handleImageLoad = React.useCallback(
|
||
(event: any) => {
|
||
if (autoMeasure) {
|
||
try {
|
||
// 尝试从不同的位置获取图片尺寸
|
||
let width: number | undefined;
|
||
let height: number | undefined;
|
||
|
||
// 方式 1: event.nativeEvent.source
|
||
if (event?.nativeEvent?.source) {
|
||
width = event.nativeEvent.source.width;
|
||
height = event.nativeEvent.source.height;
|
||
}
|
||
|
||
// 方式 2: event.nativeEvent
|
||
if (!width || !height) {
|
||
width = event?.nativeEvent?.width;
|
||
height = event?.nativeEvent?.height;
|
||
}
|
||
|
||
// 方式 3: 直接从 event
|
||
if (!width || !height) {
|
||
width = event?.width;
|
||
height = event?.height;
|
||
}
|
||
|
||
if (width && height && typeof width === 'number' && typeof height === 'number') {
|
||
const ratio = width / height;
|
||
setMeasuredAspectRatio(ratio);
|
||
if (__DEV__) {
|
||
console.log(
|
||
`[Image] Measured dimensions from event: ${width}x${height}, ratio: ${ratio.toFixed(3)}`
|
||
);
|
||
}
|
||
} else {
|
||
// 如果从 event 获取失败,尝试从本地路径的映射表获取
|
||
if (isLocalPath(source)) {
|
||
const size = getGameImageSizeFromPath(source);
|
||
if (size && size.width && size.height) {
|
||
const ratio = size.width / size.height;
|
||
setMeasuredAspectRatio(ratio);
|
||
if (__DEV__) {
|
||
console.log(
|
||
`[Image] Measured dimensions from mapping: ${size.width}x${size.height}, ratio: ${ratio.toFixed(3)}`
|
||
);
|
||
}
|
||
} else if (__DEV__) {
|
||
console.warn(
|
||
`[Image] Failed to get dimensions from event or mapping for: ${source}`
|
||
);
|
||
}
|
||
} else if (__DEV__) {
|
||
console.warn(
|
||
`[Image] Failed to get dimensions from event. Event:`,
|
||
JSON.stringify(event, null, 2)
|
||
);
|
||
}
|
||
}
|
||
} catch (error) {
|
||
// 忽略获取尺寸的错误
|
||
console.warn('Failed to measure image dimensions:', error);
|
||
}
|
||
}
|
||
onLoad?.(event);
|
||
},
|
||
[autoMeasure, onLoad, source]
|
||
);
|
||
|
||
// 计算自适应样式
|
||
const adaptiveStyle = React.useMemo(() => {
|
||
// 优先使用提供的 aspectRatio,其次使用测量的 aspectRatio
|
||
const ratio = aspectRatio || (autoMeasure ? measuredAspectRatio : null);
|
||
|
||
if (!ratio || !adaptiveMode) {
|
||
return {};
|
||
}
|
||
|
||
// 从 style 中提取宽度和高度
|
||
const styleObj = Array.isArray(style) ? Object.assign({}, ...style) : style || {};
|
||
const fixedWidth = styleObj.width;
|
||
const fixedHeight = styleObj.height;
|
||
|
||
// adaptiveMode 的含义:
|
||
// - 'height': 高度固定,宽度自适应
|
||
// 计算方式:width = height * aspectRatio
|
||
// - 'width': 宽度固定,高度自适应
|
||
// 计算方式:height = width / aspectRatio
|
||
|
||
if (adaptiveMode === 'height' && fixedHeight) {
|
||
// 高度固定,计算宽度
|
||
// 注意:不返回 aspectRatio,让宽高完全由计算值决定
|
||
// 这样可以避免 resizeMode 对宽高比的影响
|
||
const calculatedWidth = fixedHeight * ratio;
|
||
if (__DEV__) {
|
||
console.log(
|
||
`[Image] adaptiveMode=height: fixedHeight=${fixedHeight}, ratio=${ratio.toFixed(3)}, calculatedWidth=${calculatedWidth.toFixed(1)}`
|
||
);
|
||
}
|
||
return {
|
||
width: calculatedWidth,
|
||
height: fixedHeight,
|
||
};
|
||
} else if (adaptiveMode === 'width' && fixedWidth) {
|
||
// 宽度固定,计算高度
|
||
// 注意:不返回 aspectRatio,让宽高完全由计算值决定
|
||
const calculatedHeight = fixedWidth / ratio;
|
||
if (__DEV__) {
|
||
console.log(
|
||
`[Image] adaptiveMode=width: fixedWidth=${fixedWidth}, ratio=${ratio.toFixed(3)}, calculatedHeight=${calculatedHeight.toFixed(1)}`
|
||
);
|
||
}
|
||
return {
|
||
width: fixedWidth,
|
||
height: calculatedHeight,
|
||
};
|
||
}
|
||
|
||
// 如果没有固定值,仅使用 aspectRatio
|
||
return { aspectRatio: ratio };
|
||
}, [aspectRatio, adaptiveMode, autoMeasure, measuredAspectRatio, style]);
|
||
|
||
// 如果没有有效的图片 URI,显示占位符
|
||
if (!imageUri) {
|
||
if (showPlaceholder) {
|
||
return (
|
||
<View
|
||
style={[
|
||
{
|
||
backgroundColor: '#f0f0f0',
|
||
justifyContent: 'center',
|
||
alignItems: 'center',
|
||
},
|
||
style,
|
||
adaptiveStyle,
|
||
]}
|
||
>
|
||
<Text style={{ color: '#999', fontSize: 12 }}>图片加载失败</Text>
|
||
</View>
|
||
);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
// 处理本地路径和外部 URL
|
||
let imageSource: any;
|
||
|
||
if (isLocalPath(imageUri)) {
|
||
// 本地路径:/images/game/chess/color_101.png
|
||
// 转换为 require 对象
|
||
const localSource = convertLocalPathToUri(imageUri);
|
||
imageSource = localSource || { uri: defaultSource };
|
||
} else {
|
||
// 外部 URL
|
||
imageSource = { uri: imageUri };
|
||
}
|
||
|
||
return (
|
||
<RNImage
|
||
ref={ref}
|
||
source={imageSource}
|
||
style={[style]}
|
||
onLoad={handleImageLoad}
|
||
{...props}
|
||
/>
|
||
);
|
||
}
|
||
);
|
||
|
||
Image.displayName = 'Image';
|
||
|
||
export default Image;
|