优化联赛选择模态框性能

This commit is contained in:
xianyi
2026-01-14 10:45:22 +08:00
parent c936f3ba6b
commit 7bdbdbaa18
3 changed files with 230 additions and 193 deletions

View File

@@ -10,7 +10,7 @@ import { Colors } from "@/constants/theme";
import { useTheme } from "@/context/ThemeContext"; import { useTheme } from "@/context/ThemeContext";
import { fetchLeagues, fetchSports, fetchTodayMatches } from "@/lib/api"; import { fetchLeagues, fetchSports, fetchTodayMatches } from "@/lib/api";
import { League, Match, Sport } from "@/types/api"; import { League, Match, Sport } from "@/types/api";
import React, { useEffect, useState } from "react"; import React, { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import {
ActivityIndicator, ActivityIndicator,
@@ -168,6 +168,29 @@ export default function HomeScreen() {
console.log("Selected league:", leagueKey); console.log("Selected league:", leagueKey);
}; };
// 缓存运动选项,避免每次渲染都重新计算
const sportOptions = useMemo(() => {
return sports.map((s) => {
const sportKeyMap: { [key: number]: string } = {
1: "football",
2: "basketball",
3: "tennis",
4: "cricket",
5: "baseball",
6: "badminton",
7: "snooker",
8: "volleyball",
};
const sportKey = sportKeyMap[s.id] || s.name.toLowerCase();
return {
id: s.id,
label: s.name,
value: s.id,
icon: s.icon,
};
});
}, [sports]);
const renderHeader = () => ( const renderHeader = () => (
<View style={styles.filterContainer}> <View style={styles.filterContainer}>
{/* Time/League Filter Toggle */} {/* Time/League Filter Toggle */}
@@ -265,41 +288,28 @@ export default function HomeScreen() {
/> />
)} )}
{/* Modals */} {/* Modals - 条件渲染,只在可见时渲染 */}
{showSportModal && (
<SelectionModal <SelectionModal
visible={showSportModal} visible={showSportModal}
onClose={() => setShowSportModal(false)} onClose={() => setShowSportModal(false)}
title={t("home.select_sport")} title={t("home.select_sport")}
options={sports.map((s) => { options={sportOptions}
const sportKeyMap: { [key: number]: string } = {
1: "football",
2: "basketball",
3: "tennis",
4: "cricket",
5: "baseball",
6: "badminton",
7: "snooker",
8: "volleyball",
};
const sportKey = sportKeyMap[s.id] || s.name.toLowerCase();
return {
id: s.id,
label: s.name, // 保留原始名称用于图标识别
value: s.id,
icon: s.icon,
};
})}
selectedValue={selectedSportId} selectedValue={selectedSportId}
onSelect={setSelectedSportId} onSelect={setSelectedSportId}
/> />
)}
{showCalendarModal && (
<CalendarModal <CalendarModal
visible={showCalendarModal} visible={showCalendarModal}
onClose={() => setShowCalendarModal(false)} onClose={() => setShowCalendarModal(false)}
selectedDate={selectedDate} selectedDate={selectedDate}
onSelectDate={setSelectedDate} onSelectDate={setSelectedDate}
/> />
)}
{showLeagueModal && (
<LeagueModal <LeagueModal
visible={showLeagueModal} visible={showLeagueModal}
onClose={() => setShowLeagueModal(false)} onClose={() => setShowLeagueModal(false)}
@@ -308,6 +318,7 @@ export default function HomeScreen() {
loading={loadingLeagues} loading={loadingLeagues}
onSelect={handleLeagueSelect} onSelect={handleLeagueSelect}
/> />
)}
</ThemedView> </ThemedView>
); );
} }

View File

