import React, { useCallback, useRef, useState, useEffect } from "react"; import { View, StyleSheet, ScrollView, ActivityIndicator, TouchableOpacity, BackHandler } from "react-native"; import { ThemedText } from "@/components/ThemedText"; import { useResponsiveLayout } from "@/hooks/useResponsiveLayout"; import { getCommonResponsiveStyles } from "@/utils/ResponsiveStyles"; interface CustomScrollViewProps { data: any[]; renderItem: ({ item, index }: { item: any; index: number }) => React.ReactNode; numColumns?: number; // 如果不提供,将使用响应式默认值 loading?: boolean; loadingMore?: boolean; error?: string | null; onEndReached?: () => void; loadMoreThreshold?: number; emptyMessage?: string; ListFooterComponent?: React.ComponentType | React.ReactElement | null; } const CustomScrollView: React.FC = ({ data, renderItem, numColumns, loading = false, loadingMore = false, error = null, onEndReached, loadMoreThreshold = 200, emptyMessage = "暂无内容", ListFooterComponent, }) => { const scrollViewRef = useRef(null); const firstCardRef = useRef(null); // <--- 新增 const [showScrollToTop, setShowScrollToTop] = useState(false); const responsiveConfig = useResponsiveLayout(); const commonStyles = getCommonResponsiveStyles(responsiveConfig); const { deviceType } = responsiveConfig; // 添加返回键处理逻辑 useEffect(() => { if (deviceType === 'tv') { const backHandler = BackHandler.addEventListener('hardwareBackPress', () => { if (showScrollToTop) { scrollToTop(); return true; // 阻止默认的返回行为 } return false; // 允许默认的返回行为 }); return () => backHandler.remove(); } }, [showScrollToTop,deviceType]); // 使用响应式列数,如果没有明确指定的话 const effectiveColumns = numColumns || responsiveConfig.columns; const handleScroll = useCallback( ({ nativeEvent }: { nativeEvent: any }) => { const { layoutMeasurement, contentOffset, contentSize } = nativeEvent; const isCloseToBottom = layoutMeasurement.height + contentOffset.y >= contentSize.height - loadMoreThreshold; // 显示/隐藏返回顶部按钮 setShowScrollToTop(contentOffset.y > 200); if (isCloseToBottom && !loadingMore && onEndReached) { onEndReached(); } }, [onEndReached, loadingMore, loadMoreThreshold] ); const scrollToTop = () => { scrollViewRef.current?.scrollTo({ y: 0, animated: true }); // 滚动动画结束后聚焦第一个卡片 setTimeout(() => { firstCardRef.current?.focus(); }, 500); // 500ms 适配大多数动画时长 }; const renderFooter = () => { if (ListFooterComponent) { if (React.isValidElement(ListFooterComponent)) { return ListFooterComponent; } else if (typeof ListFooterComponent === "function") { const Component = ListFooterComponent as React.ComponentType; return ; } return null; } if (loadingMore) { return ; } return null; }; if (loading) { return ( ); } if (error) { return ( {error} ); } if (data.length === 0) { return ( {emptyMessage} ); } // 将数据按行分组 const groupItemsByRow = (items: any[], columns: number) => { const rows = []; for (let i = 0; i < items.length; i += columns) { rows.push(items.slice(i, i + columns)); } return rows; }; const rows = groupItemsByRow(data, effectiveColumns); // 动态样式 const dynamicStyles = StyleSheet.create({ listContent: { paddingBottom: responsiveConfig.spacing * 2, paddingHorizontal: responsiveConfig.spacing / 2, }, rowContainer: { flexDirection: "row", marginBottom: responsiveConfig.spacing, }, fullRowContainer: { justifyContent: "space-around", marginRight: responsiveConfig.spacing / 2, }, partialRowContainer: { justifyContent: "flex-start", }, itemContainer: { width: responsiveConfig.cardWidth, }, itemWithMargin: { width: responsiveConfig.cardWidth, marginRight: responsiveConfig.spacing, }, scrollToTopButton: { position: 'absolute', right: responsiveConfig.spacing, bottom: responsiveConfig.spacing * 2, backgroundColor: 'rgba(0, 0, 0, 0.6)', padding: responsiveConfig.spacing, borderRadius: responsiveConfig.spacing, opacity: showScrollToTop ? 1 : 0, }, }); return ( {data.length > 0 ? ( <> {rows.map((row, rowIndex) => { const isFullRow = row.length === effectiveColumns; const rowStyle = isFullRow ? dynamicStyles.fullRowContainer : dynamicStyles.partialRowContainer; return ( {row.map((item, itemIndex) => { const actualIndex = rowIndex * effectiveColumns + itemIndex; const isLastItemInPartialRow = !isFullRow && itemIndex === row.length - 1; const itemStyle = isLastItemInPartialRow ? dynamicStyles.itemContainer : dynamicStyles.itemWithMargin; const cardProps = { key: actualIndex, style: isFullRow ? dynamicStyles.itemContainer : itemStyle, }; return ( {renderItem({ item, index: actualIndex })} ); })} ); })} {renderFooter()} ) : ( {emptyMessage} )} {deviceType!=='tv' && ( ⬆️ )} ); }; export default CustomScrollView;