feat: 添加用户活动热力图组件

在个人页面新增活动热力图展示组件,并实现浮动动画效果优化统计卡片交互体验。
This commit is contained in:
richarjiang
2025-08-21 15:22:16 +08:00
parent b396a7d101
commit b93a863e25
4 changed files with 454 additions and 90 deletions

View File

@@ -19,6 +19,7 @@ import dayjs from 'dayjs';
import { LinearGradient } from 'expo-linear-gradient';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import {
Animated,
SafeAreaView,
ScrollView,
StyleSheet,
@@ -28,6 +29,56 @@ import {
} from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
// 浮动动画组件
const FloatingCard = ({ children, delay = 0, style }: {
children: React.ReactNode;
delay?: number;
style?: any;
}) => {
const floatAnim = useRef(new Animated.Value(0)).current;
useEffect(() => {
const startAnimation = () => {
Animated.loop(
Animated.sequence([
Animated.timing(floatAnim, {
toValue: 1,
duration: 3000,
delay: delay,
useNativeDriver: true,
}),
Animated.timing(floatAnim, {
toValue: 0,
duration: 3000,
useNativeDriver: true,
}),
])
).start();
};
startAnimation();
}, [floatAnim, delay]);
const translateY = floatAnim.interpolate({
inputRange: [0, 1],
outputRange: [-2, -6],
});
return (
<Animated.View
style={[
style,
{
transform: [{ translateY }],
marginBottom: 8,
},
]}
>
{children}
</Animated.View>
);
};
export default function ExploreScreen() {
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
const colorTokens = Colors[theme];
@@ -309,14 +360,15 @@ export default function ExploreScreen() {
<View style={styles.masonryContainer}>
{/* 左列 */}
<View style={styles.masonryColumn}>
<StressMeter
value={hrvValue}
updateTime={hrvUpdateTime}
style={styles.masonryCard}
hrvValue={hrvValue}
/>
<FloatingCard style={styles.masonryCard} delay={0}>
<StressMeter
value={hrvValue}
updateTime={hrvUpdateTime}
hrvValue={hrvValue}
/>
</FloatingCard>
<View style={[styles.masonryCard, styles.caloriesCard]}>
<FloatingCard style={[styles.masonryCard, styles.caloriesCard]} delay={500}>
<Text style={styles.cardTitleSecondary}></Text>
{activeCalories != null ? (
<AnimatedNumber
@@ -328,9 +380,9 @@ export default function ExploreScreen() {
) : (
<Text style={styles.caloriesValue}></Text>
)}
</View>
</FloatingCard>
<View style={[styles.masonryCard, styles.stepsCard]}>
<FloatingCard style={[styles.masonryCard, styles.stepsCard]} delay={1000}>
<View style={styles.cardHeaderRow}>
<Text style={styles.cardTitle}></Text>
</View>
@@ -351,31 +403,33 @@ export default function ExploreScreen() {
fillColor="#FFC365"
showLabel={false}
/>
</View>
</FloatingCard>
</View>
{/* 右列 */}
<View style={styles.masonryColumn}>
<BMICard
weight={userProfile?.weight ? parseFloat(userProfile.weight) : undefined}
height={userProfile?.height ? parseFloat(userProfile.height) : undefined}
style={styles.masonryCardNoBg}
// compact={true}
/>
<FloatingCard style={styles.masonryCard} delay={250}>
<BMICard
weight={userProfile?.weight ? parseFloat(userProfile.weight) : undefined}
height={userProfile?.height ? parseFloat(userProfile.height) : undefined}
style={styles.bmiCardOverride}
// compact={true}
/>
</FloatingCard>
<FitnessRingsCard
activeCalories={fitnessRingsData.activeCalories}
activeCaloriesGoal={fitnessRingsData.activeCaloriesGoal}
exerciseMinutes={fitnessRingsData.exerciseMinutes}
exerciseMinutesGoal={fitnessRingsData.exerciseMinutesGoal}
standHours={fitnessRingsData.standHours}
standHoursGoal={fitnessRingsData.standHoursGoal}
resetToken={animToken}
style={styles.masonryCard}
/>
<FloatingCard style={styles.masonryCard} delay={750}>
<FitnessRingsCard
activeCalories={fitnessRingsData.activeCalories}
activeCaloriesGoal={fitnessRingsData.activeCaloriesGoal}
exerciseMinutes={fitnessRingsData.exerciseMinutes}
exerciseMinutesGoal={fitnessRingsData.exerciseMinutesGoal}
standHours={fitnessRingsData.standHours}
standHoursGoal={fitnessRingsData.standHoursGoal}
resetToken={animToken}
/>
</FloatingCard>
<View style={[styles.masonryCard, styles.sleepCard]}>
<FloatingCard style={[styles.masonryCard, styles.sleepCard]} delay={1250}>
<View style={styles.cardHeaderRow}>
<Text style={styles.cardTitle}></Text>
</View>
@@ -386,7 +440,7 @@ export default function ExploreScreen() {
) : (
<Text style={styles.sleepValue}></Text>
)}
</View>
</FloatingCard>
</View>
</View>
@@ -713,11 +767,10 @@ const styles = StyleSheet.create({
masonryContainer: {
marginBottom: 16,
flexDirection: 'row',
justifyContent: 'space-between',
gap: 12,
},
masonryColumn: {
flex: 1,
marginHorizontal: 3,
},
masonryCard: {
width: '100%',
@@ -727,26 +780,15 @@ const styles = StyleSheet.create({
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 2,
height: 4,
},
shadowOpacity: 0.08,
shadowRadius: 8,
elevation: 3,
marginBottom: 8,
shadowOpacity: 0.12,
shadowRadius: 12,
elevation: 6,
},
masonryCardNoBg: {
width: '100%',
bmiCardOverride: {
margin: -16, // 抵消 masonryCard 的 padding
borderRadius: 16,
padding: 16,
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.08,
shadowRadius: 8,
elevation: 3,
marginBottom: 8,
},
compactStepsCard: {
minHeight: 100,