mirror of
https://github.com/zimplexing/OrionTV.git
synced 2026-02-16 05:04:42 +08:00
feat: implement back button logic — single tap to scroll to top, double tap to exit.
This commit is contained in:
@@ -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<CustomScrollViewProps> = ({
|
||||
data,
|
||||
renderItem,
|
||||
numColumns, // 现在可选,如果不提供将使用响应式默认值
|
||||
numColumns,
|
||||
loading = false,
|
||||
loadingMore = false,
|
||||
error = null,
|
||||
@@ -29,8 +29,27 @@ const CustomScrollView: React.FC<CustomScrollViewProps> = ({
|
||||
emptyMessage = "暂无内容",
|
||||
ListFooterComponent,
|
||||
}) => {
|
||||
const scrollViewRef = useRef<ScrollView>(null);
|
||||
const firstCardRef = useRef<any>(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<CustomScrollViewProps> = ({
|
||||
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<CustomScrollViewProps> = ({
|
||||
[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<CustomScrollViewProps> = ({
|
||||
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 (
|
||||
<ScrollView
|
||||
contentContainerStyle={dynamicStyles.listContent}
|
||||
onScroll={handleScroll}
|
||||
scrollEventThrottle={16}
|
||||
showsVerticalScrollIndicator={responsiveConfig.deviceType !== 'tv'}
|
||||
>
|
||||
{data.length > 0 ? (
|
||||
<>
|
||||
{rows.map((row, rowIndex) => {
|
||||
const isFullRow = row.length === effectiveColumns;
|
||||
const rowStyle = isFullRow ? dynamicStyles.fullRowContainer : dynamicStyles.partialRowContainer;
|
||||
<View style={{ flex: 1 }}>
|
||||
<ScrollView
|
||||
ref={scrollViewRef}
|
||||
contentContainerStyle={dynamicStyles.listContent}
|
||||
onScroll={handleScroll}
|
||||
scrollEventThrottle={16}
|
||||
showsVerticalScrollIndicator={responsiveConfig.deviceType !== 'tv'}
|
||||
>
|
||||
{data.length > 0 ? (
|
||||
<>
|
||||
{rows.map((row, rowIndex) => {
|
||||
const isFullRow = row.length === effectiveColumns;
|
||||
const rowStyle = isFullRow ? dynamicStyles.fullRowContainer : dynamicStyles.partialRowContainer;
|
||||
|
||||
return (
|
||||
<View key={rowIndex} style={[dynamicStyles.rowContainer, rowStyle]}>
|
||||
{row.map((item, itemIndex) => {
|
||||
const actualIndex = rowIndex * effectiveColumns + itemIndex;
|
||||
const isLastItemInPartialRow = !isFullRow && itemIndex === row.length - 1;
|
||||
const itemStyle = isLastItemInPartialRow ? dynamicStyles.itemContainer : dynamicStyles.itemWithMargin;
|
||||
return (
|
||||
<View key={rowIndex} style={[dynamicStyles.rowContainer, rowStyle]}>
|
||||
{row.map((item, itemIndex) => {
|
||||
const actualIndex = rowIndex * effectiveColumns + itemIndex;
|
||||
const isLastItemInPartialRow = !isFullRow && itemIndex === row.length - 1;
|
||||
const itemStyle = isLastItemInPartialRow ? dynamicStyles.itemContainer : dynamicStyles.itemWithMargin;
|
||||
|
||||
return (
|
||||
<View key={actualIndex} style={isFullRow ? dynamicStyles.itemContainer : itemStyle}>
|
||||
{renderItem({ item, index: actualIndex })}
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
{renderFooter()}
|
||||
</>
|
||||
) : (
|
||||
<View style={commonStyles.center}>
|
||||
<ThemedText>{emptyMessage}</ThemedText>
|
||||
</View>
|
||||
const cardProps = {
|
||||
key: actualIndex,
|
||||
style: isFullRow ? dynamicStyles.itemContainer : itemStyle,
|
||||
};
|
||||
|
||||
return (
|
||||
<View {...cardProps}>
|
||||
{renderItem({ item, index: actualIndex })}
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
{renderFooter()}
|
||||
</>
|
||||
) : (
|
||||
<View style={commonStyles.center}>
|
||||
<ThemedText>{emptyMessage}</ThemedText>
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
{deviceType!=='tv' && (
|
||||
<TouchableOpacity
|
||||
style={dynamicStyles.scrollToTopButton}
|
||||
onPress={scrollToTop}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<ThemedText>⬆️</ThemedText>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
export default CustomScrollView;
|
||||
|
||||
Reference in New Issue
Block a user