import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Animated, StyleSheet, Text, TouchableOpacity, View, ViewStyle, InteractionManager } from 'react-native'; import { fetchHourlyStepSamples, fetchStepCount, HourlyStepData } from '@/utils/health'; import { logger } from '@/utils/logger'; import dayjs from 'dayjs'; import { Image } from 'expo-image'; import { useRouter } from 'expo-router'; import { AnimatedNumber } from './AnimatedNumber'; interface StepsCardProps { curDate: Date stepGoal: number; style?: ViewStyle; } const StepsCardOptimized: React.FC = ({ curDate, style, }) => { const router = useRouter(); const [stepCount, setStepCount] = useState(0) const [hourlySteps, setHourSteps] = useState([]) const [isLoading, setIsLoading] = useState(false) // 优化:使用debounce减少频繁的数据获取 const debounceTimer = useRef(null); const getStepData = useCallback(async (date: Date) => { try { setIsLoading(true); logger.info('获取步数数据...'); // 先获取步数,立即更新UI const steps = await fetchStepCount(date); setStepCount(steps); // 清除之前的定时器 if (debounceTimer.current) { clearTimeout(debounceTimer.current); } // 使用 InteractionManager 在空闲时获取更复杂的小时数据 InteractionManager.runAfterInteractions(async () => { try { const hourly = await fetchHourlyStepSamples(date); setHourSteps(hourly); } catch (error) { logger.error('获取小时步数数据失败:', error); } finally { setIsLoading(false); } }); } catch (error) { logger.error('获取步数数据失败:', error); setIsLoading(false); } }, []); useEffect(() => { if (curDate) { getStepData(curDate); } }, [curDate, getStepData]); // 优化:减少动画值数量,只为有数据的小时创建动画 const animatedValues = useRef>(new Map()).current; // 优化:简化柱状图数据计算,减少计算量 const chartData = useMemo(() => { if (!hourlySteps || hourlySteps.length === 0) { return Array.from({ length: 24 }, (_, i) => ({ hour: i, steps: 0, height: 0 })); } // 优化:只计算有数据的小时的最大步数 const activeSteps = hourlySteps.filter(data => data.steps > 0); if (activeSteps.length === 0) { return Array.from({ length: 24 }, (_, i) => ({ hour: i, steps: 0, height: 0 })); } const maxSteps = Math.max(...activeSteps.map(data => data.steps)); const maxHeight = 20; return hourlySteps.map(data => ({ ...data, height: data.steps > 0 ? (data.steps / maxSteps) * maxHeight : 0 })); }, [hourlySteps]); // 获取当前小时 const currentHour = new Date().getHours(); // 优化:延迟执行动画,减少UI阻塞 useEffect(() => { const hasData = chartData && chartData.length > 0 && chartData.some(data => data.steps > 0); if (hasData && !isLoading) { // 使用 InteractionManager 确保动画不会阻塞用户交互 InteractionManager.runAfterInteractions(() => { // 只为有数据的小时创建和执行动画 const animations = chartData .map((data, index) => { if (data.steps > 0) { // 懒创建动画值 if (!animatedValues.has(index)) { animatedValues.set(index, new Animated.Value(0)); } const animValue = animatedValues.get(index)!; animValue.setValue(0); // 使用更高性能的timing动画替代spring return Animated.timing(animValue, { toValue: 1, duration: 200, // 减少动画时长 useNativeDriver: false, }); } return null; }) .filter(Boolean) as Animated.CompositeAnimation[]; // 批量执行动画,提高性能 if (animations.length > 0) { Animated.stagger(50, animations).start(); } }); } }, [chartData, animatedValues, isLoading]); // 优化:使用React.memo包装复杂的渲染组件 const ChartBars = useMemo(() => { return chartData.map((data, index) => { // 判断是否是当前小时或者有活动的小时 const isActive = data.steps > 0; const isCurrent = index <= currentHour; // 优化:只为有数据的柱体创建动画插值 const animValue = animatedValues.get(index); let animatedScale: Animated.AnimatedInterpolation | undefined; let animatedOpacity: Animated.AnimatedInterpolation | undefined; if (animValue && isActive) { animatedScale = animValue.interpolate({ inputRange: [0, 1], outputRange: [0, 1], }); animatedOpacity = animValue.interpolate({ inputRange: [0, 1], outputRange: [0, 1], }); } return ( {/* 背景柱体 - 始终显示,使用相似色系的淡色 */} {/* 数据柱体 - 只有当有数据时才显示并执行动画 */} {isActive && ( )} ); }); }, [chartData, currentHour, animatedValues]); const CardContent = () => ( <> {/* 标题和步数显示 */} 步数 {isLoading && 加载中...} {/* 柱状图 */} {ChartBars} {/* 步数和目标显示 */} stepCount !== null ? `${Math.round(v)}` : '——'} resetToken={stepCount} /> ); return ( { // 传递当前日期参数到详情页 const dateParam = dayjs(curDate).format('YYYY-MM-DD'); router.push(`/steps/detail?date=${dateParam}`); }} activeOpacity={0.8} > ); }; const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'space-between', borderRadius: 20, padding: 16, shadowColor: '#000', shadowOffset: { width: 0, height: 4, }, shadowOpacity: 0.08, shadowRadius: 20, elevation: 8, }, header: { flexDirection: 'row', justifyContent: 'flex-start', alignItems: 'center', }, titleIcon: { width: 16, height: 16, marginRight: 6, resizeMode: 'contain', }, title: { fontSize: 14, color: '#192126', fontWeight: '600' }, loadingText: { fontSize: 10, color: '#666', marginLeft: 8, }, chartContainer: { flex: 1, justifyContent: 'center', marginTop: 6 }, chartWrapper: { width: '100%', alignItems: 'center', }, chartArea: { flexDirection: 'row', alignItems: 'flex-end', height: 20, width: '100%', maxWidth: 240, justifyContent: 'space-between', paddingHorizontal: 4, }, barContainer: { width: 4, height: 20, alignItems: 'center', justifyContent: 'flex-end', position: 'relative', }, chartBar: { width: 4, borderRadius: 1, position: 'absolute', bottom: 0, }, statsContainer: { alignItems: 'flex-start', marginTop: 6 }, stepCount: { fontSize: 18, fontWeight: '600', color: '#192126', }, }); export default StepsCardOptimized;