import React, { useState, useCallback, memo, useMemo } from 'react'; import { View, StyleSheet, LayoutChangeEvent, } from 'react-native'; import { FlashList } from '@shopify/flash-list'; export type MasonryListProps = { data: T[]; renderItem: (item: T, index: number) => React.ReactNode; keyExtractor: (item: T) => string; getItemHeight?: (item: T, width: number) => number | undefined; numColumns?: number; gap?: number; estimatedItemHeight?: number; onEndReached?: () => void; onEndReachedThreshold?: number; onRefresh?: () => void; refreshing?: boolean; contentContainerStyle?: object; }; type RowItem = { type: 'row'; items: T[]; rowIndex: number; }; function MasonryListInner({ data, renderItem, keyExtractor, getItemHeight = () => undefined, numColumns = 2, gap = 8, estimatedItemHeight = 200, onEndReached, onEndReachedThreshold = 0.5, onRefresh, refreshing = false, contentContainerStyle, }: MasonryListProps) { const [containerWidth, setContainerWidth] = useState(0); const handleLayout = useCallback((event: LayoutChangeEvent) => { const { width } = event.nativeEvent.layout; setContainerWidth(width); }, []); const rows = useMemo(() => { const result: RowItem[] = []; for (let i = 0; i < data.length; i += numColumns) { result.push({ type: 'row', items: data.slice(i, i + numColumns), rowIndex: i / numColumns, }); } return result; }, [data, numColumns]); const itemWidth = useMemo(() => { if (containerWidth === 0) return 0; return (containerWidth - gap * (numColumns - 1)) / numColumns; }, [containerWidth, gap, numColumns]); const renderRow = useCallback(({ item: row }: { item: RowItem }) => { return ( {row.items.map((item, index) => { const height = getItemHeight(item, itemWidth) || estimatedItemHeight; return ( {renderItem(item, row.rowIndex * numColumns + index)} ); })} ); }, [renderItem, keyExtractor, itemWidth, gap, numColumns, getItemHeight, estimatedItemHeight]); if (containerWidth === 0) { return ; } return ( `row-${item.rowIndex}`} onEndReached={onEndReached} onEndReachedThreshold={onEndReachedThreshold} onRefresh={onRefresh} refreshing={refreshing} contentContainerStyle={contentContainerStyle} /> ); } export const MasonryList = memo(MasonryListInner) as typeof MasonryListInner; const styles = StyleSheet.create({ container: { flex: 1, }, row: { flexDirection: 'row', }, item: {}, });