Files
digital-pilates/components/CalorieRingChart.tsx

292 lines
8.3 KiB
TypeScript

import { ThemedText } from '@/components/ThemedText';
import { useThemeColor } from '@/hooks/useThemeColor';
import React, { useEffect, useRef } from 'react';
import { Animated, StyleSheet, View } from 'react-native';
import Svg, { Circle } from 'react-native-svg';
const AnimatedCircle = Animated.createAnimatedComponent(Circle);
export type CalorieRingChartProps = {
metabolism: number;
exercise: number;
consumed: number;
protein: number;
fat: number;
carbs: number;
proteinGoal: number;
fatGoal: number;
carbsGoal: number;
};
export function CalorieRingChart({
metabolism,
exercise,
consumed,
protein,
fat,
carbs,
proteinGoal,
fatGoal,
carbsGoal,
}: CalorieRingChartProps) {
const surfaceColor = useThemeColor({}, 'surface');
const textColor = useThemeColor({}, 'text');
const textSecondaryColor = useThemeColor({}, 'textSecondary');
// 动画值
const animatedProgress = useRef(new Animated.Value(0)).current;
// 计算还能吃的卡路里:代谢 + 运动 - 饮食
const remainingCalories = metabolism + exercise - consumed;
const canEat = Math.max(0, remainingCalories);
// 计算进度百分比 (用于圆环显示)
const totalAvailable = metabolism + exercise;
const progressPercentage = totalAvailable > 0 ? Math.min((consumed / totalAvailable) * 100, 100) : 0;
// 圆环参数 - 减小尺寸以优化空间占用
const radius = 48;
const strokeWidth = 8; // 增加圆环厚度
const center = radius + strokeWidth;
const circumference = 2 * Math.PI * radius;
const strokeDasharray = circumference;
// 动画效果
useEffect(() => {
Animated.timing(animatedProgress, {
toValue: progressPercentage,
duration: 600,
useNativeDriver: false,
}).start();
}, [progressPercentage]);
// 使用动画值计算strokeDashoffset
const strokeDashoffset = animatedProgress.interpolate({
inputRange: [0, 100],
outputRange: [circumference, 0],
extrapolate: 'clamp',
});
return (
<View style={[styles.container, { backgroundColor: surfaceColor }]}>
{/* 左上角公式展示 */}
<View style={styles.formulaContainer}>
<ThemedText style={[styles.formulaText, { color: textSecondaryColor }]}>
= + -
</ThemedText>
</View>
{/* 主要内容区域 */}
<View style={styles.mainContent}>
{/* 左侧圆环图 */}
<View style={styles.chartContainer}>
<Svg width={center * 2} height={center * 2}>
{/* 背景圆环 */}
<Circle
cx={center}
cy={center}
r={radius}
stroke="#F0F0F0"
strokeWidth={strokeWidth}
fill="none"
/>
{/* 进度圆环 - 保持固定颜色 */}
<AnimatedCircle
cx={center}
cy={center}
r={radius}
stroke={progressPercentage > 80 ? "#FF6B6B" : "#4ECDC4"}
strokeWidth={strokeWidth}
fill="none"
strokeDasharray={`${strokeDasharray}`}
strokeDashoffset={strokeDashoffset}
strokeLinecap="round"
transform={`rotate(-90 ${center} ${center})`}
/>
</Svg>
{/* 中心内容 */}
<View style={styles.centerContent}>
<ThemedText style={[styles.centerLabel, { color: textSecondaryColor }]}>
</ThemedText>
<ThemedText style={[styles.centerValue, { color: textColor }]}>
{Math.round(canEat)}
</ThemedText>
</View>
</View>
{/* 右侧数据展示 */}
<View style={styles.dataContainer}>
<View style={styles.dataBackground}>
{/* 左右两列布局 */}
<View style={styles.dataColumns}>
{/* 左列:卡路里数据 */}
<View style={styles.dataColumn}>
<View style={styles.dataItem}>
<ThemedText style={[styles.dataLabel, { color: textSecondaryColor }]}></ThemedText>
<ThemedText style={[styles.dataValue, { color: textColor }]}>
{Math.round(metabolism)}
</ThemedText>
</View>
<View style={styles.dataItem}>
<ThemedText style={[styles.dataLabel, { color: textSecondaryColor }]}></ThemedText>
<ThemedText style={[styles.dataValue, { color: textColor }]}>
{Math.round(exercise)}
</ThemedText>
</View>
<View style={styles.dataItem}>
<ThemedText style={[styles.dataLabel, { color: textSecondaryColor }]}></ThemedText>
<ThemedText style={[styles.dataValue, { color: textColor }]}>
{Math.round(consumed)}
</ThemedText>
</View>
</View>
{/* 右列:营养数据 */}
<View style={styles.dataColumn}>
<View style={styles.dataItem}>
<ThemedText style={[styles.dataLabel, { color: textSecondaryColor }]}></ThemedText>
<ThemedText style={[styles.dataValue, { color: textColor }]}>
{Math.round(protein)}g
</ThemedText>
</View>
<View style={styles.dataItem}>
<ThemedText style={[styles.dataLabel, { color: textSecondaryColor }]}></ThemedText>
<ThemedText style={[styles.dataValue, { color: textColor }]}>
{Math.round(fat)}g
</ThemedText>
</View>
<View style={styles.dataItem}>
<ThemedText style={[styles.dataLabel, { color: textSecondaryColor }]}></ThemedText>
<ThemedText style={[styles.dataValue, { color: textColor }]}>
{Math.round(carbs)}g
</ThemedText>
</View>
</View>
</View>
</View>
</View>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
backgroundColor: '#FFFFFF',
borderRadius: 16,
padding: 16,
marginHorizontal: 16,
marginBottom: 8,
shadowColor: '#000000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.04,
shadowRadius: 8,
elevation: 2,
},
formulaContainer: {
alignItems: 'flex-start',
marginBottom: 12,
},
formulaText: {
fontSize: 12,
fontWeight: '500',
color: '#999999',
lineHeight: 16,
},
mainContent: {
width: '100%',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: 0, // 移除底部间距,因为不再有底部营养容器
paddingHorizontal: 8,
},
chartContainer: {
position: 'relative',
alignItems: 'center',
justifyContent: 'center',
width: 112, // 减少宽度以匹配更小的圆环 (48*2 + 8*2)
flexShrink: 0,
},
centerContent: {
position: 'absolute',
alignItems: 'center',
justifyContent: 'center',
},
centerLabel: {
fontSize: 11,
fontWeight: '500',
color: '#999999',
marginBottom: 2,
},
centerValue: {
fontSize: 14,
fontWeight: '600',
color: '#333333',
marginBottom: 1,
},
centerPercentage: {
fontSize: 11,
fontWeight: '500',
color: '#999999',
},
dataContainer: {
flex: 1,
marginLeft: 16,
},
dataBackground: {
backgroundColor: 'rgba(248, 250, 252, 0.8)', // 毛玻璃背景色
borderRadius: 12,
padding: 12,
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 1,
},
shadowOpacity: 0.06,
shadowRadius: 3,
elevation: 1,
// 添加边框增强毛玻璃效果
borderWidth: 0.5,
borderColor: 'rgba(255, 255, 255, 0.8)',
gap: 4,
},
dataItem: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
},
dataIcon: {
width: 6,
height: 6,
borderRadius: 3,
},
dataLabel: {
fontSize: 11,
fontWeight: '500',
color: '#999999',
minWidth: 28,
},
dataValue: {
fontSize: 11,
fontWeight: '600',
color: '#333333',
},
dataColumns: {
flexDirection: 'row',
justifyContent: 'space-between',
gap: 12,
},
dataColumn: {
flex: 1,
gap: 4,
},
});