React Native中TranslateY动画异常:Logo动效未达预期
修复React Native Logo动画逻辑
问题分析
原代码的核心问题在于动画初始值设置错误和动画序列逻辑颠倒:
- Logo初始
translateY值为0,而容器设置了justifyContent: 'center',导致Logo一开始就在中心位置,没有从底部进入的效果 - 初始
opacity为0,且第一个动画没有先将透明度恢复为1,导致Logo一开始不可见 - 动画序列的执行顺序不符合需求,没有先完成"从底部到中心"的入场动画
修复后的完整代码
import React, { useState, useEffect, useRef } from 'react'; import { View, Image, StyleSheet, Animated, TouchableOpacity, Text, Dimensions } from 'react-native'; import Modal from 'react-native-modal'; import PickerSelect from 'react-native-picker-select'; const { height: SCREEN_HEIGHT } = Dimensions.get('window'); const LOGO_HEIGHT = 200; // 计算初始Y偏移:让Logo初始在屏幕底部外一点的位置 const INITIAL_TRANSLATE_Y = (SCREEN_HEIGHT / 2) + (LOGO_HEIGHT / 2); const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', alignItems: 'center', }, logo: { width: LOGO_HEIGHT, height: LOGO_HEIGHT, resizeMode: 'contain', }, modalContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', }, modalContent: { backgroundColor: 'white', padding: 30, borderRadius: 10, width: '80%', }, chooseLanguageText: { marginBottom: 20, }, picker: { width: '100%', marginBottom: 20, borderBottomWidth: 1, borderColor: 'gray', paddingVertical: 10, }, nextButton: { backgroundColor: '#4CAF50', padding: 15, borderRadius: 5, alignItems: 'center', marginTop: 20, }, nextButtonText: { color: 'white', fontSize: 16, }, background: { ...StyleSheet.absoluteFillObject, backgroundColor: 'lightblue', opacity: 0, }, buttonContainer: { marginVertical: 10, opacity: 0, }, yellowButton: { backgroundColor: 'yellow', padding: 15, borderRadius: 10, marginVertical: 10, }, grayButton: { backgroundColor: 'gray', padding: 15, borderRadius: 10, marginVertical: 10, }, orangeButton: { backgroundColor: 'orange', padding: 15, borderRadius: 10, marginVertical: 10, }, greenButton: { backgroundColor: 'green', width: 50, height: 50, borderRadius: 25, marginVertical: 10, }, buttonText: { textAlign: 'center', color: 'black', }, grayButtonContainer: { flexDirection: 'row', }, }); const DisplayLogo = () => { const translateY = useRef(new Animated.Value(INITIAL_TRANSLATE_Y)).current; const opacity = useRef(new Animated.Value(0)).current; const buttonOpacity = useRef(new Animated.Value(0)).current; const backgroundOpacity = useRef(new Animated.Value(0)).current; const [isModalVisible, setModalVisible] = useState(false); const [selectedLanguage, setSelectedLanguage] = useState(''); const [selectedRegion, setSelectedRegion] = useState(''); const [step, setStep] = useState(1); const handleButtonPress = (buttonName) => { console.log(`Button pressed: ${buttonName}`); }; useEffect(() => { // 仅在初始加载时执行一次动画 if (step === 1) { const imageAnimation = Animated.sequence([ // 第一步:从底部移动到中心,同时显示Logo Animated.parallel([ Animated.timing(translateY, { toValue: 0, duration: 500, useNativeDriver: true, }), Animated.timing(opacity, { toValue: 1, duration: 500, useNativeDriver: true, }), ]), // 停留2秒 Animated.delay(2000), // 第二步:移动到顶部并淡出 Animated.parallel([ Animated.timing(translateY, { toValue: -(SCREEN_HEIGHT / 2) - (LOGO_HEIGHT / 2), duration: 500, useNativeDriver: true, }), Animated.timing(opacity, { toValue: 0, duration: 500, useNativeDriver: true, }), ]), ]); imageAnimation.start(() => { setModalVisible(true); }); } return () => { translateY.setValue(INITIAL_TRANSLATE_Y); opacity.setValue(0); }; }, [step]); const handleNext = () => { if (step === 1) { setStep(2); } else if (step === 2) { console.log('Selected Language:', selectedLanguage || 'English'); setModalVisible(false); Animated.parallel([ Animated.timing(buttonOpacity, { toValue: 1, duration: 500, useNativeDriver: true, }), Animated.timing(backgroundOpacity, { toValue: 1, duration: 500, useNativeDriver: true, }), ]).start(); console.log('Selected Region:', selectedRegion || 'North India'); } }; return ( <View style={styles.container}> <Animated.Image style={{ ...styles.logo, transform: [{ translateY }], opacity }} source={require('./assets/Logo.png')} /> <Modal isVisible={isModalVisible} animationIn="slideInUp" animationOut="slideOutDown" onBackdropPress={null} style={styles.modalContainer} > <View style={styles.modalContent}> {step === 1 && ( <> <Text style={styles.chooseLanguageText}>Select Your Region:</Text> <PickerSelect style={styles.picker} value={selectedRegion} onValueChange={(value) => setSelectedRegion(value)} items={[ { label: 'North India', value: 'North India' }, { label: 'South India', value: 'South India' }, ]} placeholder={{}} Icon={() => ( <View style={{ backgroundColor: 'transparent', borderTopWidth: 10, borderTopColor: 'gray', borderRightWidth: 10, borderRightColor: 'transparent', borderLeftWidth: 10, borderLeftColor: 'transparent', width: 0, height: 0, alignSelf: 'center', }} /> )} /> </> )} {step === 2 && ( <> <Text style={styles.chooseLanguageText}>Choose Your Preferred Language:</Text> <PickerSelect style={styles.picker} value={selectedLanguage} onValueChange={(value) => setSelectedLanguage(value)} items={[ { label: 'English', value: 'English' }, { label: 'Hindi', value: 'Hindi' }, { label: 'Telugu', value: 'Telugu' }, ]} placeholder={{}} Icon={() => ( <View style={{ backgroundColor: 'transparent', borderTopWidth: 10, borderTopColor: 'gray', borderRightWidth: 10, borderRightColor: 'transparent', borderLeftWidth: 10, borderLeftColor: 'transparent', width: 0, height: 0, alignSelf: 'center', }} /> )} /> </> )} <TouchableOpacity style={styles.nextButton} onPress={handleNext}> <Text style={styles.nextButtonText}>Next</Text> </TouchableOpacity> </View> </Modal> <Animated.View style={{ ...styles.background, opacity: backgroundOpacity }} /> <Animated.View style={{ ...styles.buttonContainer, opacity: buttonOpacity }}> {[ 'Tips For Farming', 'Weather Updates', 'Buy Implements', 'Sell Harvest', 'Discuss On Forums', 'Donate', ].map((buttonName, index) => ( <TouchableOpacity key={index} style={styles.yellowButton} onPress={() => handleButtonPress(buttonName)} > <Text style={styles.buttonText}>{buttonName}</Text> </TouchableOpacity> ))} </Animated.View> <Animated.View style={{ ...styles.buttonContainer, opacity: buttonOpacity }}> <View style={styles.grayButtonContainer}> {['Help', 'Settings'].map((buttonName, index) => ( <TouchableOpacity key={index} style={styles.grayButton} onPress={() => handleButtonPress(buttonName)} > <Text style={styles.buttonText}>{buttonName}</Text> </TouchableOpacity> ))} </View> </Animated.View> <Animated.View style={{ ...styles.buttonContainer, opacity: buttonOpacity }}> <TouchableOpacity style={styles.orangeButton} onPress={() => handleButtonPress('Return Feedback')} > <Text style={styles.buttonText}>Return Feedback</Text> </TouchableOpacity> </Animated.View> <Animated.View style={{ ...styles.buttonContainer, opacity: buttonOpacity }}> <TouchableOpacity style={styles.greenButton} onPress={() => handleButtonPress('Contact')} /> </Animated.View> </View> ); }; export default DisplayLogo;
关键优化点
- 动态计算初始位置:通过
Dimensions获取屏幕高度,计算出Logo初始在屏幕底部外的位置,确保动画从底部开始 - 修正动画序列:
- 先执行入场动画:同时将
translateY从初始值变为0(中心)、opacity从0变为1(显示Logo) - 延迟2秒后执行退场动画:同时将
translateY移到屏幕顶部外、opacity变为0(淡出)
- 先执行入场动画:同时将
- 简化依赖逻辑:仅在
step为1时执行初始动画,避免不必要的重复触发 - 精确的退场位置:计算顶部偏移量,确保Logo完全移出屏幕后消失
内容的提问来源于stack exchange,提问作者Aditya Raj
相关产品推荐
相关产品推荐

