diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index d3102a4..f766889 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -1,291 +1,183 @@ +import { Fonts } from '@/constants/Fonts'; import { checkAuthStatus } from '@/lib/auth'; import { useRouter } from 'expo-router'; -import React, { useEffect, useRef, useState } from 'react'; +import React, { useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { Animated, Dimensions, Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; +import { Dimensions, Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; +import Animated, { + interpolate, + useAnimatedStyle, + useSharedValue, + withDelay, + withRepeat, + withSequence, + withTiming +} from 'react-native-reanimated'; import { useSafeAreaInsets } from "react-native-safe-area-context"; +// Worklet function for animations +const runShakeAnimation = (value: Animated.SharedValue) => { + 'worklet'; + return withRepeat( + withSequence( + withTiming(1, { duration: 300 }), + withTiming(-1, { duration: 300 }), + withTiming(1, { duration: 300 }), + withTiming(-1, { duration: 300 }), + withTiming(0, { duration: 200 }), + withDelay(1000, withTiming(0, { duration: 0 })) + ), + -1 + ); +}; + +const runWaveAnimation = (value: Animated.SharedValue) => { + 'worklet'; + return withRepeat( + withSequence( + withTiming(1, { duration: 500 }), + withTiming(-1, { duration: 500 }), + withTiming(0, { duration: 500 }), + withDelay(1000, withTiming(0, { duration: 0 })) + ), + -1 + ); +}; + export default function HomeScreen() { const router = useRouter(); const { t } = useTranslation(); const insets = useSafeAreaInsets(); - const [isLoading, setIsLoading] = useState(false); - - // 获取屏幕宽度 + const [isLoading, setIsLoading] = React.useState(false); const screenWidth = Dimensions.get('window').width; - // 动画值 - const fadeAnim = useRef(new Animated.Value(0)).current; // IP图标的淡入动画 - const shakeAnim = useRef(new Animated.Value(0)).current; // IP图标的摇晃动画 - const animationRef = useRef(null); // 动画引用 - const descriptionAnim = useRef(new Animated.Value(0)).current; // 描述文本的淡入动画 - const buttonAnim = useRef(new Animated.Value(0)).current; // 按钮的淡入动画 - const buttonShakeAnim = useRef(new Animated.Value(0)).current; // 按钮的摇晃动画 - const buttonLoopAnim = useRef(null); // 按钮循环动画引用 - const fadeInAnim = useRef(new Animated.Value(0)).current; + // Animation values + const fadeAnim = useSharedValue(0); + const shakeAnim = useSharedValue(0); + const waveAnim = useSharedValue(0); + const buttonShakeAnim = useSharedValue(0); + const fadeInAnim = useSharedValue(0); + const descriptionAnim = useSharedValue(0); + const textAnimations = { + line1: useSharedValue(0), + line2: useSharedValue(0), + line3: useSharedValue(0), + subtitle: useSharedValue(0), + }; - // 文本行动画值 - const [textAnimations] = useState(() => ({ - line1: new Animated.Value(0), // 第一行文本动画 - line2: new Animated.Value(0), // 第二行文本动画 - line3: new Animated.Value(0), // 第三行文本动画 - subtitle: new Animated.Value(0), // 副标题动画 + // Animation styles + const ipAnimatedStyle = useAnimatedStyle(() => ({ + opacity: fadeAnim.value, + transform: [ + { translateX: interpolate(shakeAnim.value, [-1, 1], [-2, 2]) }, + { rotate: `${interpolate(shakeAnim.value, [-1, 1], [-2, 2])}deg` }, + ], })); - // 添加挥手动画值 - const waveAnim = useRef(new Animated.Value(0)).current; + const waveAnimatedStyle = useAnimatedStyle(() => ({ + transform: [ + { rotate: `${interpolate(waveAnim.value, [-1, 0, 1], [-15, 0, 15])}deg` }, + ], + })); - // 启动IP图标摇晃动画 - const startShaking = () => { - // 停止任何正在进行的动画 - if (animationRef.current) { - animationRef.current.stop(); - } + const buttonStyle = useAnimatedStyle(() => ({ + opacity: fadeInAnim.value, + transform: [ + { translateY: interpolate(fadeInAnim.value, [0, 1], [20, 0]) }, + { translateX: interpolate(buttonShakeAnim.value, [-1, 0, 1], [-5, 0, 5]) } + ] + })); - // 创建动画序列 - const sequence = Animated.sequence([ - // 第一次左右摇晃 - Animated.timing(shakeAnim, { - toValue: 1, - duration: 300, - useNativeDriver: true, - }), - Animated.timing(shakeAnim, { - toValue: -1, - duration: 300, - useNativeDriver: true, - }), - // 第二次左右摇晃 - Animated.timing(shakeAnim, { - toValue: 1, - duration: 300, - useNativeDriver: true, - }), - Animated.timing(shakeAnim, { - toValue: -1, - duration: 300, - useNativeDriver: true, - }), - // 回到中心位置 - Animated.timing(shakeAnim, { - toValue: 0, - duration: 200, - useNativeDriver: true, - }), - // 1秒延迟 - Animated.delay(1000), - ]); + const welcomeStyle = useAnimatedStyle(() => ({ + opacity: fadeInAnim.value, + transform: [{ translateY: interpolate(fadeInAnim.value, [0, 1], [20, 0]) }] + })); - // 循环播放动画序列 - animationRef.current = Animated.loop(sequence); - animationRef.current.start(); - }; + const descriptionStyle = useAnimatedStyle(() => ({ + opacity: descriptionAnim.value, + transform: [{ translateY: interpolate(descriptionAnim.value, [0, 1], [20, 0]) }] + })); - // 启动文本动画 - const startTextAnimations = () => { - // 按顺序延迟启动每行文本动画 - return new Promise((resolve) => { - Animated.stagger(300, [ - Animated.timing(textAnimations.line1, { - toValue: 1, - duration: 500, - useNativeDriver: true, - }), - Animated.timing(textAnimations.line2, { - toValue: 1, - duration: 500, - useNativeDriver: true, - }), - Animated.timing(textAnimations.line3, { - toValue: 1, - duration: 500, - useNativeDriver: true, - }), - Animated.timing(textAnimations.subtitle, { - toValue: 1, - duration: 500, - useNativeDriver: true, - }), - ]).start(() => resolve()); - }); - }; + const textLine1Style = useAnimatedStyle(() => ({ + opacity: textAnimations.line1.value, + transform: [{ translateY: interpolate(textAnimations.line1.value, [0, 1], [10, 0]) }] + })); - // 启动描述文本动画 - const startDescriptionAnimation = () => { - // IP图标显示后淡入描述文本 - return new Promise((resolve) => { - Animated.sequence([ - Animated.delay(200), // IP图标显示后延迟200ms - Animated.timing(descriptionAnim, { - toValue: 1, - duration: 800, - useNativeDriver: true, - }) - ]).start(() => resolve()); - }); - }; - // 启动欢迎语动画 - const startWelcomeAnimation = () => { - // IP图标显示后淡入描述文本 - return new Promise((resolve) => { - Animated.sequence([ - Animated.delay(200), // IP图标显示后延迟200ms - Animated.timing(fadeInAnim, { - toValue: 1, - duration: 800, - useNativeDriver: true, - }) - ]).start(() => resolve()); - }); - }; + const textLine2Style = useAnimatedStyle(() => ({ + opacity: textAnimations.line2.value, + transform: [{ translateY: interpolate(textAnimations.line2.value, [0, 1], [10, 0]) }] + })); - // 启动按钮动画 - const startButtonAnimation = () => { - // 首先淡入按钮 - Animated.sequence([ - Animated.timing(buttonAnim, { - toValue: 1, - duration: 800, - useNativeDriver: true, - }) - ]).start(() => { - // 淡入完成后开始循环摇晃动画 - startButtonShakeLoop(); - }); - }; + const textLine3Style = useAnimatedStyle(() => ({ + opacity: textAnimations.line3.value, + transform: [{ translateY: interpolate(textAnimations.line3.value, [0, 1], [10, 0]) }] + })); - // 启动按钮循环摇晃动画 - const startButtonShakeLoop = () => { - // 停止任何正在进行的动画 - if (buttonLoopAnim.current) { - buttonLoopAnim.current.stop(); - } + const subtitleStyle = useAnimatedStyle(() => ({ + opacity: textAnimations.subtitle.value, + transform: [{ translateY: interpolate(textAnimations.subtitle.value, [0, 1], [10, 0]) }] + })); - // 创建摇晃动画序列 - const shakeSequence = Animated.sequence([ - // 向右摇晃 - Animated.timing(buttonShakeAnim, { - toValue: 1, - duration: 100, - useNativeDriver: true, - }), - // 向左摇晃 - Animated.timing(buttonShakeAnim, { - toValue: -1, - duration: 100, - useNativeDriver: true, - }), - // 再次向右摇晃 - Animated.timing(buttonShakeAnim, { - toValue: 1, - duration: 100, - useNativeDriver: true, - }), - // 回到中心位置 - Animated.timing(buttonShakeAnim, { - toValue: 0, - duration: 100, - useNativeDriver: true, - }), - // 暂停3秒 - Animated.delay(3000) - ]); - - // 循环播放动画序列 - buttonLoopAnim.current = Animated.loop(shakeSequence); - buttonLoopAnim.current.start(); - }; - - // 启动挥手动画 - const startWaveAnimation = () => { - // 创建循环动画:左右摇摆 - Animated.loop( - Animated.sequence([ - Animated.timing(waveAnim, { - toValue: 1, - duration: 500, - useNativeDriver: true, - }), - Animated.timing(waveAnim, { - toValue: -1, - duration: 500, - useNativeDriver: true, - }), - Animated.timing(waveAnim, { - toValue: 0, - duration: 500, - useNativeDriver: true, - }), - Animated.delay(1000), // 暂停1秒 - ]) - ).start(); - }; - - // 组件挂载时启动动画 + // Start animations useEffect(() => { setIsLoading(true); + checkAuthStatus(router, () => { - router.replace('/ask') + router.replace('/ask'); }, false).then(() => { setIsLoading(false); }).catch(() => { setIsLoading(false); }); - // IP图标的淡入动画 - Animated.timing(fadeAnim, { - toValue: 1, - duration: 1000, - useNativeDriver: true, - }).start(() => { - // 淡入完成后开始摇晃动画 - startShaking(); - // IP显示后开始文本动画 - startTextAnimations() - .then(() => startWelcomeAnimation()) - .then(() => startDescriptionAnimation()) - .then(() => startButtonAnimation()) - .catch(console.error); - // 启动挥手动画 - startWaveAnimation(); + + // Start fade in animation + fadeAnim.value = withTiming(1, { duration: 1000 }, () => { + // Start shake animation + shakeAnim.value = runShakeAnimation(shakeAnim); + + // Start text animations with delays + textAnimations.line1.value = withDelay(0, withTiming(1, { duration: 500 })); + textAnimations.line2.value = withDelay(300, withTiming(1, { duration: 500 })); + textAnimations.line3.value = withDelay(600, withTiming(1, { duration: 500 })); + textAnimations.subtitle.value = withDelay(900, withTiming(1, { duration: 500 })); + + // Start welcome animation + fadeInAnim.value = withDelay(200, withTiming(1, { duration: 800 })); + + // Start description animation + descriptionAnim.value = withDelay(200, withTiming(1, { duration: 800 })); + + // Start button animation + fadeInAnim.value = withDelay(200, withTiming(1, { duration: 800 }, () => { + // Start button shake animation + buttonShakeAnim.value = withRepeat( + withSequence( + withTiming(1, { duration: 100 }), + withTiming(-1, { duration: 100 }), + withTiming(1, { duration: 100 }), + withTiming(0, { duration: 100 }), + withDelay(3000, withTiming(0, { duration: 0 })) + ), + -1 + ); + })); + + // Start wave animation + waveAnim.value = runWaveAnimation(waveAnim); }); - // 组件卸载时清理动画 + // Cleanup return () => { - if (buttonLoopAnim.current) { - buttonLoopAnim.current.stop(); - } - if (animationRef.current) { - animationRef.current.stop(); - } + fadeAnim.value = 0; + shakeAnim.value = 0; + waveAnim.value = 0; + buttonShakeAnim.value = 0; + fadeInAnim.value = 0; + descriptionAnim.value = 0; + Object.values(textAnimations).forEach(anim => anim.value = 0); }; - }, []); - // 动画样式 - const animatedStyle = { - opacity: fadeAnim, - transform: [ - { - translateX: shakeAnim.interpolate({ - inputRange: [-1, 1], - outputRange: [-2, 2], - }) - }, - { - rotate: shakeAnim.interpolate({ - inputRange: [-1, 1], - outputRange: ['-2deg', '2deg'], - }), - }, - ], - }; - - // 旋转动画插值 - const rotate = waveAnim.interpolate({ - inputRange: [-1, 0, 1], - outputRange: ['-15deg', '0deg', '15deg'], - }); - if (isLoading) { return ( @@ -297,86 +189,27 @@ export default function HomeScreen() { return ( - {/* 标题区域 */} - + {t('auth.welcomeAwaken.awaken', { ns: 'login' })} - + {t('auth.welcomeAwaken.your', { ns: 'login' })} - + {t('auth.welcomeAwaken.pm', { ns: 'login' })} - + {t('auth.welcomeAwaken.slogan', { ns: 'login' })} - {/* 欢迎语 */} + - + - {/* Animated IP */} - + - {/* 介绍文本 */} - + {t('auth.welcomeAwaken.gallery', { ns: 'login' })} {"\n"} {t('auth.welcomeAwaken.back', { ns: 'login' })} - {/* 唤醒按钮 */} - + { - router.push('/login'); - }} + onPress={() => router.push('/login')} activeOpacity={0.8} > @@ -485,17 +281,19 @@ const styles = StyleSheet.create({ }, titleText: { color: '#FFFFFF', - fontSize: 30, + fontSize: 32, fontWeight: 'bold', marginBottom: 12, textAlign: 'left', lineHeight: 36, + fontFamily: Fonts['quicksand'] }, subtitleText: { color: 'rgba(255, 255, 255, 0.85)', fontSize: 16, textAlign: 'left', lineHeight: 24, + fontFamily: Fonts['inter'] }, ipContainer: { alignItems: 'center', @@ -514,11 +312,12 @@ const styles = StyleSheet.create({ opacity: 0.9, paddingHorizontal: 40, marginTop: -16, + fontFamily: Fonts['inter'] }, awakenButton: { backgroundColor: '#FFFFFF', borderRadius: 28, - paddingVertical: 16, + paddingVertical: 20, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, @@ -532,5 +331,6 @@ const styles = StyleSheet.create({ color: '#4C320C', fontWeight: 'bold', fontSize: 18, + fontFamily: Fonts['quicksand'] }, }); \ No newline at end of file diff --git a/app/(tabs)/login.tsx b/app/(tabs)/login.tsx index 3c84020..46cd102 100644 --- a/app/(tabs)/login.tsx +++ b/app/(tabs)/login.tsx @@ -83,7 +83,7 @@ const LoginScreen = () => { className="absolute left-1/2 z-10" style={{ top: containerHeight > 0 ? windowHeight - containerHeight - 210 + statusBarHeight - insets.top - 28 : 0, - transform: [{ translateX: -200 }, { translateY: keyboardOffset > 0 ? -keyboardOffset + statusBarHeight - insets.top - 28 : -keyboardOffset }] + transform: [{ translateX: -200 }, { translateY: keyboardOffset > 0 ? -keyboardOffset + statusBarHeight : -keyboardOffset }] }} > { @@ -98,7 +98,7 @@ const LoginScreen = () => { className="absolute left-1/2 z-[1000] -translate-x-[39.5px] -translate-y-[4px]" style={{ top: containerHeight > 0 ? windowHeight - containerHeight - 1 + statusBarHeight - insets.top - 30 : 0, - transform: [{ translateX: -39.5 }, { translateY: keyboardOffset > 0 ? -4 - keyboardOffset + statusBarHeight - insets.top - 30 : -4 - keyboardOffset }] + transform: [{ translateX: -39.5 }, { translateY: keyboardOffset > 0 ? -4 - keyboardOffset + statusBarHeight : -4 - keyboardOffset }] }} > diff --git a/assets/images/png/icon/ip.png b/assets/images/png/icon/ip.png index 2355ed9..04e7239 100644 Binary files a/assets/images/png/icon/ip.png and b/assets/images/png/icon/ip.png differ