- 在 Explore 页面中添加日期选择功能,允许用户查看指定日期的健康数据 - 重构健康数据获取逻辑,支持根据日期获取健康数据 - 在个人信息页面中集成用户资料编辑功能,支持姓名、性别、年龄、体重和身高的输入 - 新增 AnimatedNumber 和 CircularRing 组件,优化数据展示效果 - 更新 package.json 和 package-lock.json,添加 react-native-svg 依赖 - 修改布局以支持新功能的显示和交互
122 lines
3.3 KiB
TypeScript
122 lines
3.3 KiB
TypeScript
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);
|
||
|
||
|