diff --git a/app/(tabs)/calendar/hooks/useCalendarState.ts b/app/(tabs)/calendar/hooks/useCalendarState.ts index 4cfd4e333..8c88a91aa 100644 --- a/app/(tabs)/calendar/hooks/useCalendarState.ts +++ b/app/(tabs)/calendar/hooks/useCalendarState.ts @@ -1,5 +1,5 @@ -import { useState, useRef, useEffect, useCallback } from 'react'; -import { Dimensions, FlatList } from 'react-native'; +import { useState, useRef, useEffect, useCallback } from "react"; +import { Dimensions, FlatList } from "react-native"; import { getWeekNumberFromDate } from "@/database/useHomework"; import { warn } from "@/utils/logger/logger"; @@ -28,17 +28,17 @@ export function useCalendarState() { base.setHours(0, 0, 0, 0); const target = new Date(d); target.setHours(0, 0, 0, 0); - const diff = Math.round((target.getTime() - base.getTime()) / (1000 * 60 * 60 * 24)); + const diff = Math.round( + (target.getTime() - base.getTime()) / (1000 * 60 * 60 * 24) + ); return INITIAL_INDEX + diff; }, []); const handleDateChange = useCallback((newDate: Date) => { setDate(newDate); const newWeekNumber = getWeekNumberFromDate(newDate); - if (newWeekNumber !== weekNumber) { - setWeekNumber(newWeekNumber); - } - }, [weekNumber]); + setWeekNumber(prev => (prev !== newWeekNumber ? newWeekNumber : prev)); + }, []); // Sync FlatList with date useEffect(() => { @@ -58,7 +58,7 @@ export function useCalendarState() { animated: false, }); } catch (e) { - warn(String(e)) + warn(String(e)); } } } @@ -68,31 +68,41 @@ export function useCalendarState() { } }, [date, getIndexFromDate, currentIndex, weekNumber]); - const onMomentumScrollEnd = useCallback((e: any) => { - const newIndex = Math.round(e.nativeEvent.contentOffset.x / windowWidth); - if (newIndex !== currentIndex) { - setCurrentIndex(newIndex); - const newDate = getDateFromIndex(newIndex); - setDate((prev) => prev.getTime() !== newDate.getTime() ? newDate : prev); - } - }, [windowWidth, currentIndex, getDateFromIndex]); + const onMomentumScrollEnd = useCallback( + (e: any) => { + const newIndex = Math.round(e.nativeEvent.contentOffset.x / windowWidth); + if (newIndex !== currentIndex) { + setCurrentIndex(newIndex); + const newDate = getDateFromIndex(newIndex); + setDate(prev => + prev.getTime() !== newDate.getTime() ? newDate : prev + ); + } + }, + [windowWidth, currentIndex, getDateFromIndex] + ); const lastEmittedIndex = useRef(currentIndex); - const onScroll = useCallback((e: any) => { - const offsetX = e.nativeEvent.contentOffset.x; - const newIndex = Math.round(offsetX / windowWidth); - if (newIndex !== lastEmittedIndex.current) { - lastEmittedIndex.current = newIndex; - setCurrentIndex(newIndex); - const newDate = getDateFromIndex(newIndex); - setDate((prev) => prev.getTime() !== newDate.getTime() ? newDate : prev); - const newWeekNumber = getWeekNumberFromDate(newDate); - if (newWeekNumber !== weekNumber) { - setWeekNumber(newWeekNumber); + const onScroll = useCallback( + (e: any) => { + const offsetX = e.nativeEvent.contentOffset.x; + const newIndex = Math.round(offsetX / windowWidth); + if (newIndex !== lastEmittedIndex.current) { + lastEmittedIndex.current = newIndex; + setCurrentIndex(newIndex); + const newDate = getDateFromIndex(newIndex); + setDate(prev => + prev.getTime() !== newDate.getTime() ? newDate : prev + ); + const newWeekNumber = getWeekNumberFromDate(newDate); + if (newWeekNumber !== weekNumber) { + setWeekNumber(newWeekNumber); + } } - } - }, [windowWidth, getDateFromIndex, weekNumber]); + }, + [windowWidth, getDateFromIndex, weekNumber] + ); return { date, @@ -107,6 +117,6 @@ export function useCalendarState() { onMomentumScrollEnd, onScroll, INITIAL_INDEX, - windowWidth + windowWidth, }; } diff --git a/app/(tabs)/calendar/index.tsx b/app/(tabs)/calendar/index.tsx index f06512be8..3328a11ed 100644 --- a/app/(tabs)/calendar/index.tsx +++ b/app/(tabs)/calendar/index.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useCallback, useState } from "react"; +import React, { useRef, useCallback, useState, useMemo } from "react"; import { View, FlatList, StyleSheet } from "react-native"; import { useTheme } from "@react-navigation/native"; import { useCalendarState } from "./hooks/useCalendarState"; @@ -20,7 +20,6 @@ export default function TabOneScreen() { const { date, weekNumber, - currentIndex, flatListRef, getDateFromIndex, handleDateChange, @@ -37,15 +36,26 @@ export default function TabOneScreen() { isLoading } = useTimetableData(weekNumber, date); + const timetableMap = useMemo(() => { + const map = new Map(); + + timetable.forEach(day => { + const normalized = new Date(day.date); + normalized.setHours(0, 0, 0, 0); + const key = normalized.getTime(); + map.set(key, day.courses); + }); + + return map; + }, [timetable]); + const renderDay = useCallback(({ index }: { index: number }) => { const dayDate = getDateFromIndex(index); const normalizedDate = new Date(dayDate); + normalizedDate.setHours(0, 0, 0, 0); - const dayCourses = timetable.find(d => { - const dDate = new Date(d.date); - dDate.setHours(0, 0, 0, 0); - return dDate.getTime() === normalizedDate.getTime(); - })?.courses || []; + + const dayCourses = timetableMap.get(normalizedDate.getTime()) || []; return ( ); - }, [getDateFromIndex, timetable, manualRefreshing, handleRefresh, colors, headerHeight]); + }, [getDateFromIndex, timetableMap, manualRefreshing, handleRefresh, colors, headerHeight, insets, tabBarHeight]); + + const extraData = useMemo(() => ({ + manualRefreshing, + headerHeight, + colors, + timetableVersion: timetable.length + }), [manualRefreshing, headerHeight, colors, timetable.length]); return ( <> @@ -83,18 +100,23 @@ export default function TabOneScreen() { renderItem={renderDay} keyExtractor={(_, index) => "renderDay:" + String(index)} onScroll={onScroll} - decelerationRate={0.98} - disableIntervalMomentum={true} + + decelerationRate="fast" + disableIntervalMomentum={false} scrollEventThrottle={16} onMomentumScrollEnd={onMomentumScrollEnd} snapToInterval={windowWidth} + snapToAlignment="start" bounces={false} - windowSize={4} - maxToRenderPerBatch={3} - initialNumToRender={3} + + windowSize={5} + maxToRenderPerBatch={2} + initialNumToRender={1} + updateCellsBatchingPeriod={50} + showsVerticalScrollIndicator={false} removeClippedSubviews - extraData={{ manualRefreshing, headerHeight, colors, timetable }} + extraData={extraData} />