Files
digital-pilates/components/CircularRing.tsx
richarjiang 8ffebfb297 feat: 更新健康数据功能和用户个人信息页面
- 在 Explore 页面中添加日期选择功能,允许用户查看指定日期的健康数据
- 重构健康数据获取逻辑,支持根据日期获取健康数据
- 在个人信息页面中集成用户资料编辑功能,支持姓名、性别、年龄、体重和身高的输入
- 新增 AnimatedNumber 和 CircularRing 组件,优化数据展示效果
- 更新 package.json 和 package-lock.json,添加 react-native-svg 依赖
- 修改布局以支持新功能的显示和交互
2025-08-12 18:54:15 +08:00

122 lines
3.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useEffect, useMemo, useRef } from 'react';
import { Animated, Easing, StyleSheet, Text, View } from 'react-native';
import Svg, { Circle, G } from 'react-native-svg';
type CircularRingProps = {
size?: number;
strokeWidth?: number;
trackColor?: string;
progressColor?: string;
progress: number; // 0..1
durationMs?: number;
showCenterText?: boolean;
/** 当该值变化时会从0重新动画到 progress */
resetToken?: unknown;
/** 进度起始角度(度),默认 -90使进度从正上方开始 */
startAngleDeg?: number;
};
/**
* 纯 View 实现的圆环进度条(无依赖),通过两个半圆与旋转实现。
*/
export function CircularRing({
size = 120,
strokeWidth = 12,
trackColor = '#E2D9FD',
progressColor = '#8B74F3',
progress,
durationMs = 900,
showCenterText = true,
resetToken,
startAngleDeg = -90,
}: CircularRingProps) {
const clamped = useMemo(() => {
if (Number.isNaN(progress)) return 0;
return Math.min(1, Math.max(0, progress));
}, [progress]);
const animated = useRef(new Animated.Value(0)).current;
useEffect(() => {
// 每次 resetToken 或目标进度变化时从0动画到 clamped
animated.stopAnimation(() => {
animated.setValue(0);
Animated.timing(animated, {
toValue: clamped,
duration: durationMs,
easing: Easing.out(Easing.cubic),
useNativeDriver: false,
}).start();
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [clamped, resetToken]);
const radius = useMemo(() => (size - strokeWidth) / 2, [size, strokeWidth]);
const circumference = useMemo(() => 2 * Math.PI * radius, [radius]);
const dashOffset = animated.interpolate({
inputRange: [0, 1],
outputRange: [circumference, 0],
});
const percentText = useMemo(() => `${Math.round(clamped * 100)}%`, [clamped]);
const containerStyle = useMemo(() => [styles.container, { width: size, height: size }], [size]);
return (
<View style={containerStyle}>
<Svg width={size} height={size}>
<G rotation={startAngleDeg} originX={size / 2} originY={size / 2}>
{/* 轨道 */}
<Circle
cx={size / 2}
cy={size / 2}
r={radius}
stroke={trackColor}
strokeWidth={strokeWidth}
strokeLinecap="round"
fill="none"
/>
{/* 进度 */}
<AnimatedCircle
cx={size / 2}
cy={size / 2}
r={radius}
stroke={progressColor}
strokeWidth={strokeWidth}
strokeDasharray={`${circumference} ${circumference}`}
strokeDashoffset={dashOffset as unknown as number}
strokeLinecap="round"
fill="none"
/>
</G>
</Svg>
{showCenterText && (
<View style={styles.center}>
<Text style={styles.centerText}>{percentText}</Text>
</View>
)}
</View>
);
}
const styles = StyleSheet.create({
container: {
alignItems: 'center',
justifyContent: 'center',
alignSelf: 'center',
},
center: {
...StyleSheet.absoluteFillObject,
alignItems: 'center',
justifyContent: 'center',
},
centerText: {
fontSize: 18,
fontWeight: '800',
color: '#8B74F3',
},
});
const AnimatedCircle = Animated.createAnimatedComponent(Circle);