/** * ImageGrid 图片网格组件 * 支持 1-9 张图片的智能布局 * 根据图片数量自动选择最佳展示方式 */ import React, { useMemo, useCallback, useState, useEffect } from 'react'; import { View, StyleSheet, Dimensions, ViewStyle, Pressable, Text, Image, } from 'react-native'; import { SmartImage, ImageSource } from './SmartImage'; import { colors, spacing, borderRadius } from '../../theme'; const { width: SCREEN_WIDTH } = Dimensions.get('window'); // 默认容器内边距(用于计算可用宽度) const DEFAULT_CONTAINER_PADDING = spacing.lg * 2; // 左右各 spacing.lg // 单张图片的默认宽高比 const SINGLE_IMAGE_DEFAULT_ASPECT_RATIO = 4 / 3; // 默认4:3比例 // 单张图片的最大高度 const SINGLE_IMAGE_MAX_HEIGHT = 400; // 单张图片的最小高度 const SINGLE_IMAGE_MIN_HEIGHT = 150; // 图片项类型 - 兼容 PostImageDTO 和 CommentImage export interface ImageGridItem { id?: string; uri?: string; url?: string; thumbnailUrl?: string; thumbnail_url?: string; width?: number; height?: number; } // 布局模式 export type GridLayoutMode = 'auto' | 'single' | 'horizontal' | 'grid' | 'masonry'; // 组件 Props export interface ImageGridProps { /** 图片列表 */ images: ImageGridItem[]; /** 容器样式 */ style?: ViewStyle; /** 最大显示数量 */ maxDisplayCount?: number; /** 布局模式 */ mode?: GridLayoutMode; /** 图片间距 */ gap?: number; /** 圆角大小 */ borderRadius?: number; /** 是否显示更多遮罩 */ showMoreOverlay?: boolean; /** 单张图片最大高度 */ singleImageMaxHeight?: number; /** 网格列数(2或3) */ gridColumns?: 2 | 3; /** 图片点击回调 */ onImagePress?: (images: ImageGridItem[], index: number) => void; /** 测试ID */ testID?: string; } /** * 计算图片网格尺寸 */ const calculateGridDimensions = ( count: number, containerWidth: number, gap: number, columns: number ) => { const totalGap = (columns - 1) * gap; const itemSize = (containerWidth - totalGap) / columns; // 计算行数 const rows = Math.ceil(count / columns); return { itemSize, rows, containerHeight: rows * itemSize + (rows - 1) * gap, }; }; // ─── 单张图片子组件 ─────────────────────────────────────────────────────────── // 独立成组件,方便用 useState 管理异步加载到的实际尺寸 interface SingleImageItemProps { image: ImageGridItem; containerWidth: number; maxHeight: number; borderRadiusValue: number; onPress: () => void; } const SingleImageItem: React.FC = ({ image, containerWidth, maxHeight, borderRadiusValue, onPress, }) => { const [aspectRatio, setAspectRatio] = useState(null); const uri = image.uri || image.url || ''; useEffect(() => { if (!uri) return; let cancelled = false; // 添加超时处理,防止高分辨率图片加载卡住 const timeoutId = setTimeout(() => { if (!cancelled && aspectRatio === null) { setAspectRatio(SINGLE_IMAGE_DEFAULT_ASPECT_RATIO); } }, 3000); Image.getSize( uri, (w, h) => { if (!cancelled) { clearTimeout(timeoutId); setAspectRatio(w / h); } }, () => { if (!cancelled) { clearTimeout(timeoutId); setAspectRatio(SINGLE_IMAGE_DEFAULT_ASPECT_RATIO); } }, ); return () => { cancelled = true; clearTimeout(timeoutId); }; }, [uri]); const effectiveContainerWidth = containerWidth || SCREEN_WIDTH - DEFAULT_CONTAINER_PADDING; // 尺寸还没拿到时先占位,避免闪烁 if (aspectRatio === null) { return ( ); } // 适配最大边界框,保证宽高比不变 let width: number; let height: number; if (aspectRatio > effectiveContainerWidth / maxHeight) { // 宽图:宽度撑满容器 width = effectiveContainerWidth; height = effectiveContainerWidth / aspectRatio; } else { // 高图:高度触及上限 height = maxHeight; width = maxHeight * aspectRatio; } // 最小高度兜底 if (height < SINGLE_IMAGE_MIN_HEIGHT) { height = SINGLE_IMAGE_MIN_HEIGHT; width = Math.min(SINGLE_IMAGE_MIN_HEIGHT * aspectRatio, effectiveContainerWidth); } return ( ); }; /** * 图片网格组件 * 智能布局:根据图片数量自动选择最佳展示方式 */ export const ImageGrid: React.FC = ({ images, style, maxDisplayCount = 9, mode = 'auto', gap = 4, borderRadius: borderRadiusValue = borderRadius.md, showMoreOverlay = true, singleImageMaxHeight = 300, gridColumns = 3, onImagePress, testID, }) => { // 通过 onLayout 拿到容器实际宽度 const [containerWidth, setContainerWidth] = useState(0); // 过滤有效图片 - 支持 uri 或 url 字段 const validImages = useMemo(() => { const filtered = images.filter(img => img.uri || img.url || typeof img === 'string'); return filtered; }, [images]); // 实际显示的图片 const displayImages = useMemo(() => { return validImages.slice(0, maxDisplayCount); }, [validImages, maxDisplayCount]); // 剩余图片数量 const remainingCount = useMemo(() => { return Math.max(0, validImages.length - maxDisplayCount); }, [validImages, maxDisplayCount]); // 处理图片点击 const handleImagePress = useCallback( (index: number) => { onImagePress?.(validImages, index); }, [onImagePress, validImages] ); // 确定布局模式 const layoutMode = useMemo(() => { if (mode !== 'auto') return mode; const count = displayImages.length; if (count === 1) return 'single'; if (count === 2) return 'horizontal'; return 'grid'; }, [mode, displayImages.length]); // 渲染单张图片 const renderSingleImage = () => { const image = displayImages[0]; if (!image) return null; return ( handleImagePress(0)} /> ); }; // 渲染横向双图 const renderHorizontal = () => { return ( {displayImages.map((image, index) => ( handleImagePress(index)} style={[ styles.horizontalItem, { flex: 1, aspectRatio: 1, borderRadius: borderRadiusValue, }, ]} > ))} ); }; // 渲染网格布局 const renderGrid = () => { return ( {displayImages.map((image, index) => { const isLastVisible = index === displayImages.length - 1; const showOverlay = isLastVisible && remainingCount > 0 && showMoreOverlay; return ( handleImagePress(index)} style={[ styles.gridItem, gridColumns === 3 ? styles.gridItem3 : styles.gridItem2, { borderRadius: borderRadiusValue, }, ]} > {showOverlay && ( +{remainingCount} )} ); })} ); }; // 渲染瀑布流布局 const renderMasonry = () => { const containerWidth = SCREEN_WIDTH - DEFAULT_CONTAINER_PADDING; const columns = 2; const itemWidth = (containerWidth - gap) / columns; // 将图片分配到两列 const leftColumn: ImageGridItem[] = []; const rightColumn: ImageGridItem[] = []; displayImages.forEach((image, index) => { if (index % 2 === 0) { leftColumn.push(image); } else { rightColumn.push(image); } }); const renderColumn = (columnImages: ImageGridItem[], columnIndex: number) => { return ( {columnImages.map((image, index) => { const actualIndex = columnIndex + index * 2; const aspectRatio = image.width && image.height ? image.width / image.height : 1; const height = itemWidth / aspectRatio; return ( handleImagePress(actualIndex)} style={[ styles.masonryItem, { width: itemWidth, height: Math.max(height, itemWidth * 0.7), borderRadius: borderRadiusValue, }, ]} > ); })} ); }; return ( {renderColumn(leftColumn, 0)} {renderColumn(rightColumn, 1)} ); }; // 根据布局模式渲染 const renderContent = () => { switch (layoutMode) { case 'single': return renderSingleImage(); case 'horizontal': return renderHorizontal(); case 'masonry': return renderMasonry(); case 'grid': default: return renderGrid(); } }; // 如果没有图片,返回null if (displayImages.length === 0) { return null; } return ( setContainerWidth(e.nativeEvent.layout.width)} > {renderContent()} ); }; // 紧凑模式 - 用于评论等小空间场景 export interface CompactImageGridProps extends Omit { /** 最大尺寸限制 */ maxWidth?: number; } /** * 紧凑图片网格 * 适用于评论等空间有限的场景 */ export const CompactImageGrid: React.FC = ({ maxWidth, gap = 4, borderRadius: borderRadiusValue = borderRadius.sm, ...props }) => { const containerWidth = maxWidth || SCREEN_WIDTH - DEFAULT_CONTAINER_PADDING - 36 - spacing.sm; // 36是头像宽度 const renderCompactGrid = () => { const { images } = props; const count = images.length; if (count === 0) return null; if (count === 1) { const image = images[0]; const size = Math.min(containerWidth * 0.6, 150); return ( props.onImagePress?.(images, 0)} style={[ styles.compactItem, { width: size, height: size, borderRadius: borderRadiusValue, }, ]} > ); } // 多张图片使用小网格 const columns = count <= 4 ? 2 : 3; const { itemSize } = calculateGridDimensions(count, containerWidth, gap, columns); return ( {images.slice(0, 6).map((image, index) => ( props.onImagePress?.(images, index)} style={[ styles.compactItem, { width: itemSize, height: itemSize, borderRadius: borderRadiusValue, }, ]} > {index === 5 && images.length > 6 && ( +{images.length - 6} )} ))} ); }; return {renderCompactGrid()}; }; const styles = StyleSheet.create({ container: { marginTop: spacing.sm, }, fullSize: { flex: 1, width: '100%', height: '100%', }, // 单图样式 singleContainer: { overflow: 'hidden', backgroundColor: colors.background.disabled, }, // 横向布局样式 horizontalContainer: { flexDirection: 'row', }, horizontalItem: { overflow: 'hidden', backgroundColor: colors.background.disabled, }, // 网格布局样式 gridContainer: { flexDirection: 'row', flexWrap: 'wrap', }, gridItem: { overflow: 'hidden', backgroundColor: colors.background.disabled, aspectRatio: 1, }, gridItem2: { width: '48%', // 2列布局,每列约48%宽度,留有余量避免换行 }, gridItem3: { width: '31%', // 3列布局,每列约31%宽度,留有余量避免换行 }, // 瀑布流样式 masonryContainer: { flexDirection: 'row', }, masonryColumn: { flex: 1, }, masonryItem: { overflow: 'hidden', backgroundColor: colors.background.disabled, }, // 更多遮罩 - 类似微博的灰色蒙版 moreOverlay: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(0, 0, 0, 0.4)', justifyContent: 'center', alignItems: 'center', borderRadius: borderRadius.md, }, moreText: { color: colors.text.inverse, fontSize: fontSizes.xl, fontWeight: '500', }, // 紧凑模式样式 compactContainer: { marginTop: spacing.xs, }, compactGrid: { flexDirection: 'row', flexWrap: 'wrap', }, compactItem: { overflow: 'hidden', backgroundColor: colors.background.disabled, }, }); // 导入字体大小 import { fontSizes } from '../../theme'; export default ImageGrid;