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 * // 基础用法 * * * // 设置高度,宽度自适应(需要提供 aspectRatio) * * * // 设置宽度,高度自适应(需要提供 aspectRatio) * * * // 自动测量图片尺寸(推荐用于未知尺寸的图片) * * ``` */ interface CustomImageProps extends Omit { /** 图片 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( ( { source, defaultSource = 'https://via.placeholder.com/200', showPlaceholder = false, style, aspectRatio, adaptiveMode, autoMeasure = false, onLoad, ...props }, ref ) => { // 自动测量的宽高比状态 const [measuredAspectRatio, setMeasuredAspectRatio] = React.useState(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 ( 图片加载失败 ); } 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 ( ); } ); Image.displayName = 'Image'; export default Image;