diff --git a/app/index.tsx b/app/index.tsx index 7e8193e..885bcb7 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useCallback, useRef, useState } from "react"; -import { View, StyleSheet, ActivityIndicator, FlatList, Pressable, Animated, StatusBar, Platform } from "react-native"; +import { View, StyleSheet, ActivityIndicator, FlatList, Pressable, Animated, StatusBar, Platform, BackHandler, ToastAndroid } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { ThemedView } from "@/components/ThemedView"; import { ThemedText } from "@/components/ThemedText"; @@ -53,6 +53,41 @@ export default function HomeScreen() { }, [refreshPlayRecords]) ); + // 双击返回退出逻辑(只限当前页面) + const backPressTimeRef = useRef(null); + const exitToastShownRef = useRef(false); // 防止重复显示提示 + + useFocusEffect( + useCallback(() => { + const handleBackPress = () => { + const now = Date.now(); + + // 如果还没按过返回键,或距离上次超过2秒 + if (!backPressTimeRef.current || now - backPressTimeRef.current > 2000) { + backPressTimeRef.current = now; + ToastAndroid.show("再按一次返回键退出", ToastAndroid.SHORT); + return true; // 拦截返回事件,不退出 + } + + // 两次返回键间隔小于2秒,退出应用 + BackHandler.exitApp(); + return true; + }; + + // 仅限 Android 平台启用此功能 + if (Platform.OS === "android") { + const backHandler = BackHandler.addEventListener("hardwareBackPress", handleBackPress); + + // 返回首页时重置状态 + return () => { + backHandler.remove(); + backPressTimeRef.current = null; + exitToastShownRef.current = false; + }; + } + }, []) +); + // 统一的数据获取逻辑 useEffect(() => { if (!selectedCategory) return; diff --git a/components/CustomScrollView.tsx b/components/CustomScrollView.tsx index 50f0c10..abf545d 100644 --- a/components/CustomScrollView.tsx +++ b/components/CustomScrollView.tsx @@ -1,5 +1,5 @@ -import React, { useCallback } from "react"; -import { View, StyleSheet, ScrollView, ActivityIndicator } from "react-native"; +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"; @@ -20,7 +20,7 @@ interface CustomScrollViewProps { const CustomScrollView: React.FC = ({ data, renderItem, - numColumns, // 现在可选,如果不提供将使用响应式默认值 + numColumns, loading = false, loadingMore = false, error = null, @@ -29,8 +29,27 @@ const CustomScrollView: React.FC = ({ 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; @@ -40,6 +59,9 @@ const CustomScrollView: React.FC = ({ const { layoutMeasurement, contentOffset, contentSize } = nativeEvent; const isCloseToBottom = layoutMeasurement.height + contentOffset.y >= contentSize.height - loadMoreThreshold; + // 显示/隐藏返回顶部按钮 + setShowScrollToTop(contentOffset.y > 200); + if (isCloseToBottom && !loadingMore && onEndReached) { onEndReached(); } @@ -47,6 +69,14 @@ const CustomScrollView: React.FC = ({ [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)) { @@ -124,47 +154,72 @@ const CustomScrollView: React.FC = ({ 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; + + + {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; + return ( + + {row.map((item, itemIndex) => { + const actualIndex = rowIndex * effectiveColumns + itemIndex; + const isLastItemInPartialRow = !isFullRow && itemIndex === row.length - 1; + const itemStyle = isLastItemInPartialRow ? dynamicStyles.itemContainer : dynamicStyles.itemWithMargin; - return ( - - {renderItem({ item, index: actualIndex })} - - ); - })} - - ); - })} - {renderFooter()} - - ) : ( - - {emptyMessage} - + const cardProps = { + key: actualIndex, + style: isFullRow ? dynamicStyles.itemContainer : itemStyle, + }; + + return ( + + {renderItem({ item, index: actualIndex })} + + ); + })} + + ); + })} + {renderFooter()} + + ) : ( + + {emptyMessage} + + )} + + {deviceType!=='tv' && ( + + ⬆️ + )} - + ); - }; export default CustomScrollView;