@@ -4,9 +4,9 @@ import { Colors } from "@/constants/theme";
import { useTheme } from "@/context/ThemeContext"; import { useTheme } from "@/context/ThemeContext";
import { League } from "@/types/api"; import { League } from "@/types/api";
import { Image } from "expo-image"; import { Image } from "expo-image";
import React from "react"; import React, { useMemo } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { ActivityIndicator, Modal, Pressable, ScrollView, StyleSheet, View } from "react-native"; import { ActivityIndicator, FlatList, Modal, Pressable, StyleSheet, View } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context"; import { SafeAreaView } from "react-native-safe-area-context";
interface LeagueModalProps { interface LeagueModalProps {
@@ -31,55 +31,15 @@ export function LeagueModal({
const isDark = theme === "dark"; const isDark = theme === "dark";
const bg = isDark ? "#1C1C1E" : "#FFFFFF"; const bg = isDark ? "#1C1C1E" : "#FFFFFF";
const text = isDark ? "#FFFFFF" : "#000000"; const text = isDark ? "#FFFFFF" : "#000000";
return (
<Modal
visible={visible}
transparent
animationType="slide"
onRequestClose={onClose}
>
<Pressable style={styles.overlay} onPress={onClose}>
<View />
</Pressable>
<View style={[styles.sheet, { backgroundColor: bg }]}>
<SafeAreaView edges={["bottom"]}>
<View style={styles.header}>
<ThemedText type="subtitle" style={styles.title}>
{t("home.select_league")}
</ThemedText>
<Pressable onPress={onClose} style={styles.closeButton}>
<IconSymbol name="close" size={24} color={text} />
</Pressable>
</View>
<ScrollView style={styles.scrollView} showsVerticalScrollIndicator={false}>
{loading ? (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={Colors[theme].tint} />
<ThemedText style={styles.loadingText}>
{t("home.loading")}
</ThemedText>
</View>
) : leagues.length === 0 ? (
<View style={styles.emptyContainer}>
<ThemedText style={styles.emptyText}>
{t("home.no_leagues")}
</ThemedText>
</View>
) : (
leagues.map((league) => {
const isSelected = selectedLeagueKey === league.key;
const optionBg = isDark ? "#2C2C2E" : "#F5F5F5"; const optionBg = isDark ? "#2C2C2E" : "#F5F5F5";
const iconBg = isDark ? "#3A3A3C" : "#E5E5EA"; const iconBg = isDark ? "#3A3A3C" : "#E5E5EA";
const renderLeagueItem = ({ item: league }: { item: League }) => {
const isSelected = selectedLeagueKey === league.key;
return ( return (
<Pressable <Pressable
key={league.id} style={[styles.option, { backgroundColor: optionBg }]}
style={[
styles.option,
{ backgroundColor: optionBg },
]}
onPress={() => { onPress={() => {
onSelect(league.key); onSelect(league.key);
onClose(); onClose();
@@ -130,9 +90,68 @@ export function LeagueModal({
/> />
</Pressable> </Pressable>
); );
}) };
)}
</ScrollView> const renderContent = () => {
if (loading) {
return (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={Colors[theme].tint} />
<ThemedText style={styles.loadingText}>
{t("home.loading")}
</ThemedText>
</View>
);
}
if (leagues.length === 0) {
return (
<View style={styles.emptyContainer}>
<ThemedText style={styles.emptyText}>
{t("home.no_leagues")}
</ThemedText>
</View>
);
}
return (
<FlatList
data={leagues}
renderItem={renderLeagueItem}
keyExtractor={(item) => item.id.toString()}
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.listContent}
removeClippedSubviews={true}
maxToRenderPerBatch={10}
updateCellsBatchingPeriod={50}
windowSize={10}
initialNumToRender={10}
/>
);
};
return (
<Modal
visible={visible}
transparent
animationType="slide"
onRequestClose={onClose}
>
<Pressable style={styles.overlay} onPress={onClose}>
<View />
</Pressable>
<View style={[styles.sheet, { backgroundColor: bg }]}>
<SafeAreaView edges={["bottom"]}>
<View style={styles.header}>
<ThemedText type="subtitle" style={styles.title}>
{t("home.select_league")}
</ThemedText>
<Pressable onPress={onClose} style={styles.closeButton}>
<IconSymbol name="close" size={24} color={text} />
</Pressable>
</View>
{renderContent()}
</SafeAreaView> </SafeAreaView>
</View> </View>
</Modal> </Modal>
@@ -166,8 +185,8 @@ const styles = StyleSheet.create({
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
}, },
scrollView: { listContent: {
maxHeight: 400, paddingBottom: 20,
}, },
loadingContainer: { loadingContainer: {
padding: 40, padding: 40,

View File

@@ -5,7 +5,7 @@ import { useTheme } from "@/context/ThemeContext";
import { Image } from "expo-image"; import { Image } from "expo-image";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Modal, Pressable, ScrollView, StyleSheet, View } from "react-native"; import { FlatList, Modal, Pressable, StyleSheet, View } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context"; import { SafeAreaView } from "react-native-safe-area-context";
interface SelectionOption { interface SelectionOption {
@@ -119,34 +119,12 @@ export function SelectionModal({
const isDark = theme === "dark"; const isDark = theme === "dark";
const bg = isDark ? "#1C1C1E" : "#FFFFFF"; const bg = isDark ? "#1C1C1E" : "#FFFFFF";
const text = isDark ? "#FFFFFF" : "#000000"; const text = isDark ? "#FFFFFF" : "#000000";
return (
<Modal
visible={visible}
transparent
animationType="slide"
onRequestClose={onClose}
>
<Pressable style={styles.overlay} onPress={onClose}>
<View />
</Pressable>
<View style={[styles.sheet, { backgroundColor: bg }]}>
<SafeAreaView edges={["bottom"]}>
<View style={styles.header}>
<ThemedText type="subtitle" style={styles.title}>
{title}
</ThemedText>
<Pressable onPress={onClose} style={styles.closeButton}>
<IconSymbol name="close" size={24} color={text} />
</Pressable>
</View>
<ScrollView style={styles.scrollView} showsVerticalScrollIndicator={false}>
{options.map((opt) => {
const isSelected = selectedValue === opt.value;
const optionBg = isDark ? "#2C2C2E" : "#F5F5F5"; const optionBg = isDark ? "#2C2C2E" : "#F5F5F5";
const iconBg = isDark ? "#3A3A3C" : "#E5E5EA"; const iconBg = isDark ? "#3A3A3C" : "#E5E5EA";
const renderOption = ({ item: opt }: { item: SelectionOption }) => {
const isSelected = selectedValue === opt.value;
// 获取运动键用于i18n和图标 // 获取运动键用于i18n和图标
const sportId = typeof opt.id === "number" ? opt.id : undefined; const sportId = typeof opt.id === "number" ? opt.id : undefined;
const sportKey = sportId ? getSportKeyById(sportId) : getSportIconName(opt.label, sportId); const sportKey = sportId ? getSportKeyById(sportId) : getSportIconName(opt.label, sportId);
@@ -154,11 +132,7 @@ export function SelectionModal({
return ( return (
<Pressable <Pressable
key={opt.id} style={[styles.option, { backgroundColor: optionBg }]}
style={[
styles.option,
{ backgroundColor: optionBg },
]}
onPress={() => { onPress={() => {
onSelect(opt.value); onSelect(opt.value);
onClose(); onClose();
@@ -203,8 +177,41 @@ export function SelectionModal({
/> />
</Pressable> </Pressable>
); );
})} };
</ScrollView>
return (
<Modal
visible={visible}
transparent
animationType="slide"
onRequestClose={onClose}
>
<Pressable style={styles.overlay} onPress={onClose}>
<View />
</Pressable>
<View style={[styles.sheet, { backgroundColor: bg }]}>
<SafeAreaView edges={["bottom"]}>
<View style={styles.header}>
<ThemedText type="subtitle" style={styles.title}>
{title}
</ThemedText>
<Pressable onPress={onClose} style={styles.closeButton}>
<IconSymbol name="close" size={24} color={text} />
</Pressable>
</View>
<FlatList
data={options}
renderItem={renderOption}
keyExtractor={(item) => item.id.toString()}
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.listContent}
removeClippedSubviews={true}
maxToRenderPerBatch={10}
updateCellsBatchingPeriod={50}
windowSize={10}
initialNumToRender={10}
/>
</SafeAreaView> </SafeAreaView>
</View> </View>
</Modal> </Modal>
@@ -238,8 +245,8 @@ const styles = StyleSheet.create({
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
}, },
scrollView: { listContent: {
maxHeight: 400, paddingBottom: 20,
}, },
option: { option: {
flexDirection: "row", flexDirection: "row",