import { useEffect, useLayoutEffect, useMemo, useState, useCallback, useRef } from 'react'; import { StyleSheet, View, Dimensions, Text, Pressable, PanResponder, Animated as RNAnimated } from 'react-native'; import { useTranslation } from 'react-i18next'; import { useNavigation, useFocusEffect } from 'expo-router'; import Animated, { Easing, runOnJS, useAnimatedStyle, useSharedValue, withSequence, withTiming, } from 'react-native-reanimated'; import { MOCK_CONTENT } from '@/src/constants/mockContent'; import { addFavorite, getThemeMode, getUserProfile, setReaction, setThemeMode, type ThemeMode, } from '@/src/storage/appStorage'; import ProfileModal from '@/components/home/ProfileModal'; import ThemeModal from '@/components/home/ThemeModal'; import ThemeIcon from '@/assets/images/home/theme.svg'; import MyIcon from '@/assets/images/home/my.svg'; import LikeFilledIcon from '@/assets/images/home/like_filled.svg'; import LikeIcon from '@/assets/images/icon/like_icon.svg'; const { height: SCREEN_HEIGHT } = Dimensions.get('window'); export default function HomeScreen() { const { t } = useTranslation(); const navigation = useNavigation(); const [index, setIndex] = useState(0); const [themeMode, setThemeModeState] = useState('scenery'); const [themeOpen, setThemeOpen] = useState(false); const [profileOpen, setProfileOpen] = useState(false); const [profileName, setProfileName] = useState(undefined); const [busy, setBusy] = useState(false); const [likeFilled, setLikeFilled] = useState(false); const item = useMemo(() => MOCK_CONTENT[index % MOCK_CONTENT.length], [index]); // 动画相关 Shared Values const translateY = useSharedValue(0); const opacity = useSharedValue(1); const likeScale = useSharedValue(1); // 每次进入页面或页面获得焦点时刷新个人信息 useFocusEffect( useCallback(() => { let cancelled = false; (async () => { const mode = await getThemeMode(); const profile = await getUserProfile(); if (cancelled) return; setThemeModeState(mode); setProfileName(profile.name); })(); return () => { cancelled = true; }; }, []) ); const backgroundColor = themeMode === 'color' ? '#F3D0E1' : '#F4D6C2'; useLayoutEffect(() => { navigation.setOptions({ headerShadowVisible: false, headerStyle: { backgroundColor }, headerRight: () => ( setThemeOpen(true)} accessibilityLabel={t('home.theme')} > setProfileOpen(true)} accessibilityLabel={t('home.profile')} > ), }); }, [backgroundColor, navigation, t]); const textAnimatedStyle = useAnimatedStyle(() => ({ transform: [{ translateY: translateY.value }], opacity: opacity.value, })); const likeAnimatedStyle = useAnimatedStyle(() => ({ transform: [{ scale: likeScale.value }], })); // 切换到下一条文案的统一动画逻辑 const triggerNextContent = useCallback(() => { if (busy) return; setBusy(true); // 1. 当前文案向上移动并消失 translateY.value = withTiming(-40, { duration: 300, easing: Easing.out(Easing.quad) }); opacity.value = withTiming(0, { duration: 300 }, (finished) => { if (finished) { // 2. 切换数据索引 runOnJS(setIndex)(index + 1); runOnJS(setLikeFilled)(false); // 3. 准备下一条文案:先瞬移到下方 40pt translateY.value = 40; // 4. 下一条文案向上移动到原位并显现 translateY.value = withTiming(0, { duration: 400, easing: Easing.out(Easing.back(1)) }); opacity.value = withTiming(1, { duration: 400 }, (finished) => { if (finished) { runOnJS(setBusy)(false); } }); } }); }, [busy, index, translateY, opacity]); const lastTapRef = useRef(0); // 使用 Ref 解决 PanResponder 闭包陷阱,确保手势回调能拿到最新的 state 和 function const handlersRef = useRef({ onPressLike, triggerNextContent }); useEffect(() => { handlersRef.current = { onPressLike, triggerNextContent }; }, [onPressLike, triggerNextContent]); // 使用系统自带的 PanResponder 代替第三方手势库 const panResponder = useRef( PanResponder.create({ onStartShouldSetPanResponder: () => true, // 允许开始捕获 onMoveShouldSetPanResponder: (_, gestureState) => { // 只有当垂直滑动距离大于 20 时才接管位移手势 return Math.abs(gestureState.dy) > 20; }, onPanResponderRelease: (_, gestureState) => { const now = Date.now(); const DOUBLE_TAP_DELAY = 300; // 1. 双击逻辑判定 if (now - lastTapRef.current < DOUBLE_TAP_DELAY) { // 判定为双击 if (Math.abs(gestureState.dx) < 10 && Math.abs(gestureState.dy) < 10) { runOnJS(handlersRef.current.onPressLike)(); lastTapRef.current = 0; // 重置 return; } } lastTapRef.current = now; // 2. 上滑逻辑判定 if (gestureState.dy < -50) { // 上滑超过 50pt runOnJS(handlersRef.current.triggerNextContent)(); } }, }) ).current; async function onPressLike() { if (busy) return; setLikeFilled(true); // 1. 获取当前日期 const now = new Date(); const dateStr = `${now.getFullYear()}.${String(now.getMonth() + 1).padStart(2, '0')}.${String(now.getDate()).padStart(2, '0')}`; // 2. 保存到收藏夹,包含当前背景信息 await addFavorite({ id: item.id, date: dateStr, themeMode: themeMode, background: backgroundColor, // 目前存储的是颜色值 }); // 3. 爱心缩放动画 likeScale.value = withSequence( withTiming(0.8, { duration: 100 }), withTiming(1.2, { duration: 150 }), withTiming(1, { duration: 100 }, (finished) => { if (finished) { runOnJS(triggerNextContent)(); } }) ); } async function onSelectTheme(next: ThemeMode) { setThemeModeState(next); await setThemeMode(next); setThemeOpen(false); } return ( {item.text} {likeFilled ? ( ) : ( )} setThemeOpen(false)} /> setProfileOpen(false)} /> ); } function CircleIconButton({ onPress, accessibilityLabel, children, }: { onPress: () => void; accessibilityLabel: string; children: React.ReactNode; }) { return ( {children} ); } const styles = StyleSheet.create({ container: { flex: 1, padding: 20, justifyContent: 'center', alignItems: 'center', }, headerRight: { flexDirection: 'row', gap: 10, paddingRight: 10, }, circleBtn: { width: 34, height: 34, borderRadius: 17, backgroundColor: 'rgba(255,255,255,0.75)', alignItems: 'center', justifyContent: 'center', }, card: { paddingHorizontal: 30, alignItems: 'center', justifyContent: 'center', }, text: { fontSize: 22, lineHeight: 32, color: '#5E2A28', fontWeight: '700', textAlign: 'center', }, actions: { position: 'absolute', bottom: SCREEN_HEIGHT * 0.16, left: 0, right: 0, flexDirection: 'row', justifyContent: 'center', }, reactionButton: { alignItems: 'center', justifyContent: 'center', }, reactionInner: { alignItems: 'center', justifyContent: 'center', }, });