feat: 动画+验证

This commit is contained in:
jinyaqiu 2025-08-01 17:20:09 +08:00
parent e6a8faa610
commit d996b4dab6
3 changed files with 412 additions and 39 deletions

View File

@ -166,7 +166,7 @@ export default function AskScreen() {
>
<ReturnArrow />
</TouchableOpacity>
<ThemedText style={styles.title}>MemoWake</ThemedText>
<ThemedText style={styles.title} onPress={() => { router.push('/owner') }}>MemoWake</ThemedText>
<View style={styles.placeholder} />
</View>

View File

@ -1,17 +1,182 @@
import IP from '@/assets/icons/svg/ip.svg';
import { checkAuthStatus } from '@/lib/auth';
import { useRouter } from 'expo-router';
import React, { useEffect, useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Text, TouchableOpacity, View } from 'react-native';
import { Animated, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { useSafeAreaInsets } from "react-native-safe-area-context";
export default function HomeScreen() {
const router = useRouter();
const { t } = useTranslation();
const insets = useSafeAreaInsets();
const [isLoading, setIsLoading] = useState(true);
const [isLoading, setIsLoading] = useState(false);
// 动画值
const fadeAnim = useRef(new Animated.Value(0)).current; // IP图标的淡入动画
const shakeAnim = useRef(new Animated.Value(0)).current; // IP图标的摇晃动画
const animationRef = useRef<Animated.CompositeAnimation | null>(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<Animated.CompositeAnimation | null>(null); // 按钮循环动画引用
// 文本行动画值
const [textAnimations] = useState(() => ({
line1: new Animated.Value(0), // 第一行文本动画
line2: new Animated.Value(0), // 第二行文本动画
line3: new Animated.Value(0), // 第三行文本动画
subtitle: new Animated.Value(0), // 副标题动画
}));
// 启动IP图标摇晃动画
const startShaking = () => {
// 停止任何正在进行的动画
if (animationRef.current) {
animationRef.current.stop();
}
// 创建动画序列
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),
]);
// 循环播放动画序列
animationRef.current = Animated.loop(sequence);
animationRef.current.start();
};
// 启动文本动画
const startTextAnimations = () => {
// 按顺序延迟启动每行文本动画
return new Promise<void>((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 startDescriptionAnimation = () => {
// IP图标显示后淡入描述文本
return new Promise<void>((resolve) => {
Animated.sequence([
Animated.delay(200), // IP图标显示后延迟200ms
Animated.timing(descriptionAnim, {
toValue: 1,
duration: 800,
useNativeDriver: true,
})
]).start(() => resolve());
});
};
// 启动按钮动画
const startButtonAnimation = () => {
// 首先淡入按钮
Animated.sequence([
Animated.timing(buttonAnim, {
toValue: 1,
duration: 800,
useNativeDriver: true,
})
]).start(() => {
// 淡入完成后开始循环摇晃动画
startButtonShakeLoop();
});
};
// 启动按钮循环摇晃动画
const startButtonShakeLoop = () => {
// 停止任何正在进行的动画
if (buttonLoopAnim.current) {
buttonLoopAnim.current.stop();
}
// 创建摇晃动画序列
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();
};
// 组件挂载时启动动画
useEffect(() => {
setIsLoading(true);
checkAuthStatus(router, () => {
@ -21,58 +186,266 @@ export default function HomeScreen() {
}).catch(() => {
setIsLoading(false);
});
// IP图标的淡入动画
Animated.timing(fadeAnim, {
toValue: 1,
duration: 1000,
useNativeDriver: true,
}).start(() => {
// 淡入完成后开始摇晃动画
startShaking();
// IP显示后开始文本动画
startTextAnimations()
.then(() => startDescriptionAnimation())
.then(() => startButtonAnimation())
.catch(console.error);
});
// 组件卸载时清理动画
return () => {
if (buttonLoopAnim.current) {
buttonLoopAnim.current.stop();
}
if (animationRef.current) {
animationRef.current.stop();
}
};
}, []);
// 动画样式
const animatedStyle = {
opacity: fadeAnim,
transform: [
{
translateX: shakeAnim.interpolate({
inputRange: [-1, 1],
outputRange: [-2, 2],
})
},
{
rotate: shakeAnim.interpolate({
inputRange: [-1, 1],
outputRange: ['-2deg', '2deg'],
}),
},
],
};
if (isLoading) {
return (
<View className="flex-1 bg-bgPrimary justify-center items-center">
<Text className="text-white">...</Text>
<View style={styles.loadingContainer}>
<Text style={styles.loadingText}>{t('common.loading')}</Text>
</View>
);
}
return (
<View className="flex-1">
<View className="flex-1 bg-bgPrimary px-[1rem] h-screen overflow-auto py-[2rem] " style={{ paddingTop: insets.top + 48 }}>
<View style={styles.container}>
<View style={[styles.contentContainer, { paddingTop: insets.top + 16 }]}>
{/* 标题区域 */}
<View className="mb-10 w-full px-5">
<Text className="text-white text-3xl font-bold mb-3 text-left">
<View style={styles.headerContainer}>
<Animated.Text
style={[
styles.titleText,
{
opacity: textAnimations.line1, transform: [{
translateY: textAnimations.line1.interpolate({
inputRange: [0, 1],
outputRange: [10, 0]
})
}]
}
]}
>
{t('auth.welcomeAwaken.awaken', { ns: 'login' })}
{"\n"}
</Animated.Text>
<Animated.Text
style={[
styles.titleText,
{
opacity: textAnimations.line2, transform: [{
translateY: textAnimations.line2.interpolate({
inputRange: [0, 1],
outputRange: [10, 0]
})
}]
}
]}
>
{t('auth.welcomeAwaken.your', { ns: 'login' })}
{"\n"}
</Animated.Text>
<Animated.Text
style={[
styles.titleText,
{
opacity: textAnimations.line3, transform: [{
translateY: textAnimations.line3.interpolate({
inputRange: [0, 1],
outputRange: [10, 0]
})
}]
}
]}
>
{t('auth.welcomeAwaken.pm', { ns: 'login' })}
</Text>
<Text className="text-white/85 text-base text-left">
</Animated.Text>
<Animated.Text
style={[
styles.subtitleText,
{
opacity: textAnimations.subtitle,
transform: [{
translateY: textAnimations.subtitle.interpolate({
inputRange: [0, 1],
outputRange: [10, 0]
})
}]
}
]}
>
{t('auth.welcomeAwaken.slogan', { ns: 'login' })}
</Text>
</Animated.Text>
</View>
{/* Memo 形象区域 */}
<View className="items-center">
<IP />
{/* Animated IP */}
<View style={styles.ipContainer}>
<Animated.View style={[styles.ipWrapper, animatedStyle]}>
<IP />
</Animated.View>
</View>
{/* 介绍文本 */}
<Text className="text-white text-base text-center mb-[1rem] leading-6 opacity-90 px-10 -mt-[4rem]">
<Animated.Text
style={[
styles.descriptionText,
{
opacity: descriptionAnim,
transform: [{
translateY: descriptionAnim.interpolate({
inputRange: [0, 1],
outputRange: [20, 0]
})
}]
}
]}
>
{t('auth.welcomeAwaken.gallery', { ns: 'login' })}
{"\n"}
{t('auth.welcomeAwaken.back', { ns: 'login' })}
</Text>
</Animated.Text>
{/* 唤醒按钮 */}
<TouchableOpacity
className="bg-white rounded-full px-10 py-4 shadow-[0_2px_4px_rgba(0,0,0,0.1)] w-full items-center"
onPress={async () => {
router.push('/login')
<Animated.View
style={{
opacity: buttonAnim,
transform: [
{
translateY: buttonAnim.interpolate({
inputRange: [0, 1],
outputRange: [20, 0]
})
},
{
translateX: buttonShakeAnim.interpolate({
inputRange: [-1, 0, 1],
outputRange: [-5, 0, 5]
})
}
]
}}
activeOpacity={0.8}
>
<Text className="text-[#4C320C] font-bold text-lg">
{t('auth.welcomeAwaken.awake', { ns: 'login' })}
</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.awakenButton}
onPress={async () => {
router.push('/login');
}}
activeOpacity={0.8}
>
<Text style={styles.buttonText}>
{t('auth.welcomeAwaken.awake', { ns: 'login' })}
</Text>
</TouchableOpacity>
</Animated.View>
</View>
</View >
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#FFB645',
},
loadingContainer: {
flex: 1,
backgroundColor: '#FFB645',
justifyContent: 'center',
alignItems: 'center',
},
loadingText: {
color: '#FFFFFF',
fontSize: 16,
},
contentContainer: {
flex: 1,
backgroundColor: '#FFB645',
paddingHorizontal: 16,
paddingBottom: 32,
},
headerContainer: {
marginBottom: 40,
width: '100%',
paddingHorizontal: 20,
},
titleText: {
color: '#FFFFFF',
fontSize: 30,
fontWeight: 'bold',
marginBottom: 12,
textAlign: 'left',
lineHeight: 36,
},
subtitleText: {
color: 'rgba(255, 255, 255, 0.85)',
fontSize: 16,
textAlign: 'left',
lineHeight: 24,
},
ipContainer: {
alignItems: 'center',
marginBottom: 16,
minHeight: 200,
},
ipWrapper: {
alignItems: 'center',
justifyContent: 'center',
},
descriptionText: {
color: '#FFFFFF',
fontSize: 16,
textAlign: 'center',
lineHeight: 24,
opacity: 0.9,
paddingHorizontal: 40,
marginTop: -16,
},
awakenButton: {
backgroundColor: '#FFFFFF',
borderRadius: 28,
paddingVertical: 16,
paddingHorizontal: 40,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 2,
width: '100%',
alignItems: 'center',
marginTop: 24,
},
buttonText: {
color: '#4C320C',
fontWeight: 'bold',
fontSize: 18,
},
});

View File

@ -74,14 +74,14 @@ function CarouselComponent(props: Props) {
height={width * 0.75}
data={carouselDataValue || []}
mode="parallax"
defaultIndex={
carouselDataValue?.length
? Math.max(0, Math.min(
carouselDataValue.length - 1,
carouselDataValue.findIndex((item) => item?.key === 'total_count') - 1
))
: 0
}
// defaultIndex={
// carouselDataValue?.length
// ? Math.max(0, Math.min(
// carouselDataValue.length - 1,
// carouselDataValue.findIndex((item) => item?.key === 'total_count') - 1
// ))
// : 0
// }
modeConfig={{
parallaxScrollingScale: 1,
parallaxScrollingOffset: 150,